mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 02:10:12 -06:00
Compare commits
1 Commits
ReviewBot/
...
ReviewBot/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
008bb4c7dc |
@@ -12,8 +12,8 @@
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": ["dbaeumer.vscode-eslint"],
|
||||
},
|
||||
"extensions": ["dbaeumer.vscode-eslint"]
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
@@ -25,5 +25,5 @@
|
||||
"postAttachCommand": "pnpm dev --filter=web... --filter=demo...",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
"remoteUser": "node"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
# **/node_modules
|
||||
**/node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store/
|
||||
|
||||
43
.env.example
43
.env.example
@@ -1,7 +1,8 @@
|
||||
########################################################################
|
||||
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------ #
|
||||
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------#
|
||||
########################################################################
|
||||
|
||||
|
||||
############
|
||||
# BASICS #
|
||||
############
|
||||
@@ -50,28 +51,13 @@ SMTP_SECURE_ENABLED=0
|
||||
SMTP_USER=smtpUser
|
||||
SMTP_PASSWORD=smtpPassword
|
||||
|
||||
|
||||
########################################################################
|
||||
# ------------------------------ OPTIONAL -----------------------------#
|
||||
########################################################################
|
||||
|
||||
# Uncomment the variables you would like to use and customize the values.
|
||||
|
||||
# Custom local storage path for file uploads
|
||||
#UPLOADS_DIR=
|
||||
|
||||
##############
|
||||
# S3 STORAGE #
|
||||
##############
|
||||
|
||||
# S3 Storage is required for the file upload in serverless environments like Vercel
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
S3_REGION=
|
||||
S3_BUCKET_NAME=
|
||||
# Configure a third party S3 compatible storage service endpoint like StorJ leave empty if you use Amazon S3
|
||||
# e.g., https://gateway.storjshare.io
|
||||
S3_ENDPOINT_URL=
|
||||
|
||||
#####################
|
||||
# Disable Features #
|
||||
#####################
|
||||
@@ -85,9 +71,6 @@ PASSWORD_RESET_DISABLED=1
|
||||
# Signup. Disable the ability for new users to create an account.
|
||||
# SIGNUP_DISABLED=1
|
||||
|
||||
# Email login. Disable the ability for users to login with email.
|
||||
# EMAIL_AUTH_DISABLED=1
|
||||
|
||||
# Team Invite. Disable the ability for invited users to create an account.
|
||||
# INVITE_DISABLED=1
|
||||
|
||||
@@ -101,25 +84,21 @@ TERMS_URL=
|
||||
IMPRINT_URL=
|
||||
|
||||
# Configure Github Login
|
||||
GITHUB_AUTH_ENABLED=0
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
# Configure Google Login
|
||||
GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Configure Azure Active Directory Login
|
||||
AZUREAD_AUTH_ENABLED=0
|
||||
AZUREAD_CLIENT_ID=
|
||||
AZUREAD_CLIENT_SECRET=
|
||||
AZUREAD_TENANT_ID=
|
||||
|
||||
# OpenID Connect (OIDC) configuration
|
||||
# OIDC_CLIENT_ID=
|
||||
# OIDC_CLIENT_SECRET=
|
||||
# OIDC_ISSUER=
|
||||
# OIDC_DISPLAY_NAME=
|
||||
# OIDC_SIGNING_ALGORITHM=
|
||||
|
||||
# Cron Secret
|
||||
CRON_SECRET=
|
||||
|
||||
@@ -139,12 +118,12 @@ NEXT_PUBLIC_FORMBRICKS_API_HOST=
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
|
||||
|
||||
# Oauth credentials for Google sheet integration
|
||||
# Oauth credentials for Google sheet integration
|
||||
GOOGLE_SHEETS_CLIENT_ID=
|
||||
GOOGLE_SHEETS_CLIENT_SECRET=
|
||||
GOOGLE_SHEETS_REDIRECT_URL=
|
||||
|
||||
# Oauth credentials for Airtable integration
|
||||
# Oauth credentials for Airtable integration
|
||||
AIRTABLE_CLIENT_ID=
|
||||
|
||||
# Enterprise License Key
|
||||
@@ -162,9 +141,3 @@ ENTERPRISE_LICENSE_KEY=
|
||||
# Send new users to customer.io
|
||||
# CUSTOMER_IO_API_KEY=
|
||||
# CUSTOMER_IO_SITE_ID=
|
||||
|
||||
# Ignore Rate Limiting across the Formbricks app
|
||||
# RATE_LIMITING_DISABLED=1
|
||||
|
||||
# OpenTelemetry URL for tracing
|
||||
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -32,6 +32,7 @@ Fixes # (issue)
|
||||
- [ ] Removed all `console.logs`
|
||||
- [ ] Merged the latest changes from main onto my branch with `git pull origin main`
|
||||
- [ ] My changes don't cause any responsiveness issues
|
||||
- [ ] First PR at Formbricks? [Please sign the CLA!](https://formbricks.com/clmyhzfrymr4ko00hycsg1tvx) Without it we wont be able to merge it 🙏
|
||||
|
||||
### Appreciated
|
||||
|
||||
|
||||
55
.github/actions/cache-build-web/action.yml
vendored
55
.github/actions/cache-build-web/action.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Build & Cache Web App
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Cache Build
|
||||
uses: actions/cache@v3
|
||||
id: cache-build
|
||||
env:
|
||||
cache-name: prod-build
|
||||
key-1: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/web/.next
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
pnpm build --filter=web...
|
||||
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
@@ -1,10 +0,0 @@
|
||||
name: Dangerous git Checkout
|
||||
description: "Git Checkout from PR code so we can run checks from forks"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
6
.github/workflows/build-formbricks-com.yml
vendored
6
.github/workflows/build-formbricks-com.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build formbricks-com
|
||||
name: Build
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
10
.github/workflows/build-web.yml
vendored
10
.github/workflows/build-web.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build web
|
||||
name: Build
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
@@ -8,13 +8,13 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
41
.github/workflows/e2e.yml
vendored
41
.github/workflows/e2e.yml
vendored
@@ -7,18 +7,29 @@ jobs:
|
||||
name: Run E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build & Cache Web Binaries
|
||||
uses: ./.github/actions/cache-build-web
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
- name: Install Docker Compose
|
||||
run: sudo apt-get update && sudo apt-get install -y docker-compose
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: npm install -g pnpm && pnpm install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate ENCRYPTION_KEY
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Start PostgreSQL
|
||||
run: |
|
||||
@@ -32,6 +43,11 @@ jobs:
|
||||
sleep 5
|
||||
done
|
||||
pnpm db:migrate:dev
|
||||
|
||||
- name: Build App in dev mode without external dependencies
|
||||
run: |
|
||||
pnpm build:dev --filter=web...
|
||||
|
||||
- name: Serve packages for lazy loading
|
||||
run: |
|
||||
cd packages/surveys && pnpm serve &
|
||||
@@ -52,17 +68,6 @@ jobs:
|
||||
run: |
|
||||
curl -s http://localhost:3003
|
||||
|
||||
- name: Cache Playwright
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install Playwright
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Run E2E Tests
|
||||
run: |
|
||||
pnpm test:e2e
|
||||
|
||||
53
.github/workflows/ecs-deployment.yml
vendored
53
.github/workflows/ecs-deployment.yml
vendored
@@ -7,12 +7,15 @@ name: ECS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch: # Add manual trigger support
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: formbricks/formbricks-experimental
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}/formbricks-experimental
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
|
||||
jobs:
|
||||
@@ -21,9 +24,21 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # Only necessary for sigstore/fulcio outside PRs
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
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
|
||||
|
||||
@@ -38,7 +53,7 @@ jobs:
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v3 # v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -51,15 +66,14 @@ jobs:
|
||||
uses: docker/metadata-action@v5 # v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,format=long
|
||||
|
||||
# Build and push Docker image with Buildx
|
||||
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: depot/build-push-action@v1
|
||||
env:
|
||||
env:
|
||||
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
with:
|
||||
project: tw0fqmsx3c
|
||||
@@ -67,28 +81,25 @@ jobs:
|
||||
context: .
|
||||
file: ./apps/web/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
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 }}
|
||||
NEXT_PUBLIC_SENTRY_DSN=${{ env.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
|
||||
- name: Sign the images with GitHub OIDC Token
|
||||
- name: Sign the published Docker image
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
images=""
|
||||
for tag in ${TAGS}; do
|
||||
images+="${tag}@${DIGEST} "
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
|
||||
outputs:
|
||||
image_tag_sha: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
# This step uses the identity token to provision an ephemeral certificate
|
||||
# against the sigstore community Fulcio instance.
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}${DIGEST}
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -110,7 +121,7 @@ jobs:
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: prod-webapp-container
|
||||
image: ${{ needs.build.outputs.image_tag_sha }}
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
|
||||
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
|
||||
121
.github/workflows/kamal.yml
vendored
121
.github/workflows/kamal.yml
vendored
@@ -1,121 +0,0 @@
|
||||
name: Kamal Deploy
|
||||
concurrency:
|
||||
group: deploy-to-kamal
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
Deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
|
||||
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
|
||||
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
||||
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
|
||||
MAIL_FROM: ${{ secrets.MAIL_FROM }}
|
||||
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
|
||||
TERMS_URL: ${{ vars.TERMS_URL }}
|
||||
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
|
||||
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
|
||||
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
|
||||
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
|
||||
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
|
||||
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
|
||||
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
|
||||
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
|
||||
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
|
||||
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
|
||||
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
|
||||
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
|
||||
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
|
||||
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
|
||||
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
|
||||
DEFAULT_TEAM_ID: ${{ vars.DEFAULT_TEAM_ID }}
|
||||
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
|
||||
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
|
||||
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
|
||||
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
NODE_ENV: production
|
||||
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
|
||||
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
|
||||
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||
S3_REGION: ${{ vars.S3_REGION }}
|
||||
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
|
||||
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
|
||||
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
|
||||
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.3.0
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
gem install kamal
|
||||
|
||||
- uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Create builder
|
||||
run: docker buildx create --use --name formbricks-gh-actions-builder
|
||||
if: steps.buildx.outputs.should_create_builder == 'true'
|
||||
|
||||
- name: Push env variables to Kamal
|
||||
run: |
|
||||
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||
kamal env push
|
||||
|
||||
- name: Run deploy command
|
||||
run: |
|
||||
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||
set +e
|
||||
DEPLOY_OUTPUT=$(kamal setup 2>&1)
|
||||
DEPLOY_EXIT_CODE=$?
|
||||
echo "$DEPLOY_OUTPUT"
|
||||
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
|
||||
echo "Deployment reported healthy container. Considering as success."
|
||||
kamal lock release
|
||||
exit 0
|
||||
else
|
||||
exit $DEPLOY_EXIT_CODE
|
||||
fi
|
||||
shell: bash
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -8,8 +8,8 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
30
.github/workflows/pr.yml
vendored
30
.github/workflows/pr.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: PR Update
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
@@ -12,53 +12,27 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
|
||||
test:
|
||||
name: Run Unit Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
name: Run Tests
|
||||
uses: ./.github/workflows/test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Run Linters
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Build Formbricks-web
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/build-web.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-test:
|
||||
name: Run E2E Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
name: PR Check Summary
|
||||
needs: [lint, test, build, e2e-test]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Docker for Data Migrations
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: formbricks/data-migrations
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
|
||||
with:
|
||||
cosign-release: "v2.1.1"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/database/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
|
||||
- name: Sign the published Docker image
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
cosign sign --yes ghcr.io/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
14
.github/workflows/release-docker-github.yml
vendored
14
.github/workflows/release-docker-github.yml
vendored
@@ -31,6 +31,16 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Generate Random NEXTAUTH_SECRET
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -79,6 +89,10 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}
|
||||
|
||||
# Sign the resulting Docker image digest except on PRs.
|
||||
# This will only write to the public Rekor transparency log when the Docker
|
||||
|
||||
14
.github/workflows/release-docker.yml
vendored
14
.github/workflows/release-docker.yml
vendored
@@ -14,6 +14,16 @@ jobs:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
steps:
|
||||
- name: Generate Random NEXTAUTH_SECRET
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -42,3 +52,7 @@ jobs:
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }}
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:latest
|
||||
build-args: |
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}
|
||||
|
||||
19
.github/workflows/test.yml
vendored
19
.github/workflows/test.yml
vendored
@@ -3,18 +3,22 @@ on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Unit Tests
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
@@ -30,5 +34,8 @@ jobs:
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Build formbricks-js dependencies
|
||||
run: pnpm build --filter=js
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -51,6 +51,3 @@ Zone.Identifier
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# uploads
|
||||
packages/lib/uploads
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample post-deploy hook
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_ROLE (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
# KAMAL_RUNTIME
|
||||
|
||||
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooted Traefik on $KAMAL_HOSTS"
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample pre-build hook
|
||||
#
|
||||
# Checks:
|
||||
# 1. We have a clean checkout
|
||||
# 2. A remote is configured
|
||||
# 3. The branch has been pushed to the remote
|
||||
# 4. The version we are deploying matches the remote
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_ROLE (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Git checkout is not clean, aborting..." >&2
|
||||
git status --porcelain >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
first_remote=$(git remote)
|
||||
|
||||
if [ -z "$first_remote" ]; then
|
||||
echo "No git remote set, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_branch=$(git branch --show-current)
|
||||
|
||||
if [ -z "$current_branch" ]; then
|
||||
echo "Not on a git branch, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
|
||||
|
||||
if [ -z "$remote_head" ]; then
|
||||
echo "Branch not pushed to remote, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
|
||||
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# A sample pre-connect check
|
||||
#
|
||||
# Warms DNS before connecting to hosts in parallel
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_ROLE (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
# KAMAL_RUNTIME
|
||||
|
||||
hosts = ENV["KAMAL_HOSTS"].split(",")
|
||||
results = nil
|
||||
max = 3
|
||||
|
||||
elapsed = Benchmark.realtime do
|
||||
results = hosts.map do |host|
|
||||
Thread.new do
|
||||
tries = 1
|
||||
|
||||
begin
|
||||
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
|
||||
rescue SocketError
|
||||
if tries < max
|
||||
puts "Retrying DNS warmup: #{host}"
|
||||
tries += 1
|
||||
sleep rand
|
||||
retry
|
||||
else
|
||||
puts "DNS warmup failed: #{host}"
|
||||
host
|
||||
end
|
||||
end
|
||||
|
||||
tries
|
||||
end
|
||||
end.map(&:value)
|
||||
end
|
||||
|
||||
retries = results.sum - hosts.size
|
||||
nopes = results.count { |r| r == max }
|
||||
|
||||
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# A sample pre-deploy hook
|
||||
#
|
||||
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
|
||||
#
|
||||
# Fails unless the combined status is "success"
|
||||
#
|
||||
# These environment variables are available:
|
||||
# KAMAL_RECORDED_AT
|
||||
# KAMAL_PERFORMER
|
||||
# KAMAL_VERSION
|
||||
# KAMAL_HOSTS
|
||||
# KAMAL_COMMAND
|
||||
# KAMAL_SUBCOMMAND
|
||||
# KAMAL_ROLE (if set)
|
||||
# KAMAL_DESTINATION (if set)
|
||||
|
||||
# Only check the build status for production deployments
|
||||
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
|
||||
exit 0
|
||||
end
|
||||
|
||||
require "bundler/inline"
|
||||
|
||||
# true = install gems so this is fast on repeat invocations
|
||||
gemfile(true, quiet: true) do
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "octokit"
|
||||
gem "faraday-retry"
|
||||
end
|
||||
|
||||
MAX_ATTEMPTS = 72
|
||||
ATTEMPTS_GAP = 10
|
||||
|
||||
def exit_with_error(message)
|
||||
$stderr.puts message
|
||||
exit 1
|
||||
end
|
||||
|
||||
class GithubStatusChecks
|
||||
attr_reader :remote_url, :git_sha, :github_client, :combined_status
|
||||
|
||||
def initialize
|
||||
@remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
|
||||
@git_sha = `git rev-parse HEAD`.strip
|
||||
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
||||
refresh!
|
||||
end
|
||||
|
||||
def refresh!
|
||||
@combined_status = github_client.combined_status(remote_url, git_sha)
|
||||
end
|
||||
|
||||
def state
|
||||
combined_status[:state]
|
||||
end
|
||||
|
||||
def first_status_url
|
||||
first_status = combined_status[:statuses].find { |status| status[:state] == state }
|
||||
first_status && first_status[:target_url]
|
||||
end
|
||||
|
||||
def complete_count
|
||||
combined_status[:statuses].count { |status| status[:state] != "pending"}
|
||||
end
|
||||
|
||||
def total_count
|
||||
combined_status[:statuses].count
|
||||
end
|
||||
|
||||
def current_status
|
||||
if total_count > 0
|
||||
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
|
||||
else
|
||||
"Build not started..."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
$stdout.sync = true
|
||||
|
||||
puts "Checking build status..."
|
||||
attempts = 0
|
||||
checks = GithubStatusChecks.new
|
||||
|
||||
begin
|
||||
loop do
|
||||
case checks.state
|
||||
when "success"
|
||||
puts "Checks passed, see #{checks.first_status_url}"
|
||||
exit 0
|
||||
when "failure"
|
||||
exit_with_error "Checks failed, see #{checks.first_status_url}"
|
||||
when "pending"
|
||||
attempts += 1
|
||||
end
|
||||
|
||||
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
|
||||
|
||||
puts checks.current_status
|
||||
sleep(ATTEMPTS_GAP)
|
||||
checks.refresh!
|
||||
end
|
||||
rescue Octokit::NotFound
|
||||
exit_with_error "Build status could not be found"
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooting Traefik on $KAMAL_HOSTS..."
|
||||
108
README.md
108
README.md
@@ -1,46 +1,50 @@
|
||||
<div id="top"></div>
|
||||
<div id="top"></div>
|
||||
|
||||
<p align="center">
|
||||
<p align="center">
|
||||
|
||||
<a href="https://formbricks.com">
|
||||
<a href="https://formbricks.com">
|
||||
|
||||
<img width="120" alt="Open Source Privacy First Experience Management Solution Qualtrics Alternative Logo" src="https://github.com/formbricks/formbricks/assets/72809645/0086704f-bee7-4d38-9cc8-fa42ee59e004">
|
||||
<img width="120" alt="Open Source Privacy First Experience Management Solution Qualtrics Alternative Logo" src="https://github.com/formbricks/formbricks/assets/72809645/0086704f-bee7-4d38-9cc8-fa42ee59e004">
|
||||
|
||||
</a>
|
||||
|
||||
<h3 align="center">Formbricks</h3>
|
||||
<h3 align="center">Formbricks</h3>
|
||||
|
||||
<p align="center">
|
||||
<p align="center">
|
||||
Harvest user-insights, build irresistible experiences.
|
||||
<br />
|
||||
<a href="https://formbricks.com/">Website</a> | <a href="https://formbricks.com/discord">Join Discord community</a>
|
||||
<br />
|
||||
<a href="https://formbricks.com/">Website</a> | <a href="https://formbricks.com/discord">Join Discord community</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/formbricks/formbricks/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL-purple" alt="License"></a> <a href="https://formbricks.com/discord"><img src="https://img.shields.io/discord/979077669410979880?label=Discord&logo=discord&logoColor=%23fff" alt="Join Formbricks Discord"></a> <a href="https://github.com/formbricks/formbricks/stargazers"><img src="https://img.shields.io/github/stars/formbricks/formbricks?logo=github" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=32303986"><img src="https://img.shields.io/badge/Hacker%20News-122-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="[https://www.producthunt.com/products/formbricks](https://www.producthunt.com/posts/formbricks)"><img src="https://img.shields.io/badge/Product%20Hunt-455-orange?logo=producthunt&logoColor=%23fff" alt="Product Hunt"></a>
|
||||
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next/"><img src="https://img.shields.io/badge/2023-blue?logo=github&label=Github%20Accelerator" alt="Github Accelerator"></a>
|
||||
<a href="https://github.com/formbricks/formbricks/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||
<p align="center">
|
||||
<a href="https://github.com/formbricks/formbricks/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL-purple" alt="License"></a> <a href="https://formbricks.com/discord"><img src="https://img.shields.io/discord/979077669410979880?label=Discord&logo=discord&logoColor=%23fff" alt="Join Formbricks Discord"></a> <a href="https://github.com/formbricks/formbricks/stargazers"><img src="https://img.shields.io/github/stars/formbricks/formbricks?logo=github" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=32303986"><img src="https://img.shields.io/badge/Hacker%20News-122-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="[https://www.producthunt.com/products/formbricks](https://www.producthunt.com/posts/formbricks)"><img src="https://img.shields.io/badge/Product%20Hunt-455-orange?logo=producthunt&logoColor=%23fff" alt="Product Hunt"></a>
|
||||
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next/"><img src="https://img.shields.io/badge/2023-blue?logo=github&label=Github%20Accelerator" alt="Github Accelerator"></a>
|
||||
<a href="https://github.com/formbricks/formbricks/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="background-color:#f8fafc; border-radius:5px;">
|
||||
<p align="center">
|
||||
<i>Trusted by</i><br/>
|
||||
<img width="867" alt="clients-hi-res" src="https://github.com/formbricks/formbricks/assets/72809645/924d3693-f66a-4063-bb31-6e5789a8175a">
|
||||
<p align="center">
|
||||
<i>Trusted by</i>
|
||||
<a href="https://flixbus.com"><img src="https://github.com/formbricks/formbricks/assets/72809645/d6c91d89-7633-4845-ae1e-03bbd2ce0946" height="35px"></a>
|
||||
<a href="https://github.com/calcom/cal.com/"><img src="https://github.com/formbricks/formbricks/assets/675065/1a8763cf-f47e-4960-90f6-334f6dc12a17#gh-light-mode-only" height="20px"></a>
|
||||
<a href="https://github.com/CrowdDotDev/crowd.dev"><img src="https://github.com/formbricks/formbricks/assets/675065/59b1a4d4-25e4-4ef3-b0bf-4426446fbfd0#gh-light-mode-only" height="20px"></a>
|
||||
<a href="https://neverinstall.com/"><img src="https://github.com/formbricks/formbricks/assets/675065/72e5e37b-8ef7-4340-b06e-f1d12a05330f#gh-light-mode-only" height="20px"></a>
|
||||
<a href="https://clovyr.io/"><img src="https://github.com/formbricks/formbricks/assets/675065/9291c8df-9aac-423a-a430-a9a581240075" height="20px"></a>
|
||||
</p>
|
||||
<div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/2570" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2570" alt="Trendshift Badge for formbricks/formbricks" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/2570" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2570" alt="Trendshift Badge for formbricks/formbricks" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
## ✨ About Formbricks
|
||||
|
||||
<img width="1527" alt="formbricks-sneak" src="https://github-production-user-asset-6210df.s3.amazonaws.com/675065/249441967-ccb89ea3-82b4-4bf2-8d2c-528721ec313b.png">
|
||||
<img width="1527" alt="formbricks-sneak" src="https://github-production-user-asset-6210df.s3.amazonaws.com/675065/249441967-ccb89ea3-82b4-4bf2-8d2c-528721ec313b.png">
|
||||
|
||||
Formbricks provides a free and open source surveying platform. Gather feedback at every point in the user journey with beautiful in-app, website, link and email surveys. Build on top of Formbricks or leverage prebuilt data analysis capabilities.
|
||||
|
||||
@@ -66,11 +70,11 @@ Formbricks is both a free and open source survey platform - and a privacy-first
|
||||
|
||||
- [Contact](#contact-us)
|
||||
|
||||
- [Security](#security)
|
||||
|
||||
- [License](#license)
|
||||
|
||||
<a id="features"></a>
|
||||
- [Security](#security)
|
||||
|
||||
<a id="features"></a>
|
||||
|
||||
### Features
|
||||
|
||||
@@ -104,21 +108,19 @@ Formbricks is both a free and open source survey platform - and a privacy-first
|
||||
|
||||
- 🧘♂️ [Zod](https://zod.dev/)
|
||||
|
||||
- 🐛 [Vitest](https://vitest.dev/)
|
||||
|
||||
<a id="getting-started"></a>
|
||||
<a id="getting-started"></a>
|
||||
|
||||
## 🚀 Getting started
|
||||
|
||||
We've got several options depending on your need to help you quickly get started with Formbricks.
|
||||
|
||||
<a id="cloud-version"></a>
|
||||
<a id="cloud-version"></a>
|
||||
|
||||
### ☁️ Cloud Version
|
||||
|
||||
Formbricks has a hosted cloud offering with a generous free plan to get you up and running as quickly as possible. To get started, please visit [formbricks.com](https://app.formbricks.com/auth/signup).
|
||||
|
||||
<a id="self-hosted-version"></a>
|
||||
<a id="self-hosted-version"></a>
|
||||
|
||||
### 🐳 Self-hosting Formbricks
|
||||
|
||||
@@ -138,11 +140,11 @@ You can deploy Formbricks on [Railway](https://railway.app) using the button bel
|
||||
|
||||
[](https://railway.app/new/template/PPDzCd)
|
||||
|
||||
<a id="development"></a>
|
||||
<a id="development"></a>
|
||||
|
||||
## 👨💻 Development
|
||||
### 👨💻 Development
|
||||
|
||||
### Prerequisites
|
||||
#### Prerequisites
|
||||
|
||||
Here is what you need to be able to run Formbricks:
|
||||
|
||||
@@ -152,11 +154,11 @@ Here is what you need to be able to run Formbricks:
|
||||
|
||||
- [Docker](https://www.docker.com/) - to run PostgreSQL and MailHog
|
||||
|
||||
### Local Setup
|
||||
#### Local Setup
|
||||
|
||||
To get started locally, we've got a [guide to help you](https://formbricks.com/docs/contributing/setup).
|
||||
|
||||
### Gitpod Setup
|
||||
#### Gitpod Setup
|
||||
|
||||
1. Click the button below to open this project in Gitpod.
|
||||
|
||||
@@ -164,7 +166,7 @@ To get started locally, we've got a [guide to help you](https://formbricks.com/d
|
||||
|
||||
[](https://gitpod.io/#https://github.com/formbricks/formbricks)
|
||||
|
||||
<a id="contribution"></a>
|
||||
<a id="contribution"></a>
|
||||
|
||||
## ✍️ Contribution
|
||||
|
||||
@@ -182,46 +184,30 @@ Please check out [our contribution guide](https://formbricks.com/docs/contributi
|
||||
|
||||
## All Thanks To Our Contributors
|
||||
|
||||
<a href="https://github.com/formbricks/formbricks/graphs/contributors">
|
||||
<a href="https://github.com/formbricks/formbricks/graphs/contributors">
|
||||
|
||||
<img src="https://contrib.rocks/image?repo=formbricks/formbricks" />
|
||||
<img src="https://contrib.rocks/image?repo=formbricks/formbricks" />
|
||||
|
||||
</a>
|
||||
|
||||
<a id="contact-us"></a>
|
||||
<a id="contact-us"></a>
|
||||
|
||||
## 📆 Contact us
|
||||
|
||||
Let's have a chat about your survey needs and get you started.
|
||||
|
||||
<a href="https://cal.com/johannes/onboarding?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||
<a href="https://cal.com/johannes/onboarding?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||
|
||||
<a id="license"></a>
|
||||
<a id="license"></a>
|
||||
|
||||
<a id="security"></a>
|
||||
## ⚖️ License
|
||||
|
||||
Distributed under the AGPLv3 License. See [`LICENSE`](./LICENSE) for more information.
|
||||
|
||||
<a id="security"></a>
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
We take security very seriously. If you come across any security vulnerabilities, please disclose them by sending an email to security@formbricks.com. We appreciate your help in making our platform as secure as possible and are committed to working with you to resolve any issues quickly and efficiently. See [`SECURITY.md`](./SECURITY.md) for more information.
|
||||
|
||||
<a id="license"></a>
|
||||
|
||||
## 👩⚖️ License
|
||||
|
||||
### The AGPL Formbricks Core
|
||||
|
||||
The Formbricks core application is licensed under the [AGPLv3 Open Source License](https://github.com/formbricks/formbricks/blob/main/LICENSE). The core application is fully functional and includes everything you need to design & run link surveys, website surveys and in-app surveys. You can use the software for free for personal and commercial use. You're also allowed to create and distribute modified versions as long as you document the changes you make incl. date. The AGPL license requires you to publish your modified version under the AGPLv3 license as well.
|
||||
|
||||
### The Enterprise Edition
|
||||
|
||||
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/packages/ee) and [license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE) for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/enterprise) to unlock it.
|
||||
|
||||
### White-Labeling Formbricks and Other Licensing Needs
|
||||
|
||||
If you have other licensing requirements such as White-Labeling please [send us an email](mailto:hola@formbricks.com).
|
||||
|
||||
### Why charge for Enterprise Features?
|
||||
|
||||
The Enterprise Edition and White-Label Licenses allow us to fund the development of Formbricks sustainably. It guarantees that the open-source surveying infrastructure we're building will be around for decades to come.
|
||||
|
||||
<p align="right"><a href="#top">🔼 Back to top</a></p>
|
||||
<p align="right"><a href="#top">🔼 Back to top</a></p>
|
||||
|
||||
@@ -3,25 +3,25 @@ import {
|
||||
ClockIcon,
|
||||
CogIcon,
|
||||
CreditCardIcon,
|
||||
FileBarChartIcon,
|
||||
HelpCircleIcon,
|
||||
DocumentChartBarIcon,
|
||||
HomeIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
ScaleIcon,
|
||||
ShieldCheckIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
UserGroupIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Home", href: "#", icon: HomeIcon, current: true },
|
||||
{ name: "History", href: "#", icon: ClockIcon, current: false },
|
||||
{ name: "Balances", href: "#", icon: ScaleIcon, current: false },
|
||||
{ name: "Cards", href: "#", icon: CreditCardIcon, current: false },
|
||||
{ name: "Recipients", href: "#", icon: UsersIcon, current: false },
|
||||
{ name: "Reports", href: "#", icon: FileBarChartIcon, current: false },
|
||||
{ name: "Recipients", href: "#", icon: UserGroupIcon, current: false },
|
||||
{ name: "Reports", href: "#", icon: DocumentChartBarIcon, current: false },
|
||||
];
|
||||
const secondaryNavigation = [
|
||||
{ name: "Settings", href: "#", icon: CogIcon },
|
||||
{ name: "Help", href: "#", icon: HelpCircleIcon },
|
||||
{ name: "Help", href: "#", icon: QuestionMarkCircleIcon },
|
||||
{ name: "Privacy", href: "#", icon: ShieldCheckIcon },
|
||||
];
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"lucide-react": "^0.356.0",
|
||||
"next": "14.1.3",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"next": "14.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
||||
@@ -21,30 +21,15 @@ export default function AppPage({}) {
|
||||
}, [darkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
// enable Formbricks debug mode by adding formbricksDebug=true GET parameter
|
||||
const addFormbricksDebugParam = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.has("formbricksDebug")) {
|
||||
urlParams.set("formbricksDebug", "true");
|
||||
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
}
|
||||
};
|
||||
addFormbricksDebugParam();
|
||||
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
const isUserId = window.location.href.includes("userId=true");
|
||||
const defaultAttributes = {
|
||||
language: "gu",
|
||||
};
|
||||
const userInitAttributes = { "Init Attribute 1": "eight", "Init Attribute 2": "two" };
|
||||
|
||||
const attributes = isUserId ? { ...defaultAttributes, ...userInitAttributes } : defaultAttributes;
|
||||
const userId = isUserId ? "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" : undefined;
|
||||
const attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
|
||||
formbricks.init({
|
||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
userId,
|
||||
debug: true,
|
||||
attributes,
|
||||
});
|
||||
window.formbricks = formbricks;
|
||||
@@ -85,7 +70,7 @@ export default function AppPage({}) {
|
||||
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">1. Setup .env</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
|
||||
Copy the environment ID of your Formbricks app to the env variable in demo/.env
|
||||
</p>
|
||||
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
|
||||
|
||||
@@ -120,8 +105,8 @@ export default function AppPage({}) {
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
On formbricks.reset() the local state will <strong>be deleted</strong> and formbricks gets{" "}
|
||||
<strong>reinitialized</strong>.
|
||||
On formbricks.reset() a few things happen: <strong>New person is created</strong> and{" "}
|
||||
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
|
||||
</p>
|
||||
<button
|
||||
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
|
||||
@@ -45,7 +45,8 @@ Adds an Actions for a given User by their User ID
|
||||
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/actions' \
|
||||
--data-raw '{
|
||||
"userId": "1",
|
||||
"name": "new_action_v2"
|
||||
"name": "new_action_v2",
|
||||
"properties":{}
|
||||
|
||||
}'
|
||||
```
|
||||
@@ -53,7 +54,8 @@ Adds an Actions for a given User by their User ID
|
||||
```json {{ title: 'Example Request Body' }}
|
||||
{
|
||||
"userId": "1",
|
||||
"name": "new_action_v3"
|
||||
"name": "new_action_v3",
|
||||
"properties":{}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
import { generateManagementApiMetadata } from "@/lib/utils";
|
||||
import {generateManagementApiMetadata} from "@/lib/utils"
|
||||
|
||||
export const metadata = generateManagementApiMetadata("Responses", ["Fetch", "Delete"]);
|
||||
export const metadata = generateManagementApiMetadata("Responses",["Fetch","Delete"])
|
||||
|
||||
#### Management API
|
||||
|
||||
# Responses API
|
||||
|
||||
This set of API can be used to
|
||||
|
||||
- [List Responses](#list-all-responses)
|
||||
- [List all Responses by surveyId](#list-all-responses-by-survey-id)
|
||||
- [Get Response](#get-response-by-id)
|
||||
- [Delete Response](#delete-a-response)
|
||||
|
||||
@@ -109,97 +107,6 @@ This set of API can be used to
|
||||
|
||||
---
|
||||
|
||||
## List all Responses by surveyId {{ tag: 'GET', label: '/api/v1/management/responses?surveyId=<survey-Id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the responses received in your survey.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses?surveyId=<survey-Id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/responses?surveyId=<survey-Id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data":[
|
||||
{
|
||||
"id": "cln8k0tqv00pcz87no4qrw333",
|
||||
"createdAt": "2023-10-02T07:13:20.023Z",
|
||||
"updatedAt": "2023-10-02T07:13:20.023Z",
|
||||
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
|
||||
"finished": true,
|
||||
"data": {
|
||||
"interview-prompt": "clicked"
|
||||
},
|
||||
"meta": {
|
||||
"userAgent": {
|
||||
"os": "MacOS",
|
||||
"browser": "Chrome"
|
||||
}
|
||||
},
|
||||
"personAttributes": null,
|
||||
"person": {
|
||||
"id": "e0x4i5tvsp8puxfztyrwykvn",
|
||||
"attributes": {
|
||||
"userId": "CYO675",
|
||||
"email": "ravi@netflix.com",
|
||||
"Name": "Ravi Kumar",
|
||||
"Role": "Manager",
|
||||
"Company": "Netflix",
|
||||
"Experience": "6 years",
|
||||
"Usage Frequency": "Monthly",
|
||||
"Company Size": "4610 employees",
|
||||
"Product Satisfaction Score": "43",
|
||||
"Recommendation Likelihood": "4"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
},
|
||||
"notes": [],
|
||||
"tags": []
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Get Response by ID {{ tag: 'GET', label: '/api/v1/management/responses/<response-id>' }}
|
||||
|
||||
<Row>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
import { generateManagementApiMetadata } from "@/lib/utils";
|
||||
import {generateManagementApiMetadata} from "@/lib/utils"
|
||||
|
||||
export const metadata = generateManagementApiMetadata("Surveys", ["Fetch", "Create", "Update", "Delete"]);
|
||||
export const metadata = generateManagementApiMetadata("Surveys",["Fetch","Create","Update","Delete"])
|
||||
|
||||
#### Management API
|
||||
|
||||
# Surveys API
|
||||
|
||||
This set of API can be used to
|
||||
|
||||
- [List All Surveys](#list-all-surveys)
|
||||
- [Get Survey](#get-survey-by-id)
|
||||
- [Create Survey](#create-survey)
|
||||
@@ -23,7 +22,8 @@ This set of API can be used to
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
Retrieve all the surveys you have for the environment with pagination.
|
||||
|
||||
Retrieve all the surveys you have for the environment.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
@@ -33,26 +33,14 @@ This set of API can be used to
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Query Parameters
|
||||
<Properties>
|
||||
<Property name="offset" type="number">
|
||||
The number of surveys to skip before returning the results.
|
||||
</Property>
|
||||
|
||||
<Property name="limit" type="number">
|
||||
The number of surveys to return.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/surveys?offset=20&limit=10' \
|
||||
'https://app.formbricks.com/api/v1/management/surveys' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
@@ -415,6 +403,7 @@ This set of API can be used to
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
@@ -464,7 +453,7 @@ This set of API can be used to
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
@@ -508,6 +497,7 @@ This set of API can be used to
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
@@ -578,7 +568,7 @@ This set of API can be used to
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
@@ -595,6 +585,7 @@ This set of API can be used to
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/<survey-id>' }}
|
||||
|
||||
<Row>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import CorsHandling from "./cors-handling-in-api.webp";
|
||||
|
||||
export const metadata = {
|
||||
@@ -26,7 +25,7 @@ Thank you for choosing to contribute to Formbricks. Before you start, please fam
|
||||
- Constants should be in the packages folder
|
||||
- Types should be in the packages folder
|
||||
- How we handle Pull Requests
|
||||
- Read server-side environment variables from `constants.ts`
|
||||
- Read environment variables from `.env.mjs`
|
||||
|
||||
---
|
||||
|
||||
@@ -84,9 +83,9 @@ You should store constants in `packages/lib/constants`
|
||||
|
||||
You should store type in `packages/types`
|
||||
|
||||
## Read server-side environment variables from `constants.ts`
|
||||
## Read environment variables from `.env.mjs`
|
||||
|
||||
Server-side environment variables (`process.env`) shouldn’t be accessed directly but included into the `constants.ts` file and read from there. This way we can assure they are used only on the server side and are also type-safe.
|
||||
Environment variables (`process.env`) shouldn’t be accessed directly but be added in the `.env.mjs` and should be accessed from here. This practice helps us ensure that the variables are typesafe.
|
||||
|
||||
## How we handle Pull Requests
|
||||
|
||||
|
||||
@@ -42,4 +42,6 @@ If you are at all unsure, just raise it as an enhancement issue first and tell u
|
||||
|
||||
To be able to keep working on Formbricks over the coming years, we need to collect a CLA from all relevant contributors.
|
||||
|
||||
Once you open a PR, you will get a message from the CLA bot to fill out the form. Please note that we can only get your contribution merged when we have a CLA signed by you.
|
||||
Please note that we can only get your contribution merged when we have a CLA signed by you.
|
||||
|
||||
To access the CLA form, please click [here](https://formbricks.com/clmyhzfrymr4ko00hycsg1tvx)
|
||||
|
||||
@@ -46,13 +46,13 @@ All you need to do is copy a `<script>` tag to your HTML head, and that’s abou
|
||||
```html {{ title: 'index.html' }}
|
||||
<!-- START Formbricks Surveys -->
|
||||
<script type="text/javascript">
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.6.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "<your-environment-id>", apiHost: "<api-host>"})},500)}();
|
||||
</script>
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.4.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "<your-environment-id>", apiHost: "<api-host>"})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -99,6 +99,7 @@ if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ export default App;
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -130,7 +131,7 @@ The app initializes 'formbricks' when it's loaded in a browser environment (due
|
||||
|
||||
<Image
|
||||
src={ReactApp}
|
||||
alt="In-app survey in React app for micro surveys"
|
||||
alt="In app survey in React app for micro surveys"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -181,6 +182,7 @@ useEffect(() => {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -230,6 +232,7 @@ if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}
|
||||
|
||||
@@ -253,7 +256,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
</Col>
|
||||
Refer to our [Example NextJS Pages Directory project](https://github.com/formbricks/examples/tree/main/nextjs-pages) for more help!
|
||||
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -266,6 +269,14 @@ Refer to our [Example NextJS Pages Directory project](https://github.com/formbri
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Optional Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="debug" type="boolean">
|
||||
Whether you want to see debug messages from Formbricks on your client-side console.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### What are we doing here?
|
||||
|
||||
First we need to initialize the Formbricks SDK, making sure it only runs on the client side.
|
||||
@@ -334,7 +345,7 @@ router.afterEach((to, from) => {
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -347,6 +358,14 @@ router.afterEach((to, from) => {
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Optional Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="debug" type="boolean">
|
||||
Whether you want to see debug messages from Formbricks on your client-side console.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
Refer to our [Example VueJs project](https://github.com/formbricks/examples/tree/main/vuejs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
|
||||
|
||||
## Validate your setup
|
||||
@@ -369,46 +388,6 @@ To this:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Debugging Formbricks Integration
|
||||
|
||||
Enabling Formbricks debug mode in your browser is a useful troubleshooting step for identifying and resolving complex issues. This section outlines how to activate debug mode, covers common use cases, and provides insights into specific debug log messages.
|
||||
|
||||
### Activate Debug Mode
|
||||
|
||||
To activate Formbricks debug mode:
|
||||
|
||||
1. **Via URL Parameter:**
|
||||
|
||||
- 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:**
|
||||
|
||||
- Open your browser's developer tools by pressing `F12` or right-clicking and selecting "Inspect."
|
||||
- Navigate to the "Console" tab to view Formbricks debugging information.
|
||||
|
||||
**How to Open Browser Console:**
|
||||
|
||||
- **Google Chrome:** Press `F12` or right-click, select "Inspect," and go to the "Console" tab.
|
||||
- **Firefox:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab.
|
||||
- **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.
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
Debug mode is beneficial for scenarios such as:
|
||||
|
||||
- Verifying Formbricks initialization.
|
||||
- Identifying survey trigger issues.
|
||||
- Troubleshooting unexpected behavior.
|
||||
|
||||
### Debug Log Messages
|
||||
|
||||
Debug log messages provide insights into:
|
||||
|
||||
- API calls and responses.
|
||||
- Event tracking, survey triggers and form interactions.
|
||||
- Initialization errors.
|
||||
|
||||
## Overwrite CSS Styles for In-App Surveys
|
||||
|
||||
You can overwrite the default CSS styles for the in-app surveys by adding the following CSS to your global CSS file (eg. `globals.css`):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import ReactApp from "../framework-guides/react-in-app-survey-app-popup-form.webp";
|
||||
import I1 from "./1-in-app-survey-or-popup-survey-setup.webp";
|
||||
import I2 from "./2-settings-for-survey-popup-in-app-for-feedback.webp";
|
||||
import I3 from "./3-web-app-survey-settings-for-in-app-survey-popup.webp";
|
||||
@@ -9,6 +8,7 @@ import I5 from "./5-options-survey-popup-in-app-for-feedback.webp";
|
||||
import I6 from "./6-setup-in-app-survey-popup-feedback-box.webp";
|
||||
import I7 from "./7-in-app-survey-popup-for-feedback.webp";
|
||||
import I8 from "./8-pop-up-form-in-web-app-survey.webp";
|
||||
import ReactApp from "../framework-guides/react-in-app-survey-app-popup-form.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Formbricks Quickstart Guide: In-App Surveys Made Simple",
|
||||
@@ -20,7 +20,7 @@ export const metadata = {
|
||||
|
||||
# Quickstart
|
||||
|
||||
In-app surveys have 6-10x better conversion rates than emailed out surveys. This tutorial explains how to run an in-app survey in your web app in 10 to 15 minutes. Let’s go!
|
||||
In app surveys have 6-10x better conversion rates than emailed out surveys. This tutorial explains how to run an in app survey in your web app in 10 to 15 minutes. Let’s go!
|
||||
|
||||
## Create a free Formbricks Cloud account
|
||||
|
||||
@@ -28,7 +28,7 @@ While you can [self-host](/docs/self-hosting/deployment) Formbricks, the quickes
|
||||
|
||||
<Image
|
||||
src={I1}
|
||||
alt="Choose in-app survey template"
|
||||
alt="Choose in app survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
@@ -59,7 +59,7 @@ Scroll down to Survey Trigger and choose “New Session”. This will cause this
|
||||
|
||||
<Image
|
||||
src={I4}
|
||||
alt="In-app survey trigger for feedback popup micro survey"
|
||||
alt="In app survey trigger for feedback popup micro survey"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -68,7 +68,7 @@ In **Recontact Options** we choose the following settings, so that we can play a
|
||||
|
||||
<Image
|
||||
src={I5}
|
||||
alt="Options for survey popup in-app micro survey"
|
||||
alt="Options for survey popup in app micro survey"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -88,7 +88,7 @@ On the Setup Checklist you have two elements. At the top you find the Widget Sta
|
||||
|
||||
<Image
|
||||
src={I7}
|
||||
alt="feedback popup in-app survey"
|
||||
alt="feedback popup in app survey"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -100,7 +100,7 @@ In the manual below, this code snippet contains all the information you need:
|
||||
|
||||
<Image
|
||||
src={I8}
|
||||
alt="settings for in-app survey popping up"
|
||||
alt="settings for in app survey popping up"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -122,7 +122,7 @@ Now, restart your app in your terminal to make sure the widget is loaded. Once i
|
||||
|
||||
<Image
|
||||
src={ReactApp}
|
||||
alt="In-app survey in React app for micro surveys"
|
||||
alt="In app survey in React app for micro surveys"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted i
|
||||
|
||||
<Image
|
||||
src={I1}
|
||||
alt="setup checklist ui of survey popup for in-app surveys"
|
||||
alt="setup checklist ui of survey popup for in app surveys"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -39,7 +39,7 @@ If your app is connected with Formbricks Cloud, the survey might have not been l
|
||||
|
||||
<Image
|
||||
src={I3}
|
||||
alt="survey logs for in-app survey pop up micro"
|
||||
alt="survey logs for in app survey pop up micro"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -50,7 +50,7 @@ The widget only loads surveys which are **public** and **in progress**. Go to Fo
|
||||
|
||||
<Image
|
||||
src={I2}
|
||||
alt="ui of survey popup for in-app micro surveys"
|
||||
alt="ui of survey popup for in app micro surveys"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -1,97 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import FilledHiddenFields from "./filled-hidden-fields.webp";
|
||||
import HiddenFieldResponses from "./hidden-field-responses.webp";
|
||||
import HiddenFields from "./hidden-fields.webp";
|
||||
import InputHiddenFields from "./input-hidden-fields.webp";
|
||||
import SettingsPage from "./settings.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Hidden Fields",
|
||||
description: "Add hidden fields to your surveys to capture additional data without requiring user inputs!",
|
||||
};
|
||||
|
||||
#### Link Surveys
|
||||
|
||||
# Hidden Fields
|
||||
|
||||
Hidden fields are a powerful feature in Formbricks that allows you to add data to a submission without asking the user to type it in. This feature is especially useful when you already have information about a user that you want to use in the analysis of the survey results (e.g. `payment plan` or `email`)
|
||||
|
||||
## How to Add Hidden Fields
|
||||
|
||||
### Enable them in the Survey Builder
|
||||
|
||||
1. Edit the survey you want to add hidden fields to & open it's settings, make sure it's selected as a **Link Survey**.
|
||||
|
||||
<Image
|
||||
src={SettingsPage}
|
||||
alt="Select the Survey Type as Link Survey"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Switch to the Questions tab and scroll down to the bottom of the page. You will see a section called **Hidden Fields**. Make sure to enable it by toggling the switch.
|
||||
|
||||
<Image
|
||||
src={HiddenFields}
|
||||
alt="Enable Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Now click on it to add a new hidden field ID. You can add as many hidden fields as you want.
|
||||
|
||||
<Image
|
||||
src={InputHiddenFields}
|
||||
alt="Add Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
<Image
|
||||
src={FilledHiddenFields}
|
||||
alt="Filled Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Set Single Hidden Field
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Example Screen from which the User filled it">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?screen=pricing
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
### Set Multiple Hidden Fields
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Example Screen from which the User filled it">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?screen=landing_page&job=Founder
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## View Hidden Fields in Responses
|
||||
|
||||
These hidden fields will now be visible in the responses tab just like other fields in the Summary as well as the Response Cards, and you can use them to filter and analyze your responses.
|
||||
|
||||
<Image
|
||||
src={HiddenFieldResponses}
|
||||
alt="Hidden Field Responses"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Tracking Source**: You can add a hidden field to track the source of the survey. For a detailed guide on Source Tracking, check out the [Source Tracking](/docs/link-surveys/source-tracking) guide.
|
||||
- **User Metadata**: You can add hidden fields to capture user metadata such as user ID, email, or any other user-specific information.
|
||||
- **Survey Metadata**: You can add hidden fields to capture other metadata, e.g. the screen from which the survey was filled, or any other app specific information.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
@@ -10,7 +10,7 @@ export const metadata = {
|
||||
|
||||
Quickly set up and start using Formbricks without getting into the technicalities of the build process. You'll be using an image that we've already built for you. The pre-built image is ready-to-run, and it only requires minimal configuration on your part. This approach is perfect for getting a functional instance of Formbricks up and running with minimal hassle. It's as easy as downloading the Docker image and firing up the container.
|
||||
|
||||
This is ideal for those who are testing Formbricks or want to run it with minimal to no modifications. For this we use the [public Docker image](https://github.com/formbricks/formbricks/pkgs/container/formbricks) and a simple docker-compose file.
|
||||
This is ideal for those who are testing Formbricks or want to run it with minimal to no modifications. For this we use the [public Docker image](https://hub.docker.com/r/formbricks/formbricks) and a simple docker-compose file.
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -201,10 +201,69 @@ formbricks-quickstart-formbricks-1 | Listening on port 3000 url: http://<random
|
||||
You can close the logs again with `CTRL + C`.
|
||||
|
||||
<Note>
|
||||
## Customizing environment variables
|
||||
## Customizing the Build Time variables
|
||||
|
||||
To edit any of the available environment variables, check out our [Configure](/docs/self-hosting/external-auth-providers) section!
|
||||
To edit any of the variables that start with `NEXT_PUBLIC_`, you need to rebuild the Formbricks Docker Image!
|
||||
|
||||
</Note>
|
||||
|
||||
## Important Run-time Variables
|
||||
|
||||
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 | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| TERMS_URL | URL for terms of service. | optional | |
|
||||
| IMPRINT_URL | URL for imprint. | optional | |
|
||||
| SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | |
|
||||
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | |
|
||||
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | |
|
||||
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | |
|
||||
| GITHUB_AUTH_ENABLED | Enables GitHub login if set to `1`. | optional | |
|
||||
| GOOGLE_AUTH_ENABLED | Enables Google login if set to `1`. | optional | |
|
||||
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
|
||||
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
|
||||
| CRON_SECRET | API Secret for running cron jobs. | optional | |
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | |
|
||||
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
|
||||
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
|
||||
| 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 | |
|
||||
|
||||
## Build-time Variables
|
||||
|
||||
These variables must be provided at the time of the docker build and would require rebuilding the image.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| -------------------------------------------------- | --------------------------------------------------------------- | -------- | ------- |
|
||||
| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | If you already have a preset environment ID of the environment. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID | If you already have an onboarding survey ID to show new users. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_KEY | API key for PostHog analytics. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_HOST | Host for PostHog analytics. | optional | |
|
||||
| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_APP_ID | ID of the DocSearch application. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_API_KEY | API key of the DocSearch application. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_INDEX_NAME | Name of the DocSearch index. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID | Environment ID for Formbricks (if you already have one) | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID | Survey ID for the feedback survey on the docs site. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_API_HOST | Host for the Formbricks API. | optional | |
|
||||
|
||||
Still facing issues? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you!
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
export const metadata = {
|
||||
title: "Enterprise License to unlock advanced functionality",
|
||||
description: "Request a enterprise licenses to unlock advanced enterprise functionality",
|
||||
};
|
||||
|
||||
#### Self-Hosting
|
||||
|
||||
# Enterprise License Request (Beta)
|
||||
|
||||
We're handing out free enterprise license keys during our beta.
|
||||
|
||||
Additional to the AGPL licensed Formbricks core, the Formbricks repository contains code licensed under an [Enterprise license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE). This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/license) to unlock it.
|
||||
|
||||
**Please note:** Sooner than later we will introduce a enterprise license pricing. For a free beta key, fill out this form:
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "100vh",
|
||||
maxHeight: "100vh",
|
||||
overflow: "auto",
|
||||
borderRadius: "12px",
|
||||
}}>
|
||||
<iframe
|
||||
src="https://app.formbricks.com/s/clrf4z8zg1u3912250j7shqb5"
|
||||
style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", border: 0 }}></iframe>
|
||||
</div>
|
||||
|
||||
**Can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
@@ -40,11 +40,12 @@ Integrating Google OAuth with your Formbricks instance allows users to log in us
|
||||
- Ensure to specify authorized JavaScript origins and authorized redirect URIs.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Configuration URLs">
|
||||
``` {{ title: "Redirect & Origin URLs" }}
|
||||
Authorized JavaScript origins: {WEBAPP_URL}
|
||||
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google ```
|
||||
</CodeGroup>
|
||||
<CodeGroup title="Configuration URLs">
|
||||
``` {{ title: 'Redirect & Origin URLs' }}
|
||||
Authorized JavaScript origins: {WEBAPP_URL}
|
||||
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. **Update Environment Variables in Docker**:
|
||||
@@ -57,12 +58,14 @@ Integrating Google OAuth with your Formbricks instance allows users to log in us
|
||||
|
||||
```sh {{ title: 'Shell commands' }}
|
||||
docker exec -it container_id /bin/bash
|
||||
export GOOGLE_AUTH_ENABLED=1
|
||||
export GOOGLE_CLIENT_ID=your-client-id-here
|
||||
export GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
exit
|
||||
```
|
||||
|
||||
```sh {{ title: 'env file' }}
|
||||
GOOGLE_AUTH_ENABLED=1
|
||||
GOOGLE_CLIENT_ID=your-client-id-here
|
||||
GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
```
|
||||
@@ -75,126 +78,3 @@ GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
- Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. Here's how you can do it:
|
||||
- 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.
|
||||
|
||||
1. Configure your OIDC provider & get the following variables:
|
||||
- `OIDC_CLIENT_ID`
|
||||
- `OIDC_CLIENT_SECRET`
|
||||
- `OIDC_ISSUER`
|
||||
- `OIDC_SIGNING_ALGORITHM`
|
||||
|
||||
<Note>
|
||||
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
|
||||
</Note>
|
||||
|
||||
2. Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
|
||||
|
||||
An example configuration for a FusionAuth OpenID Connect in Formbricks would look like:
|
||||
<Col>
|
||||
<CodeGroup title="Formbricks Env for FusionAuth OIDC">
|
||||
```yml {{ title: '.env' }}
|
||||
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
|
||||
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
|
||||
OIDC_ISSUER=http://localhost:9011
|
||||
OIDC_DISPLAY_NAME=FusionAuth
|
||||
OIDC_SIGNING_ALGORITHM=HS256
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider.
|
||||
|
||||
4. Restart your Formbricks instance.
|
||||
|
||||
5. You're all set! Users can now signup & log in using their OIDC credentials.
|
||||
|
||||
## Important Run-time Variables
|
||||
|
||||
These variables can be provided at the runtime i.e. in your docker-compose file.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
|-----------------------------|----------------------------------------------------------------------------------------------|---------------------------------------------------------|---------------------------|
|
||||
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
|
||||
| DATABASE_URL | Database URL with credentials. | required | |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user) |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
|
||||
| UPLOADS_DIR | Local directory for storing uploads. | optional | `./uploads` |
|
||||
| S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_BUCKET | Bucket name for S3. | optional (required if S3 is enabled) | |
|
||||
| S3_ENDPOINT | Endpoint for S3. | optional | (resolved by the AWS SDK) |
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| TERMS_URL | URL for terms of service. | optional | |
|
||||
| IMPRINT_URL | URL for imprint. | optional | |
|
||||
| SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | |
|
||||
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to `1`. | optional | |
|
||||
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | |
|
||||
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | |
|
||||
| RATE_LIMITING_DISABLED | Disables rate limiting if set to `1`. | optional | |
|
||||
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | |
|
||||
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
|
||||
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
|
||||
| CRON_SECRET | API Secret for running cron jobs. | optional | |
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | |
|
||||
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
|
||||
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
|
||||
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
|
||||
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | `admin` |
|
||||
| ONBOARDING_DISABLED | Disables onboarding for new users if set to `1` | optional | |
|
||||
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
|
||||
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have `.well-known` configured at this) | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
|
||||
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | | |
|
||||
|
||||
## Build-time Variables
|
||||
|
||||
These variables must be provided at the time of the docker build and would require rebuilding the image.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ------------------------------------- | ----------------------------------------------------- | -------- | ------- |
|
||||
| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API to use inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | Formbricks environment ID for use inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_KEY | API key to use PostHog analytics inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_HOST | Host to use PostHog analytics inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking inside Formbricks. | optional | |
|
||||
|
||||
Still facing issues? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you!
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
export const metadata = {
|
||||
title: "About the Formbricks Open-Source License",
|
||||
title: "Self-hosting License to run Formbricks on premises",
|
||||
description:
|
||||
"The Formbricks core is available under the AGPLv3 license",
|
||||
"Request a self-hosting licenses to run Formbricks on your own servers.",
|
||||
};
|
||||
|
||||
#### Self-Hosting
|
||||
|
||||
# License
|
||||
# License Request (Beta)
|
||||
|
||||
## The AGPL Formbricks Core
|
||||
We're handing out free self-hosting license keys during our Beta.
|
||||
|
||||
The Formbricks core application is licensed under the [AGPLv3 Open Source License](https://github.com/formbricks/formbricks/blob/main/LICENSE). The core application is fully functional and includes everything you need to design & run link surveys, website surveys and in-app surveys. You can use the software for free for personal and commercial use. You're also allowed to create and distribute modified versions as long as you document the changes you make incl. date. The AGPL license requires you to publish your modified version under the AGPLv3 license as well.
|
||||
**Please note:** Sooner than later we will introduce a self-hosting license pricing. For a free beta key, fill out this form:
|
||||
|
||||
## The Enterprise Edition
|
||||
<div style={{ position: 'relative', height: '100vh', maxHeight: '100vh', overflow: 'auto', borderRadius:'12px' }}>
|
||||
<iframe
|
||||
src="https://app.formbricks.com/s/clrf4z8zg1u3912250j7shqb5"
|
||||
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', border: 0 }}
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/packages/ee) and [license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE) for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/enterprise) to unlock it.
|
||||
|
||||
## White-Labeling Formbricks and Other Licensing Needs
|
||||
|
||||
If you have other licensing requirements such as White-Labeling please [send us an email](mailto:hola@formbricks.com).
|
||||
|
||||
## Why charge for Enterprise Features?
|
||||
|
||||
The Enterprise Edition and White-Label Licenses allow us to fund the development of Formbricks sustainably. It guarantees that the open-source surveying infrastructure we're building will be around for decades to come.
|
||||
|
||||
**Having more questions?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
**Can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
|
||||
@@ -8,101 +8,16 @@ export const metadata = {
|
||||
|
||||
# Migration Guide
|
||||
|
||||
## v1.6
|
||||
|
||||
Formbricks v1.6 comes with a big new features like Advanced Targeting & Segmentation of your end-users along with on-the-fly triggers for surveys and a ton of stability improvements & features. This also involves a few changes in our environment variables. This guide will help you migrate your existing Formbricks instance to v1.6 without any hassles or build errors.
|
||||
|
||||
<Note>
|
||||
This upgrade requires a **data migration**. Please make sure to backup your database before proceeding with
|
||||
the upgrade. Follow the below steps thoroughly to upgrade your Formbricks instance to v1.6.
|
||||
</Note>
|
||||
|
||||
### Steps to Migrate
|
||||
|
||||
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
|
||||
|
||||
To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located.
|
||||
|
||||
1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```bash
|
||||
docker exec formbricks-quickstart-postgres-1 pg_dump -U postgres -d formbricks > formbricks_pre_v1.6_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Restarting the containers will automatically pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Now let's migrate the data to the latest schema:
|
||||
|
||||
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ps`</Note>
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v1.6" \
|
||||
ghcr.io/formbricks/data-migrations:v1.6
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
|
||||
|
||||
5. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
|
||||
### In-App Surveys with @formbricks/js
|
||||
|
||||
If you are using the `@formbricks/js` package, please make sure to update it to version 1.6.0 to use the latest features and improvements.
|
||||
|
||||
### Deprecated Environment Variables
|
||||
|
||||
| Environment Variable | Comments |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| GITHUB_AUTH_ENABLED | Was used to enable GitHub OAuth, but from v1.6, you can just set the `GITHUB_ID` and `GITHUB_SECRET` environment variables. |
|
||||
| GOOGLE_AUTH_ENABLED | Was used to enable Google OAuth, but from v1.6, you can just set the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` environment variables. |
|
||||
| AZUREAD_AUTH_ENABLED | Was used to enable AzureAD OAuth, but from v1.6, you can just set the `AZUREAD_CLIENT_ID`, `AZUREAD_CLIENT_SECRET` & `AZUREAD_TENANT_ID` environment variables. |
|
||||
|
||||
## v1.2
|
||||
## v1.1 -> v1.2
|
||||
|
||||
Formbricks v1.2 ships a lot of features targeting our Link Surveys. We have also improved our security posture to be as robust as ever. However, it also comes with a few breaking changes specifically with the environment variables. This guide will help you migrate your existing Formbricks instance to v1.2 without any hassles or build errors.
|
||||
|
||||
### New Environment Variables
|
||||
|
||||
| Environment Variable | Required | Recommended Generation | Comments |
|
||||
| -------------------- | -------- | ----------------------- | ----------------------------------------------------------- |
|
||||
| ENCRYPTION_KEY | true | `openssl rand -hex 32` | Needed for 2 Factor Authentication |
|
||||
| SHORT_URL_BASE | false | `<your-short-base-url>` | Needed if you want to enable shorter links for Link Surveys |
|
||||
| Environment Variable | Required | Recommended Generation | Comments |
|
||||
| -------------------- | -------- | ------------------------------ | ----------------------------------------------------------- |
|
||||
| ENCRYPTION_KEY | true | `openssl rand -hex 32` | Needed for 2 Factor Authentication |
|
||||
| SHORT_URL_BASE | false | `<your-short-base-url>` | Needed if you want to enable shorter links for Link Surveys |
|
||||
|
||||
### Deprecated / Removed Environment Variables
|
||||
|
||||
@@ -110,7 +25,7 @@ Formbricks v1.2 ships a lot of features targeting our Link Surveys. We have also
|
||||
| -------------------- | ------------------------------------------------------------------------- |
|
||||
| SURVEY_BASE_URL | The WEBAPP_URL is now used to determine the survey base url in all places |
|
||||
|
||||
## v1.1
|
||||
## v1.0 -> v1.1
|
||||
|
||||
Formbricks v1.1 includes a lot of new features and improvements. However, it also comes with a few breaking changes specifically with the environment variables. This guide will help you migrate your existing Formbricks instance to v1.1 without losing any data.
|
||||
|
||||
@@ -198,9 +113,6 @@ x-environment: &environment
|
||||
# Uncomment the below and set it to 1 to disable Signups
|
||||
# SIGNUP_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable loging in with email
|
||||
# EMAIL_AUTH_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Invites
|
||||
# INVITE_DISABLED:
|
||||
|
||||
@@ -224,7 +136,6 @@ x-environment: &environment
|
||||
# GOOGLE_CLIENT_SECRET:
|
||||
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
Did we miss something? Are you still facing issues migrating your app? [Join our Discord!](https://formbricks.com/discord) We'd be happy to help!
|
||||
|
||||
@@ -10,16 +10,8 @@ export const metadata = {
|
||||
|
||||
If you want to quickly set up a production instance of Formbricks on a server running Ubuntu, we've got you covered! This method utilizes a convenient shell script that takes care of everything, including Docker, Postgres DB, and SSL certificate configuration. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
|
||||
|
||||
<Note>
|
||||
This setup installs **Traefik** to work as a **reverse proxy**. This configuration is crucial for directing incoming traffic to the correct container, allowing Formbricks to be accessible from the internet securely.
|
||||
Traefik is chosen for its simplicity and automatic SSL management with Let's Encrypt.
|
||||
</Note>
|
||||
|
||||
|
||||
This is the quickest way to get Formbricks up and running on an Ubuntu server. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
|
||||
|
||||
If you want to run Formbricks on a different OS or have more control over the installation process, you can follow the [advanced installation guide](/docs/self-hosting/docker) with Docker.
|
||||
|
||||
### Requirements
|
||||
|
||||
Before you proceed, make sure you have the following:
|
||||
|
||||
@@ -23,7 +23,7 @@ const variantStyles = {
|
||||
"rounded-full bg-slate-900 py-1 px-3 text-white hover:bg-slate-700 dark:bg-teal-500 dark:text-white dark:hover:bg-teal-400",
|
||||
outline:
|
||||
"rounded-full py-1 px-3 text-slate-700 ring-1 ring-inset ring-slate-900/10 hover:bg-slate-900/2.5 hover:text-slate-900 dark:text-slate-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white",
|
||||
text: "text-teal-500 hover:text-teal-700 dark:text-teal-400 dark:hover:text-teal-300",
|
||||
text: "text-teal-500 hover:text-teal-700 dark:text-teal-400 dark:hover:text-teal-600",
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Libraries() {
|
||||
<a
|
||||
key={library.name}
|
||||
href={library.href}
|
||||
className="flex flex-row-reverse gap-6 rounded-2xl p-6 transition-all duration-100 ease-in-out hover:cursor-pointer hover:bg-slate-100/50 dark:hover:bg-slate-800/50">
|
||||
className="flex flex-row-reverse gap-6 rounded-2xl p-6 transition-all duration-100 ease-in-out hover:cursor-pointer hover:bg-slate-100/50">
|
||||
<div className="flex-auto">
|
||||
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">{library.name}</h3>
|
||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">{library.description}</p>
|
||||
|
||||
@@ -207,7 +207,6 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Identify Users", href: "/docs/link-surveys/user-identification" },
|
||||
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
|
||||
{ title: "Source Tracking", href: "/docs/link-surveys/source-tracking" },
|
||||
{ title: "Hidden Fields", href: "/docs/link-surveys/hidden-fields" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -243,8 +242,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Advanced Setup", href: "/docs/self-hosting/docker" },
|
||||
{ title: "Configure", href: "/docs/self-hosting/external-auth-providers" },
|
||||
{ title: "Migration Guide", href: "/docs/self-hosting/migration-guide" },
|
||||
{ title: "License", href: "/docs/self-hosting/license" },
|
||||
{ title: "Enterprise License", href: "/docs/self-hosting/enterprise" },
|
||||
{ title: "Self-hosting License", href: "/docs/self-hosting/license" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MousePointerClickIcon } from "lucide-react";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
@@ -20,7 +20,7 @@ export const AddNoCodeEventModalDummy: React.FC<EventDetailModalProps> = ({ open
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-6 w-6 text-slate-500">
|
||||
<MousePointerClickIcon className="h-5 w-5" />
|
||||
<CursorArrowRaysIcon />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-medium text-slate-700 dark:text-slate-300">Add Action</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import HtmlBody from "./HtmlBody";
|
||||
import { TSurveyCTAQuestion } from "./types";
|
||||
|
||||
interface CTAQuestionProps {
|
||||
question: TSurveyCTAQuestion;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
import PreviewSurvey from "./PreviewSurvey";
|
||||
import { findTemplateByName } from "./templates";
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
interface DemoPreviewProps {
|
||||
template: string;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
import PreviewSurvey from "./PreviewSurvey";
|
||||
import TemplateList from "./TemplateList";
|
||||
import { templates } from "./templates";
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
export default function SurveyTemplatesPage({}) {
|
||||
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as DOMPurify from "dompurify";
|
||||
|
||||
export default function HtmlBody({ htmlString, questionId }: { htmlString: string; questionId: string }) {
|
||||
return (
|
||||
<label
|
||||
htmlFor={questionId}
|
||||
className="fb-block fb-font-normal fb-leading-6 text-sm text-slate-500 dark:text-slate-300"
|
||||
dangerouslySetInnerHTML={{ __html: htmlString }}></label>
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlString) }}></label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyMultipleChoiceMultiQuestion } from "./types";
|
||||
|
||||
interface MultipleChoiceMultiProps {
|
||||
question: TSurveyMultipleChoiceMultiQuestion;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyMultipleChoiceSingleQuestion } from "./types";
|
||||
|
||||
interface MultipleChoiceSingleProps {
|
||||
question: TSurveyMultipleChoiceSingleQuestion;
|
||||
@@ -20,7 +20,6 @@ export default function MultipleChoiceSingleQuestion({
|
||||
brandColor,
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyNPSQuestion } from "./types";
|
||||
|
||||
interface NPSQuestionProps {
|
||||
question: TSurveyNPSQuestion;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { TSurveyOpenTextQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyOpenTextQuestion } from "./types";
|
||||
|
||||
interface OpenTextQuestionProps {
|
||||
question: TSurveyOpenTextQuestion;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Modal from "./Modal";
|
||||
import QuestionConditional from "./QuestionConditional";
|
||||
import ThankYouCard from "./ThankYouCard";
|
||||
import { TSurvey, TSurveyQuestion } from "./types";
|
||||
|
||||
interface PreviewSurveyProps {
|
||||
localSurvey?: TSurvey;
|
||||
@@ -66,8 +67,8 @@ export default function PreviewSurvey({
|
||||
{activeQuestionId == "thank-you-card" ? (
|
||||
<ThankYouCard
|
||||
brandColor={brandColor}
|
||||
headline={localSurvey?.thankYouCard?.headline!}
|
||||
subheader={localSurvey?.thankYouCard?.subheader!}
|
||||
headline={localSurvey?.thankYouCard?.headline || ""}
|
||||
subheader={localSurvey?.thankYouCard?.subheader || ""}
|
||||
/>
|
||||
) : (
|
||||
questions.map(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
|
||||
import CTAQuestion from "./CTAQuestion";
|
||||
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
|
||||
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
|
||||
import NPSQuestion from "./NPSQuestion";
|
||||
import OpenTextQuestion from "./OpenTextQuestion";
|
||||
import RatingQuestion from "./RatingQuestion";
|
||||
import { TSurveyQuestion, TSurveyQuestionType } from "./types";
|
||||
|
||||
interface QuestionConditionalProps {
|
||||
question: TSurveyQuestion;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyRatingQuestion } from "./types";
|
||||
|
||||
interface RatingQuestionProps {
|
||||
question: TSurveyRatingQuestion;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
import { templates } from "./templates";
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
type TemplateList = {
|
||||
onTemplateClick: (template: TTemplate) => void;
|
||||
@@ -53,11 +53,12 @@ export default function TemplateList({ onTemplateClick, activeTemplate }: Templa
|
||||
{templates
|
||||
.filter((template) => selectedFilter === ALL_CATEGORY_NAME || template.category === selectedFilter)
|
||||
.map((template: TTemplate) => (
|
||||
<div
|
||||
key={template.name}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onTemplateClick(template); // Pass the 'template' object instead of 'activeTemplate'
|
||||
}}
|
||||
key={template.name}
|
||||
className={cn(
|
||||
activeTemplate?.name === template.name && "ring-brand ring-2",
|
||||
"duration-120 group relative rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105 dark:bg-slate-700"
|
||||
@@ -70,7 +71,7 @@ export default function TemplateList({ onTemplateClick, activeTemplate }: Templa
|
||||
{template.name}
|
||||
</h3>
|
||||
<p className="text-left text-xs text-slate-600 dark:text-slate-400">{template.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TSurveyHiddenFields, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
import {
|
||||
AppPieChartIcon,
|
||||
ArrowRightCircleIcon,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
DashboardIcon,
|
||||
DogChaserIcon,
|
||||
DoorIcon,
|
||||
EmailIcon,
|
||||
FeedbackIcon,
|
||||
GaugeSpeedFastIcon,
|
||||
HeartCommentIcon,
|
||||
@@ -26,21 +26,23 @@ import {
|
||||
VideoTabletAdjustIcon,
|
||||
} from "@formbricks/ui/icons";
|
||||
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
const thankYouCardDefault = {
|
||||
enabled: true,
|
||||
headline: "Thank you!",
|
||||
subheader: "TWe appreciate your feedback.",
|
||||
subheader: "We appreciate your feedback.",
|
||||
};
|
||||
|
||||
const welcomeCardDefault = {
|
||||
enabled: true,
|
||||
headline: "Welcome!",
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
};
|
||||
|
||||
const hiddenFieldsDefault: TSurveyHiddenFields = {
|
||||
enabled: true,
|
||||
fieldIds: [],
|
||||
};
|
||||
|
||||
export const customSurvey: TTemplate = {
|
||||
name: "Start from scratch",
|
||||
description: "Create a survey without template.",
|
||||
@@ -1229,12 +1231,12 @@ export const templates: TTemplate[] = [
|
||||
},
|
||||
{
|
||||
name: "Improve Newsletter Content",
|
||||
icon: EmailIcon,
|
||||
category: "Growth",
|
||||
description: "Find out how your subscribers like your newsletter content.",
|
||||
objectives: ["increase_conversion", "sharpen_marketing_messaging"],
|
||||
description: "Find out how your subscribers like your newsletter content.",
|
||||
preset: {
|
||||
name: "Improve Newsletter Content",
|
||||
welcomeCard: welcomeCardDefault,
|
||||
questions: [
|
||||
{
|
||||
id: createId(),
|
||||
@@ -1275,11 +1277,8 @@ export const templates: TTemplate[] = [
|
||||
dismissButtonLabel: "Find your own friends",
|
||||
},
|
||||
],
|
||||
welcomeCard: welcomeCardDefault,
|
||||
thankYouCard: thankYouCardDefault,
|
||||
hiddenFields: {
|
||||
enabled: false,
|
||||
},
|
||||
hiddenFields: hiddenFieldsDefault,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
import z from "zod";
|
||||
|
||||
export enum TSurveyQuestionType {
|
||||
FileUpload = "fileUpload",
|
||||
OpenText = "openText",
|
||||
MultipleChoiceSingle = "multipleChoiceSingle",
|
||||
MultipleChoiceMulti = "multipleChoiceMulti",
|
||||
NPS = "nps",
|
||||
CTA = "cta",
|
||||
Rating = "rating",
|
||||
Consent = "consent",
|
||||
PictureSelection = "pictureSelection",
|
||||
Cal = "cal",
|
||||
Date = "date",
|
||||
}
|
||||
|
||||
export const ZAllowedFileExtension = z.enum([
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"plain",
|
||||
"csv",
|
||||
"mp4",
|
||||
"mov",
|
||||
"avi",
|
||||
"mkv",
|
||||
"webm",
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
"tar",
|
||||
]);
|
||||
|
||||
export type TAllowedFileExtension = z.infer<typeof ZAllowedFileExtension>;
|
||||
|
||||
export const ZUserObjective = z.enum([
|
||||
"increase_conversion",
|
||||
"improve_user_retention",
|
||||
"increase_user_adoption",
|
||||
"sharpen_marketing_messaging",
|
||||
"support_sales",
|
||||
"other",
|
||||
]);
|
||||
|
||||
export type TUserObjective = z.infer<typeof ZUserObjective>;
|
||||
|
||||
export const ZSurveyWelcomeCard = z.object({
|
||||
enabled: z.boolean(),
|
||||
headline: z.string().optional(),
|
||||
html: z.string().optional(),
|
||||
fileUrl: z.string().optional(),
|
||||
buttonLabel: z.string().optional(),
|
||||
timeToFinish: z.boolean().default(true),
|
||||
showResponseCount: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type TSurveyWelcomeCard = z.infer<typeof ZSurveyWelcomeCard>;
|
||||
|
||||
export const ZSurveyThankYouCard = z.object({
|
||||
enabled: z.boolean(),
|
||||
headline: z.optional(z.string()),
|
||||
subheader: z.optional(z.string()),
|
||||
buttonLabel: z.optional(z.string()),
|
||||
buttonLink: z.optional(z.string()),
|
||||
imageUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TSurveyThankYouCard = z.infer<typeof ZSurveyThankYouCard>;
|
||||
|
||||
export const ZSurveyHiddenFields = z.object({
|
||||
enabled: z.boolean(),
|
||||
fieldIds: z.optional(z.array(z.string())),
|
||||
});
|
||||
|
||||
export type TSurveyHiddenFields = z.infer<typeof ZSurveyHiddenFields>;
|
||||
|
||||
export const ZSurveyChoice = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
});
|
||||
|
||||
export type TSurveyChoice = z.infer<typeof ZSurveyChoice>;
|
||||
|
||||
export const ZSurveyPictureChoice = z.object({
|
||||
id: z.string(),
|
||||
imageUrl: z.string(),
|
||||
});
|
||||
|
||||
export type TSurveyPictureChoice = z.infer<typeof ZSurveyPictureChoice>;
|
||||
|
||||
export const ZSurveyLogicCondition = z.enum([
|
||||
"accepted",
|
||||
"clicked",
|
||||
"submitted",
|
||||
"skipped",
|
||||
"equals",
|
||||
"notEquals",
|
||||
"lessThan",
|
||||
"lessEqual",
|
||||
"greaterThan",
|
||||
"greaterEqual",
|
||||
"includesAll",
|
||||
"includesOne",
|
||||
"uploaded",
|
||||
"notUploaded",
|
||||
"booked",
|
||||
]);
|
||||
|
||||
export type TSurveyLogicCondition = z.infer<typeof ZSurveyLogicCondition>;
|
||||
|
||||
export const ZSurveyLogicBase = z.object({
|
||||
condition: ZSurveyLogicCondition.optional(),
|
||||
value: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
destination: z.union([z.string(), z.literal("end")]).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyFileUploadLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["uploaded", "notUploaded"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyOpenTextLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyConsentLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["skipped", "accepted"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceSingleLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped", "equals", "notEquals"]).optional(),
|
||||
value: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceMultiLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped", "includesAll", "includesOne", "equals"]).optional(),
|
||||
value: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyNPSLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.enum([
|
||||
"equals",
|
||||
"notEquals",
|
||||
"lessThan",
|
||||
"lessEqual",
|
||||
"greaterThan",
|
||||
"greaterEqual",
|
||||
"submitted",
|
||||
"skipped",
|
||||
])
|
||||
.optional(),
|
||||
value: z.union([z.string(), z.number()]).optional(),
|
||||
});
|
||||
|
||||
const ZSurveyCTALogic = ZSurveyLogicBase.extend({
|
||||
// "submitted" condition is legacy and should be removed later
|
||||
condition: z.enum(["clicked", "submitted", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
const ZSurveyRatingLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.enum([
|
||||
"equals",
|
||||
"notEquals",
|
||||
"lessThan",
|
||||
"lessEqual",
|
||||
"greaterThan",
|
||||
"greaterEqual",
|
||||
"submitted",
|
||||
"skipped",
|
||||
])
|
||||
.optional(),
|
||||
value: z.union([z.string(), z.number()]).optional(),
|
||||
});
|
||||
|
||||
const ZSurveyPictureSelectionLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
const ZSurveyCalLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["booked", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyLogic = z.union([
|
||||
ZSurveyOpenTextLogic,
|
||||
ZSurveyConsentLogic,
|
||||
ZSurveyMultipleChoiceSingleLogic,
|
||||
ZSurveyMultipleChoiceMultiLogic,
|
||||
ZSurveyNPSLogic,
|
||||
ZSurveyCTALogic,
|
||||
ZSurveyRatingLogic,
|
||||
ZSurveyPictureSelectionLogic,
|
||||
ZSurveyFileUploadLogic,
|
||||
ZSurveyCalLogic,
|
||||
]);
|
||||
|
||||
export type TSurveyLogic = z.infer<typeof ZSurveyLogic>;
|
||||
|
||||
const ZSurveyQuestionBase = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
headline: z.string(),
|
||||
subheader: z.string().optional(),
|
||||
imageUrl: z.string().optional(),
|
||||
required: z.boolean(),
|
||||
buttonLabel: z.string().optional(),
|
||||
backButtonLabel: z.string().optional(),
|
||||
scale: z.enum(["number", "smiley", "star"]).optional(),
|
||||
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]).optional(),
|
||||
logic: z.array(ZSurveyLogic).optional(),
|
||||
isDraft: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyOpenTextQuestionInputType = z.enum(["text", "email", "url", "number", "phone"]);
|
||||
export type TSurveyOpenTextQuestionInputType = z.infer<typeof ZSurveyOpenTextQuestionInputType>;
|
||||
|
||||
export const ZSurveyOpenTextQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.OpenText),
|
||||
placeholder: z.string().optional(),
|
||||
longAnswer: z.boolean().optional(),
|
||||
logic: z.array(ZSurveyOpenTextLogic).optional(),
|
||||
inputType: ZSurveyOpenTextQuestionInputType.optional().default("text"),
|
||||
});
|
||||
|
||||
export type TSurveyOpenTextQuestion = z.infer<typeof ZSurveyOpenTextQuestion>;
|
||||
|
||||
export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Consent),
|
||||
html: z.string().optional(),
|
||||
label: z.string(),
|
||||
dismissButtonLabel: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
logic: z.array(ZSurveyConsentLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyConsentQuestion = z.infer<typeof ZSurveyConsentQuestion>;
|
||||
|
||||
export const ZSurveyMultipleChoiceSingleQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.MultipleChoiceSingle),
|
||||
choices: z.array(ZSurveyChoice),
|
||||
logic: z.array(ZSurveyMultipleChoiceSingleLogic).optional(),
|
||||
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
|
||||
otherOptionPlaceholder: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TSurveyMultipleChoiceSingleQuestion = z.infer<typeof ZSurveyMultipleChoiceSingleQuestion>;
|
||||
|
||||
export const ZSurveyMultipleChoiceMultiQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.MultipleChoiceMulti),
|
||||
choices: z.array(ZSurveyChoice),
|
||||
logic: z.array(ZSurveyMultipleChoiceMultiLogic).optional(),
|
||||
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
|
||||
otherOptionPlaceholder: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TSurveyMultipleChoiceMultiQuestion = z.infer<typeof ZSurveyMultipleChoiceMultiQuestion>;
|
||||
|
||||
export const ZSurveyNPSQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.NPS),
|
||||
lowerLabel: z.string(),
|
||||
upperLabel: z.string(),
|
||||
logic: z.array(ZSurveyNPSLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyNPSQuestion = z.infer<typeof ZSurveyNPSQuestion>;
|
||||
|
||||
export const ZSurveyCTAQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.CTA),
|
||||
html: z.string().optional(),
|
||||
buttonUrl: z.string().optional(),
|
||||
buttonExternal: z.boolean(),
|
||||
dismissButtonLabel: z.string().optional(),
|
||||
logic: z.array(ZSurveyCTALogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyCTAQuestion = z.infer<typeof ZSurveyCTAQuestion>;
|
||||
|
||||
// export const ZSurveyWelcomeQuestion = ZSurveyQuestionBase.extend({
|
||||
// type: z.literal(TSurveyQuestionType.Welcome),
|
||||
// html: z.string().optional(),
|
||||
// fileUrl: z.string().optional(),
|
||||
// buttonUrl: z.string().optional(),
|
||||
// timeToFinish: z.boolean().default(false),
|
||||
// logic: z.array(ZSurveyCTALogic).optional(),
|
||||
// });
|
||||
|
||||
// export type TSurveyWelcomeQuestion = z.infer<typeof ZSurveyWelcomeQuestion>;
|
||||
|
||||
export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Rating),
|
||||
scale: z.enum(["number", "smiley", "star"]),
|
||||
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]),
|
||||
lowerLabel: z.string(),
|
||||
upperLabel: z.string(),
|
||||
logic: z.array(ZSurveyRatingLogic).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyDateQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Date),
|
||||
html: z.string().optional(),
|
||||
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
|
||||
});
|
||||
|
||||
export type TSurveyDateQuestion = z.infer<typeof ZSurveyDateQuestion>;
|
||||
|
||||
export type TSurveyRatingQuestion = z.infer<typeof ZSurveyRatingQuestion>;
|
||||
|
||||
export const ZSurveyPictureSelectionQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.PictureSelection),
|
||||
allowMulti: z.boolean().optional().default(false),
|
||||
choices: z.array(ZSurveyPictureChoice),
|
||||
logic: z.array(ZSurveyPictureSelectionLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyPictureSelectionQuestion = z.infer<typeof ZSurveyPictureSelectionQuestion>;
|
||||
|
||||
export const ZSurveyFileUploadQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.FileUpload),
|
||||
allowMultipleFiles: z.boolean(),
|
||||
maxSizeInMB: z.number().optional(),
|
||||
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
|
||||
logic: z.array(ZSurveyFileUploadLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyFileUploadQuestion = z.infer<typeof ZSurveyFileUploadQuestion>;
|
||||
|
||||
export const ZSurveyCalQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Cal),
|
||||
calUserName: z.string(),
|
||||
logic: z.array(ZSurveyCalLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyCalQuestion = z.infer<typeof ZSurveyCalQuestion>;
|
||||
|
||||
export const ZSurveyQuestion = z.union([
|
||||
ZSurveyOpenTextQuestion,
|
||||
ZSurveyConsentQuestion,
|
||||
ZSurveyMultipleChoiceSingleQuestion,
|
||||
ZSurveyMultipleChoiceMultiQuestion,
|
||||
ZSurveyNPSQuestion,
|
||||
ZSurveyCTAQuestion,
|
||||
ZSurveyRatingQuestion,
|
||||
ZSurveyPictureSelectionQuestion,
|
||||
ZSurveyDateQuestion,
|
||||
ZSurveyFileUploadQuestion,
|
||||
ZSurveyCalQuestion,
|
||||
]);
|
||||
|
||||
export type TSurveyQuestion = z.infer<typeof ZSurveyQuestion>;
|
||||
|
||||
export const ZSurveyQuestions = z.array(ZSurveyQuestion);
|
||||
|
||||
export type TSurveyQuestions = z.infer<typeof ZSurveyQuestions>;
|
||||
|
||||
export const ZSurveyClosedMessage = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
heading: z.string().optional(),
|
||||
subheading: z.string().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional();
|
||||
|
||||
export type TSurveyClosedMessage = z.infer<typeof ZSurveyClosedMessage>;
|
||||
|
||||
export const ZSurveyAttributeFilter = z.object({
|
||||
attributeClassId: z.string().cuid2(),
|
||||
condition: z.enum(["equals", "notEquals"]),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type TSurveyAttributeFilter = z.infer<typeof ZSurveyAttributeFilter>;
|
||||
|
||||
export const ZSurveyType = z.enum(["web", "email", "link", "mobile"]);
|
||||
|
||||
export type TSurveyType = z.infer<typeof ZSurveyType>;
|
||||
|
||||
const ZSurveyStatus = z.enum(["draft", "inProgress", "paused", "completed"]);
|
||||
|
||||
export type TSurveyStatus = z.infer<typeof ZSurveyStatus>;
|
||||
|
||||
const ZSurveyDisplayOption = z.enum(["displayOnce", "displayMultiple", "respondMultiple"]);
|
||||
|
||||
export type TSurveyDisplayOption = z.infer<typeof ZSurveyDisplayOption>;
|
||||
|
||||
export const ZColor = z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/);
|
||||
|
||||
export const ZPlacement = z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]);
|
||||
|
||||
export type TPlacement = z.infer<typeof ZPlacement>;
|
||||
|
||||
export const ZSurveyProductOverwrites = z.object({
|
||||
brandColor: ZColor.nullish(),
|
||||
highlightBorderColor: ZColor.nullish(),
|
||||
placement: ZPlacement.nullish(),
|
||||
clickOutsideClose: z.boolean().nullish(),
|
||||
darkOverlay: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
|
||||
|
||||
export const ZSurveyStylingBackground = z.object({
|
||||
bg: z.string().nullish(),
|
||||
bgType: z.enum(["animation", "color", "image"]).nullish(),
|
||||
brightness: z.number().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyStylingBackground = z.infer<typeof ZSurveyStylingBackground>;
|
||||
|
||||
export const ZSurveyStyling = z.object({
|
||||
background: ZSurveyStylingBackground.nullish(),
|
||||
hideProgressBar: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyStyling = z.infer<typeof ZSurveyStyling>;
|
||||
|
||||
export const ZSurveySingleUse = z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
heading: z.optional(z.string()),
|
||||
subheading: z.optional(z.string()),
|
||||
isEncrypted: z.boolean(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
export type TSurveySingleUse = z.infer<typeof ZSurveySingleUse>;
|
||||
|
||||
export const ZSurveyVerifyEmail = z
|
||||
.object({
|
||||
name: z.optional(z.string()),
|
||||
subheading: z.optional(z.string()),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export type TSurveyVerifyEmail = z.infer<typeof ZSurveyVerifyEmail>;
|
||||
|
||||
export const ZSurvey = z.object({
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string(),
|
||||
type: ZSurveyType,
|
||||
environmentId: z.string(),
|
||||
createdBy: z.string().nullable(),
|
||||
status: ZSurveyStatus,
|
||||
attributeFilters: z.array(ZSurveyAttributeFilter),
|
||||
displayOption: ZSurveyDisplayOption,
|
||||
autoClose: z.number().nullable(),
|
||||
triggers: z.array(z.string()),
|
||||
redirectUrl: z.string().url().nullable(),
|
||||
recontactDays: z.number().nullable(),
|
||||
welcomeCard: ZSurveyWelcomeCard,
|
||||
questions: ZSurveyQuestions,
|
||||
thankYouCard: ZSurveyThankYouCard,
|
||||
hiddenFields: ZSurveyHiddenFields,
|
||||
delay: z.number(),
|
||||
autoComplete: z.number().nullable(),
|
||||
closeOnDate: z.date().nullable(),
|
||||
productOverwrites: ZSurveyProductOverwrites.nullable(),
|
||||
styling: ZSurveyStyling.nullable(),
|
||||
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
|
||||
singleUse: ZSurveySingleUse.nullable(),
|
||||
verifyEmail: ZSurveyVerifyEmail.nullable(),
|
||||
pin: z.string().nullable().optional(),
|
||||
resultShareKey: z.string().nullable(),
|
||||
displayPercentage: z.number().min(1).max(100).nullable(),
|
||||
});
|
||||
|
||||
export type TSurvey = z.infer<typeof ZSurvey>;
|
||||
|
||||
export const ZTemplate = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.any().optional(),
|
||||
category: z
|
||||
.enum(["Product Experience", "Exploration", "Growth", "Increase Revenue", "Customer Success"])
|
||||
.optional(),
|
||||
objectives: z.array(ZUserObjective).optional(),
|
||||
preset: z.object({
|
||||
name: z.string(),
|
||||
welcomeCard: ZSurveyWelcomeCard,
|
||||
questions: ZSurveyQuestions,
|
||||
thankYouCard: ZSurveyThankYouCard,
|
||||
hiddenFields: ZSurveyHiddenFields,
|
||||
}),
|
||||
});
|
||||
|
||||
export type TTemplate = z.infer<typeof ZTemplate>;
|
||||
@@ -1,66 +1,87 @@
|
||||
import HeadingCentered from "@/components/shared/HeadingCentered";
|
||||
import SeoFaq from "@/components/shared/seo/SeoFaq";
|
||||
import { FAQPageJsonLd } from "next-seo";
|
||||
|
||||
const FAQs = [
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
|
||||
|
||||
const FAQ_DATA = [
|
||||
{
|
||||
question: "What is Formbricks?",
|
||||
answer:
|
||||
"Formbricks is an experience management platform built on top of the fastest growing open source survey infrastructure out there. It aims to assist businesses in capturing and understanding customer insights and emotions towards their products and services. Designed to integrate seamlessly with various platforms, Formbricks focuses on user research, emphasizing data privacy and requiring minimal development effort for integration.",
|
||||
answer: () => (
|
||||
<>
|
||||
Formbricks is an open-source Experience Management tool that helps businesses understand what
|
||||
customers think and feel about their products. It integrates natively into your platform to conduct
|
||||
user research with a focus on data privacy and minimal development intervention.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "How do I integrate Formbricks into my application?",
|
||||
answer:
|
||||
"Integrating Formbricks into an application is effortless. For web applications, it involves adding a simple script tag to the HTML head. For applications built with modern frameworks such as React, Vue, or Svelte, Formbricks can be installed via NPM. Initialization with specific environment details completes the setup. Detailed instructions and framework guides are readily available in the detailed Formbricks documentation.",
|
||||
answer: () => (
|
||||
<>
|
||||
Integrating Formbricks is a breeze. Simply copy a script tag to your HTML head, or use NPM to install
|
||||
Formbricks for platforms like React, Vue, Svelte, etc. Once installed, initialize Formbricks with your
|
||||
environment details. Learn more with our framework guides{" "}
|
||||
<a href="/docs/getting-started/framework-guides" className="text-brand-dark dark:text-brand-light">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Is Formbricks GDPR compliant?",
|
||||
answer:
|
||||
"Indeed, Formbricks ensures full GDPR compliance, emphasizing the protection of user data privacy. It offers both cloud-based solutions and self-hosting options, adhering to data privacy regulations and making it a trusted choice for secure open source survey tool deployment.",
|
||||
answer: () => (
|
||||
<>
|
||||
Yes, Formbricks is fully GDPR compliant. Whether you use our cloud solution or decide to self-host, we
|
||||
ensure compliance with all data privacy regulations.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Can I self-host Formbricks?",
|
||||
answer:
|
||||
"Certainly! Formbricks encourages self-hosting, providing users with greater control over their data and compliance. This option underscores Formbricks' commitment to offering versatile and free open source experience management software, ensuring users can adapt the platform to their unique requirements. Detailed self-hosting documentation is available for users seeking to leverage this capability.",
|
||||
answer: () => (
|
||||
<>
|
||||
Absolutely! We provide an option for users to host Formbricks on their own server, ensuring even more
|
||||
control over data and compliance. And the best part? Self-hosting is available for free, always. For
|
||||
documentation on self hosting, click{" "}
|
||||
<a href="/docs/self-hosting/deployment" className="text-brand-dark dark:text-brand-light">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "How does Formbricks pricing work?",
|
||||
answer:
|
||||
"Formbricks introduces a 'Free forever' plan, showcasing its commitment to making open source survey platforms universally accessible. This plan features unlimited surveys and in-product surveys, among other functionalities. Self-hosting users can enjoy all the benefits of the free plan with additional features at no extra cost. For those seeking advanced features Formbricks invites you to explore the pricing section for more information.",
|
||||
},
|
||||
{
|
||||
question: "How does Formbricks make money?",
|
||||
answer:
|
||||
"Formbricks employs the 'Open Core' business model. The core of the Formbricks application is offered for free. Formbricks monetizes by providing advanced features and services, typically catering to the needs of larger clients, thereby generating revenue.",
|
||||
},
|
||||
{
|
||||
question: "What is the best open source survey software available?",
|
||||
answer:
|
||||
"Identifying the best open source survey software requires evaluating features, flexibility, and support. Formbricks is a noteworthy contender, offering comprehensive experience management solutions. This platform excels in enabling businesses to delve into customer insights and feedback, offering versatility and ease of system integration.",
|
||||
},
|
||||
{
|
||||
question: "Can open source survey platforms be customized for my business needs?",
|
||||
answer:
|
||||
"Definitely. Platforms like Formbricks exemplify the customizability of open source survey tools, allowing for extensive tailoring to meet specific business requirements. Access to the source code enables deep customization, from branding adjustments to complex integrations with existing systems, underscoring the flexibility of open source experience management solutions.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"What advantages does using an experience management platform offer over traditional survey tools?",
|
||||
answer:
|
||||
"Experience management platforms, especially those built on open source foundations, offer a more holistic view of customer interactions compared to traditional survey tools. They enable real-time collection, analysis, and application of customer feedback, ensuring a thorough understanding of the customer journey. This comprehensive insight facilitates informed decision-making and boosts customer satisfaction.",
|
||||
answer: () => (
|
||||
<>
|
||||
Formbricks offers a Free forever plan on the cloud that includes unlimited surveys, in-product
|
||||
surveys, and more. We also provide a self-hosting option which includes all free features and more,
|
||||
available at no cost. If you require additional features or responses, check out our pricing section
|
||||
above for more details.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const faqJsonLdData = FAQ_DATA.map((faq) => ({
|
||||
questionName: faq.question,
|
||||
acceptedAnswerText: faq.answer(),
|
||||
}));
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<div>
|
||||
<HeadingCentered heading="Frequently asked questions" teaser="FAQ" />
|
||||
<SeoFaq
|
||||
faqs={FAQs}
|
||||
headline="Open Source Experience Management Platform"
|
||||
description="Formbricks is an Experience Management Platform built of top of the largest open source survey infrastructure worldwide."
|
||||
datePublished="2023-10-11"
|
||||
dateModified="2024-03-12"
|
||||
/>
|
||||
<div className="max-w-7xl py-4 sm:px-6 sm:pb-6 lg:px-8" id="faq">
|
||||
<FAQPageJsonLd mainEntity={faqJsonLdData} />
|
||||
<HeadingCentered heading="Frequently Asked Questions" teaser="FAQ" closer />
|
||||
<Accordion type="single" collapsible className="px-4 sm:px-0">
|
||||
{FAQ_DATA.map((faq, index) => (
|
||||
<AccordionItem key={`item-${index}`} value={`item-${index + 1}`}>
|
||||
<AccordionTrigger>{faq.question}</AccordionTrigger>
|
||||
<AccordionContent>{faq.answer()}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,39 +24,42 @@ const features = [
|
||||
];
|
||||
export const Features: React.FC = () => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<HeadingCentered
|
||||
teaser="Data Privacy at heart"
|
||||
heading="The only open-source solution"
|
||||
subheading="Comply with all data privacy regulation with ease. Self-host if you want."
|
||||
/>
|
||||
<div className="relative px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 lg:pt-24">
|
||||
<div className="relative mx-auto max-w-7xl">
|
||||
<HeadingCentered
|
||||
closer
|
||||
teaser="Data Privacy at heart"
|
||||
heading="The only open-source solution"
|
||||
subheading="Comply with all data privacy regulation with ease. Self-host if you want."
|
||||
/>
|
||||
|
||||
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
|
||||
{features.map((feature) => {
|
||||
const IconComponent: React.ElementType = feature.icon;
|
||||
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
|
||||
{features.map((feature) => {
|
||||
const IconComponent: React.ElementType = feature.icon;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={feature.id}
|
||||
className="relative col-span-1 mt-16 flex flex-col rounded-xl bg-slate-100 text-center dark:bg-slate-700">
|
||||
<div className="absolute -mt-12 w-full">
|
||||
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-slate-200 shadow dark:bg-slate-800">
|
||||
<IconComponent className="text-brand-dark dark:text-brand-light mx-auto h-10 w-10 flex-shrink-0" />
|
||||
return (
|
||||
<li
|
||||
key={feature.id}
|
||||
className="relative col-span-1 mt-16 flex flex-col rounded-xl bg-slate-100 text-center dark:bg-slate-700">
|
||||
<div className="absolute -mt-12 w-full">
|
||||
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-slate-200 shadow dark:bg-slate-800">
|
||||
<IconComponent className="text-brand-dark dark:text-brand-light mx-auto h-10 w-10 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col p-10">
|
||||
<h3 className="my-4 text-lg font-medium text-slate-800 dark:text-slate-200">
|
||||
{feature.name}
|
||||
</h3>
|
||||
<dl className="mt-1 flex flex-grow flex-col justify-between">
|
||||
<dt className="sr-only">Description</dt>
|
||||
<dd className="text-sm text-slate-600 dark:text-slate-400">{feature.description}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="flex flex-1 flex-col p-10">
|
||||
<h3 className="my-4 text-lg font-medium text-slate-800 dark:text-slate-200">
|
||||
{feature.name}
|
||||
</h3>
|
||||
<dl className="mt-1 flex flex-grow flex-col justify-between">
|
||||
<dt className="sr-only">Description</dt>
|
||||
<dd className="text-sm text-slate-600 dark:text-slate-400">{feature.description}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
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 OptimoleLogo from "@/images/clients/optimole-logo.svg";
|
||||
import ThemeisleLogo from "@/images/clients/themeisle-logo.webp";
|
||||
import { ShieldCheckIcon, StarIcon } from "lucide-react";
|
||||
import NILogoLight from "@/images/clients/niLogoWhite.svg";
|
||||
import AnimationFallback from "@/public/animations/opensource-xm-platform-formbricks-fallback.png";
|
||||
import { ShieldCheckIcon, StarIcon } from "@heroicons/react/24/outline";
|
||||
import { usePlausible } from "next-plausible";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
import HeroAnimation from "./HeroAnimation";
|
||||
|
||||
export const Hero: React.FC = ({}) => {
|
||||
const plausible = usePlausible();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="text-center">
|
||||
<div className="px-4 pb-20 pt-16 text-center sm:px-6 lg:px-8 lg:pb-32 lg:pt-20">
|
||||
<div className="xs:text-sm flex items-center justify-center space-x-4 divide-x-2 text-xs text-slate-600">
|
||||
<p>
|
||||
<ShieldCheckIcon className="mb-1 inline h-4 w-4" /> Privacy-first
|
||||
@@ -40,27 +44,53 @@ export const Hero: React.FC = ({}) => {
|
||||
know what your customers need.
|
||||
</span>
|
||||
</p>
|
||||
<div className="mx-auto mt-5 max-w-xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0 lg:max-w-3xl">
|
||||
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-3 md:gap-10 lg:grid-cols-6">
|
||||
|
||||
<div className="mx-auto mt-5 max-w-2xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
|
||||
<div className="grid grid-cols-4 items-center gap-6 pt-2 md:gap-8">
|
||||
<Image
|
||||
src={FlixbusLogo}
|
||||
alt="Flixbus Flix Flixtrain Logo"
|
||||
className="rounded-lg pb-1 "
|
||||
className="rounded-lg pb-1 hover:opacity-100 md:opacity-50"
|
||||
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={CalLogoLight}
|
||||
alt="Cal Logo"
|
||||
className="block rounded-lg hover:opacity-100 md:opacity-50 dark:hidden"
|
||||
width={170}
|
||||
/>
|
||||
<Image
|
||||
src={CalLogoDark}
|
||||
alt="Cal Logo"
|
||||
className="hidden rounded-lg hover:opacity-100 md:opacity-50 dark:block"
|
||||
width={170}
|
||||
/>
|
||||
<Image
|
||||
src={CrowdLogoLight}
|
||||
alt="Crowd.dev Logo"
|
||||
className="block rounded-lg pb-1 dark:hidden"
|
||||
className="block rounded-lg pb-1 hover:opacity-100 md:opacity-50 dark:hidden"
|
||||
width={200}
|
||||
/>
|
||||
<Image
|
||||
src={CrowdLogoDark}
|
||||
alt="Crowd.dev Logo"
|
||||
className="hidden rounded-lg pb-1 hover:opacity-100 md:opacity-50 dark:block"
|
||||
width={200}
|
||||
/>
|
||||
<Image
|
||||
src={NILogoDark}
|
||||
alt="Neverinstall Logo"
|
||||
className="block pb-1 hover:opacity-100 md:opacity-50 dark:hidden"
|
||||
width={200}
|
||||
/>
|
||||
<Image
|
||||
src={NILogoLight}
|
||||
alt="Neverinstall Logo"
|
||||
className="hidden pb-1 hover:opacity-100 md:opacity-50 dark:block"
|
||||
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 className="hidden pt-14 md:block">
|
||||
<Button
|
||||
variant="highlight"
|
||||
@@ -69,7 +99,7 @@ export const Hero: React.FC = ({}) => {
|
||||
router.push("https://app.formbricks.com/auth/signup");
|
||||
plausible("Hero_CTA_GetStartedItsFree");
|
||||
}}>
|
||||
Get Started
|
||||
Get Started, it's Free
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -82,6 +112,9 @@ export const Hero: React.FC = ({}) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative px-2 md:px-0">
|
||||
<HeroAnimation fallbackImage={AnimationFallback} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export const HeroAnimation: React.FC<any> = ({ fallbackImage, ...props }) => {
|
||||
}, [lottie]);
|
||||
|
||||
return (
|
||||
<div className="relative hidden md:block" {...props}>
|
||||
<div className="relative" {...props}>
|
||||
<div ref={ref} />
|
||||
{!loaded && (
|
||||
<div className="absolute inset-0">
|
||||
|
||||
@@ -6,43 +6,62 @@ import Image from "next/image";
|
||||
|
||||
export const Highlights: React.FC = ({}) => {
|
||||
return (
|
||||
<div className="space-y-16">
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 dark:text-slate-200">
|
||||
Ask at the right moment,
|
||||
<br />
|
||||
<span className="font-light">get the data you need.</span>
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Follow up emails are so 2010. Ask users as they experience your product - and leverage a
|
||||
significantly higher conversion rate.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-100 py-6 pr-4 sm:py-16 sm:pr-8 dark:bg-slate-800">
|
||||
<Image src={ImageEventTriggerLight} alt="react library" className="block rounded-lg dark:hidden" />
|
||||
<Image src={ImageEventTriggerDark} alt="react library" className="hidden rounded-lg dark:block" />
|
||||
<>
|
||||
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
|
||||
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 dark:text-slate-200">
|
||||
Ask at the right moment,
|
||||
<br />
|
||||
<span className="font-light">get the data you need.</span>
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Follow up emails are so 2010. Ask users as they experience your product - and leverage a
|
||||
significantly higher conversion rate.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-100 py-6 pr-4 sm:py-16 sm:pr-8 dark:bg-slate-800">
|
||||
<Image
|
||||
src={ImageEventTriggerLight}
|
||||
alt="react library"
|
||||
className="block rounded-lg dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src={ImageEventTriggerDark}
|
||||
alt="react library"
|
||||
className="hidden rounded-lg dark:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="order-last rounded-lg bg-slate-100 p-4 sm:p-8 md:order-first dark:bg-slate-800">
|
||||
<Image src={ImageAttributesLight} alt="react library" className="block rounded-lg dark:hidden" />
|
||||
<Image src={ImageAttributesDark} alt="react library" className="hidden rounded-lg dark:block" />
|
||||
</div>
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
|
||||
Don't ‘Spray and pray’.
|
||||
<br />
|
||||
<span className="font-light">Pre-segment granularly.</span>
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-md leading-7 text-slate-500 dark:text-slate-400">
|
||||
Pre-segment who sees your survey based on custom attributes. Keep the signal, cancel out the
|
||||
noise.
|
||||
</p>
|
||||
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
|
||||
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="order-last rounded-lg bg-slate-100 p-4 sm:p-8 md:order-first dark:bg-slate-800">
|
||||
<Image
|
||||
src={ImageAttributesLight}
|
||||
alt="react library"
|
||||
className="block rounded-lg dark:hidden"
|
||||
/>
|
||||
<Image src={ImageAttributesDark} alt="react library" className="hidden rounded-lg dark:block" />
|
||||
</div>
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
|
||||
Don't ‘Spray and pray’.
|
||||
<br />
|
||||
<span className="font-light">Pre-segment granularly.</span>
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-md leading-7 text-slate-500 dark:text-slate-400">
|
||||
Pre-segment who sees your survey based on custom attributes. Keep the signal, cancel out the
|
||||
noise.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowUpIcon } from "@heroicons/react/24/solid";
|
||||
import throttle from "lodash/throttle";
|
||||
import { ArrowUpIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
@@ -1,123 +1,149 @@
|
||||
import DemoPreview from "@/components/dummyUI/DemoPreview";
|
||||
import DashboardMockupDark from "@/images/dashboard-mockup-dark.png";
|
||||
import DashboardMockup from "@/images/dashboard-mockup.png";
|
||||
import { MousePointerClickIcon } from "lucide-react";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
import AddEventDummy from "../dummyUI/AddEventDummy";
|
||||
import AddNoCodeEventModalDummy from "../dummyUI/AddNoCodeEventModalDummy";
|
||||
import HeadingCentered from "../shared/HeadingCentered";
|
||||
import SetupTabs from "./SetupTabs";
|
||||
|
||||
export const Steps: React.FC = () => {
|
||||
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<HeadingCentered
|
||||
closer
|
||||
teaser="Leave your engineers in peace"
|
||||
heading="Set Formbricks up in minutes"
|
||||
subheading="Formbricks is designed for as little dev attention as possible. Here’s how:"
|
||||
/>
|
||||
<div className="space-y-16">
|
||||
<div className="xs:grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 sm:pl-10 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 1</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200">
|
||||
Copy + Paste
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Simply copy a <script> tag to your HTML head - that’s about it. Or use NPM to install
|
||||
Formbricks for React, Vue, Svelte, etc.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-100 dark:bg-slate-800">
|
||||
<SetupTabs />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
|
||||
<div className="flex h-40 items-center justify-center">
|
||||
<Button variant="primary">
|
||||
<MousePointerClickIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Add Action
|
||||
</Button>
|
||||
<div id="howitworks" className="xs:m-auto mb-12 mt-16 max-w-lg md:mb-0 md:mt-8 md:max-w-none">
|
||||
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="xs:grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 sm:pl-10 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 1</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200">
|
||||
Copy + Paste
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Simply copy a <script> tag to your HTML head - that’s about it. Or use NPM to install
|
||||
Formbricks for React, Vue, Svelte, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 2</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
|
||||
No-Code: Track User Actions
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Set up user actions which can trigger your survey without writing a single line of code. Surveys
|
||||
can be triggered on specific pages or after an element is clicked.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 sm:pl-10 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 3</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
|
||||
Create your survey
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Start from a template - or from scratch. Ask what you want, in any language. You can also adjust
|
||||
the look and feel of your survey.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-full rounded-lg p-1 sm:p-8 dark:bg-slate-800">
|
||||
<DemoPreview template="Product Market Fit Survey (short)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
|
||||
<div className="mx-auto flex flex-col items-center justify-center md:w-3/4">
|
||||
<AddEventDummy />
|
||||
<div className="rounded-lg bg-slate-100 dark:bg-slate-800">
|
||||
<SetupTabs />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 4</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
|
||||
Set segment and trigger
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Create a custom segment for each survey. Use attributes and past user actions to only survey the
|
||||
people who have answers. Trigger your survey on any user action in your app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 sm:pl-10 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 5</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
|
||||
Make better decisions
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Gather all insights you can - including partial submissions. Build conviction for the next
|
||||
product decision. Better data, better business.
|
||||
</p>
|
||||
</div>
|
||||
<div className="sm:scale-125 sm:p-8">
|
||||
<Image
|
||||
src={DashboardMockup}
|
||||
quality="100"
|
||||
alt="Data Pipelines"
|
||||
className="block rounded-lg dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src={DashboardMockupDark}
|
||||
quality="100"
|
||||
alt="Data Pipelines"
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
|
||||
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
|
||||
<div className="flex h-40 items-center justify-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
className=""
|
||||
onClick={() => {
|
||||
setAddEventModalOpen(true);
|
||||
}}>
|
||||
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Add Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 2</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
|
||||
No-Code: Track User Actions
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Set up user actions which can trigger your survey without writing a single line of code.
|
||||
Surveys can be triggered on specific pages or after an element is clicked.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
|
||||
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 sm:pl-10 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 3</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
|
||||
Create your survey
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Start from a template - or from scratch. Ask what you want, in any language. You can also
|
||||
adjust the look and feel of your survey.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-full rounded-lg p-1 sm:p-8 dark:bg-slate-800">
|
||||
<DemoPreview template="Product Market Fit Survey (short)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
|
||||
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
|
||||
<div className="mx-auto flex flex-col items-center justify-center md:w-3/4">
|
||||
<AddEventDummy />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-8 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 4</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
|
||||
Set segment and trigger
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Create a custom segment for each survey. Use attributes and past user actions to only survey
|
||||
the people who have answers. Trigger your survey on any user action in your app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
|
||||
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="pb-8 sm:pl-10 md:pb-0">
|
||||
<h4 className="text-brand-dark font-bold">Step 5</h4>
|
||||
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
|
||||
Make better decisions
|
||||
</h2>
|
||||
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
|
||||
Gather all insights you can - including partial submissions. Build conviction for the next
|
||||
product decision. Better data, better business.
|
||||
</p>
|
||||
</div>
|
||||
<div className="sm:scale-125 sm:p-8">
|
||||
<Image
|
||||
src={DashboardMockup}
|
||||
quality="100"
|
||||
alt="Data Pipelines"
|
||||
className="block rounded-lg dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src={DashboardMockupDark}
|
||||
quality="100"
|
||||
alt="Data Pipelines"
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddNoCodeEventModalDummy open={isAddEventModalOpen} setOpen={setAddEventModalOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
interface TestimonialProps {
|
||||
title: string;
|
||||
text: string;
|
||||
Icon: React.ElementType;
|
||||
}
|
||||
|
||||
export default function SalesTestimonial({ title, text, Icon }: TestimonialProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-4 rounded-xl border border-slate-200 bg-gradient-to-tr from-slate-100 to-slate-100 p-4 transition-colors delay-1000 duration-1000 ease-in-out hover:to-slate-50">
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-8">
|
||||
<Icon className="h-12 w-12 text-slate-500" strokeWidth={1} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-pretty text-lg font-medium text-slate-800">{title}</h3>
|
||||
<p className="text-slate-500">{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { usePlausible } from "next-plausible";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
import { FooterLogo } from "../shared/Logo";
|
||||
|
||||
const mainNav = [
|
||||
{ name: "Link Surveys", href: "/open-source-form-builder", status: true },
|
||||
{ name: "Website Surveys", href: "/website-survey", status: true },
|
||||
{ name: "In-app Surveys", href: "/in-app-survey", status: true },
|
||||
];
|
||||
|
||||
export default function HeaderLight() {
|
||||
const plausible = usePlausible();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<header className="max-w-8xl mx-auto flex items-center justify-between px-6 py-6 lg:px-10 xl:px-12">
|
||||
<Link href="/">
|
||||
<span className="sr-only">Formbricks</span>
|
||||
<FooterLogo className="h-8 w-auto sm:h-10" />
|
||||
</Link>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
{mainNav.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="px-8 text-sm font-medium text-slate-400 hover:text-slate-700 lg:text-base dark:hover:text-slate-300">
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="highlight"
|
||||
className="hidden md:px-6 lg:block"
|
||||
onClick={() => {
|
||||
router.push("https://app.formbricks.com/auth/signup");
|
||||
plausible("Demo_CTA_TryForFree");
|
||||
}}>
|
||||
Get started - it's free!
|
||||
</Button>
|
||||
|
||||
<Popover className="block lg:hidden">
|
||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-slate-100 p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 lg:hidden dark:bg-slate-700 dark:text-slate-200">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<Menu className="h-6 w-6" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="duration-200 ease-out"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95">
|
||||
<Popover.Panel
|
||||
focus
|
||||
className="absolute inset-x-0 top-0 z-20 origin-top-right transform p-2 transition md:hidden">
|
||||
<div className="dark:divide-slate divide-y-2 divide-slate-100 rounded-lg bg-slate-200 shadow-lg ring-1 ring-black ring-opacity-5 dark:divide-slate-700 dark:bg-slate-800">
|
||||
<div className="px-5 pb-6 pt-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<FooterLogo className="h-8 w-auto" />
|
||||
</div>
|
||||
<div className="-mr-2">
|
||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
|
||||
<span className="sr-only">Close menu</span>
|
||||
<X className="h-6 w-6" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-6">
|
||||
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
|
||||
<div className="space-y-4">
|
||||
{mainNav.map((item) => (
|
||||
<Link key={item.name} href={item.href} className="block text-lg text-slate-700">
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => router.push("https://app.formbricks.com/auth/signup")}
|
||||
className="flex w-full justify-center text-lg">
|
||||
Get started, it's free!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Footer from "../shared/Footer";
|
||||
import MetaInformation from "../shared/MetaInformation";
|
||||
import HeaderLight from "./HeaderLight";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
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 space-y-24 px-6 lg:space-y-40 lg:px-24 xl:px-36 ">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import CalLogoLight from "@/images/clients/cal-logo-light.svg";
|
||||
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
|
||||
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
|
||||
import NILogoDark from "@/images/clients/niLogoDark.svg";
|
||||
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
|
||||
import ThemeisleLogo from "@/images/clients/themeisle-logo.webp";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function LogoBar() {
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<p className="text-center text-lg text-slate-700">
|
||||
10,000+ teams at the world’s best companies trust Formbricks
|
||||
</p>
|
||||
<div className="mt-5 items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
|
||||
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-2 md:gap-10 lg:grid-cols-6">
|
||||
<Image
|
||||
src={FlixbusLogo}
|
||||
alt="Flixbus Flix Flixtrain Logo"
|
||||
className="rounded-lg pb-1 "
|
||||
width={200}
|
||||
/>
|
||||
<Image src={CalLogoLight} alt="Cal Logo" className="block rounded-lg dark:hidden" width={170} />
|
||||
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" className="pb-1" width={200} />
|
||||
<Image
|
||||
src={CrowdLogoLight}
|
||||
alt="Crowd.dev Logo"
|
||||
className="block rounded-lg pb-1 dark:hidden"
|
||||
width={200}
|
||||
/>
|
||||
<Image src={OptimoleLogo} alt="Optimole Logo" className="pb-1" width={200} />
|
||||
<Image src={NILogoDark} alt="Neverinstall Logo" className="block pb-1 dark:hidden" width={200} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import SalesCTA from "@/components/salespage/SalesCTA";
|
||||
|
||||
interface Props {
|
||||
headline: string;
|
||||
subheadline: string;
|
||||
}
|
||||
|
||||
export default function SalesBreaker({ headline, subheadline }: Props) {
|
||||
return (
|
||||
<div className="xs:mx-auto xs:w-full mx-4 my-4 mt-28 max-w-6xl rounded-xl bg-gradient-to-br from-slate-200 to-slate-300 md:mb-0 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700">
|
||||
<div className="relative px-4 py-8 sm:px-6 sm:pb-12 sm:pt-8 lg:px-8 lg:pt-12">
|
||||
<div className="xs:block xs:absolute xs:right-10 hidden md:top-1/2 md:-translate-y-1/2">
|
||||
<SalesCTA />
|
||||
</div>
|
||||
<h2 className="mt-4 text-2xl font-bold tracking-tight text-slate-800 lg:text-3xl">{headline}</h2>
|
||||
<h4 className="text-md mt-4 max-w-3xl text-slate-500 lg:text-lg dark:text-slate-300">
|
||||
{subheadline}
|
||||
</h4>
|
||||
<div className="xs:hidden mt-4">
|
||||
<SalesCTA />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { usePlausible } from "next-plausible";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
export default function SalesCTA() {
|
||||
const plausible = usePlausible();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="w-fit"
|
||||
onClick={() => {
|
||||
router.push("https://app.formbricks.com/auth/signup");
|
||||
plausible("SalesPage_CTA_GetStartedNow");
|
||||
}}>
|
||||
Get started now
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import SalesCTA from "@/components/salespage/SalesCTA";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
|
||||
interface SalesPageFeatureProps {
|
||||
imgSrc: StaticImageData;
|
||||
imgAlt: string;
|
||||
headline: string;
|
||||
subheadline: string;
|
||||
imgLeft?: boolean;
|
||||
}
|
||||
|
||||
export default function SalesPageFeature({
|
||||
imgSrc,
|
||||
imgAlt,
|
||||
headline,
|
||||
subheadline,
|
||||
imgLeft,
|
||||
}: SalesPageFeatureProps) {
|
||||
return (
|
||||
<div className="group grid content-center gap-12 lg:grid-cols-2">
|
||||
<div
|
||||
className={`order-last flex flex-col justify-center space-y-6 lg:order-none ${imgLeft && `!order-last`}`}>
|
||||
<h2 className="text-balance text-3xl font-bold text-slate-800">{headline}</h2>
|
||||
<p className="text-pretty text-lg text-slate-700">{subheadline}</p>
|
||||
<SalesCTA />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt={imgAlt}
|
||||
className="rounded-3xl border border-slate-200 bg-white transition delay-75 duration-[1500ms] group-hover:scale-[105%] group-hover:border-slate-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import SalesCTA from "@/components/salespage/SalesCTA";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
|
||||
interface SalesPageHeroProps {
|
||||
imgSrc: StaticImageData;
|
||||
imgAlt: string;
|
||||
headline: React.ReactNode;
|
||||
subheadline: string;
|
||||
}
|
||||
|
||||
export default function SalesPageHero({ imgSrc, imgAlt, headline, subheadline }: SalesPageHeroProps) {
|
||||
return (
|
||||
<div className="group grid content-center gap-12 pt-20 lg:grid-cols-2">
|
||||
<div className="my-auto space-y-6">
|
||||
<h1 className="text-5xl font-bold text-slate-800">{headline}</h1>
|
||||
<p className="text-balance text-lg text-slate-700">{subheadline}</p>
|
||||
<SalesCTA />
|
||||
</div>
|
||||
<div className="relative hidden lg:block">
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt={imgAlt}
|
||||
className="scale-110 rounded-3xl border border-slate-200 bg-white transition-all delay-75 duration-[1500ms] group-hover:scale-[115%] group-hover:border-slate-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
interface SalesStepsProps {
|
||||
steps: Array<{ id: string; name: string; description: string }>;
|
||||
}
|
||||
|
||||
export default function SalesSteps({ steps }: SalesStepsProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
|
||||
{steps.map((step) => {
|
||||
return (
|
||||
<li
|
||||
key={step.id}
|
||||
className="relative col-span-1 flex flex-col rounded-xl border border-slate-200 bg-slate-100 text-center ">
|
||||
<div className="absolute -mt-12 w-full">
|
||||
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-slate-200 text-5xl font-bold text-slate-700 shadow ">
|
||||
{step.id}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col p-10">
|
||||
<h3 className="my-4 text-lg font-medium text-slate-800 ">{step.name}</h3>
|
||||
<dl className="mt-1 flex flex-grow flex-col justify-between">
|
||||
<dt className="sr-only">Description</dt>
|
||||
<dd className="text-slate-600 ">{step.description}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
|
||||
interface TestimonialProps {
|
||||
quote: string;
|
||||
author: string;
|
||||
imgSrc: StaticImageData;
|
||||
imgAlt: string;
|
||||
textSize: "base" | "large";
|
||||
}
|
||||
|
||||
export default function SalesTestimonial({
|
||||
quote,
|
||||
author,
|
||||
imgAlt,
|
||||
imgSrc,
|
||||
textSize = "base",
|
||||
}: TestimonialProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4 rounded-xl border border-slate-200 bg-slate-100 p-8 text-center">
|
||||
<h3
|
||||
className={`text-balance font-medium text-slate-700 ${textSize === "base" ? "text-xl" : "text-xl lg:text-2xl"} `}>
|
||||
{quote}
|
||||
</h3>
|
||||
<p className="text-lg text-slate-500">{author}</p>
|
||||
<Image src={imgSrc} alt={imgAlt} width={100} height={100} className="rounded-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface APICallProps {
|
||||
|
||||
@@ -79,6 +79,15 @@ export default function BestPracticeNavigation() {
|
||||
description: "Give users the chance to share feedback in a single click.",
|
||||
category: "Boost Retention",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Improve Newsletter Cotent",
|
||||
href: "/improve-newsletter-content",
|
||||
status: true,
|
||||
icon: FeedbackIcon,
|
||||
description: "Improve your newsletter content by showing this survey to your readers.",
|
||||
category: "Boost Retention",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import HeadingCentered from "@/components/shared/HeadingCentered";
|
||||
|
||||
import BestPracticeNavigation from "./BestPracticeNavigation";
|
||||
|
||||
export default function InsightOppos() {
|
||||
return (
|
||||
<div id="best-practices">
|
||||
<HeadingCentered
|
||||
heading="Get started with Best Practices"
|
||||
subheading="Run battle-tested approaches for qualitative user research in minutes."
|
||||
/>
|
||||
<div className="pb-10 pt-12 md:pt-20">
|
||||
<div className="px-4 py-20 text-center sm:px-6 lg:px-8" id="best-practices">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-800 sm:text-4xl md:text-5xl dark:text-slate-200">
|
||||
Get started with{" "}
|
||||
<span className="from-brand-light to-brand-dark bg-gradient-to-b bg-clip-text text-transparent xl:inline">
|
||||
Best Practices
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mx-auto mt-3 max-w-md text-base text-slate-500 sm:text-lg md:mt-5 md:max-w-3xl md:text-xl dark:text-slate-300">
|
||||
Run battle-tested approaches for qualitative user research in minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BestPracticeNavigation />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function BreakerCTA({ inverted = false, teaser, headline, subhead
|
||||
inverted
|
||||
? "from-slate-800 via-slate-800 to-slate-700 dark:from-slate-200 dark:to-slate-300"
|
||||
: "from-slate-200 to-slate-300 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700",
|
||||
"mx-auto w-full max-w-6xl rounded-xl bg-gradient-to-br "
|
||||
"xs:mx-auto xs:w-full mx-4 my-4 mt-28 max-w-6xl rounded-xl bg-gradient-to-br md:mb-0 "
|
||||
)}>
|
||||
<div className="relative px-4 py-8 sm:px-6 sm:pb-12 sm:pt-8 lg:px-8 lg:pt-12">
|
||||
<div className="xs:block xs:absolute xs:right-10 hidden md:top-1/2 md:-translate-y-1/2">
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function CTA() {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto px-4 py-16 sm:px-6 lg:px-8 lg:pb-40 lg:pt-24">
|
||||
<HeadingCentered teaser="Get started" heading="Ready for the last form tool you need?" />
|
||||
<HeadingCentered closer teaser="Get started" heading="Ready for the last form tool you need?" />
|
||||
|
||||
<div className="mt-12 grid grid-cols-1 content-center md:grid-cols-2">
|
||||
<div className="-mb-4 rounded-t-xl bg-gradient-to-br from-slate-300 to-slate-200 px-8 py-24 text-center text-slate-900 md:-mr-5 md:mb-0 md:ml-2.5 md:rounded-l-xl lg:p-24 dark:from-slate-800 dark:to-slate-900 dark:text-slate-100">
|
||||
|
||||
@@ -13,7 +13,7 @@ const styles: Record<string, CSSProperties> = {
|
||||
marginTop: "1rem",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "normal",
|
||||
fontWeight: "lighter",
|
||||
color: "#e5e7eb",
|
||||
},
|
||||
pre: {
|
||||
@@ -21,7 +21,7 @@ const styles: Record<string, CSSProperties> = {
|
||||
},
|
||||
code: {
|
||||
textShadow: "none",
|
||||
color: "#333333",
|
||||
color: "#fbbf24",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,39 +4,13 @@ import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6";
|
||||
import { FooterLogo } from "./Logo";
|
||||
|
||||
const navigation = {
|
||||
products: [
|
||||
{ name: "Link Surveys", href: "/open-source-form-builder", status: true },
|
||||
{ name: "Website Surveys", href: "/website-survey", status: true },
|
||||
{ name: "In-app Surveys", href: "/in-app-survey", status: true },
|
||||
],
|
||||
comparisons: [
|
||||
{ name: "vs. Google Forms", href: "/vs-google-forms", status: true },
|
||||
{ name: "vs. Formspree", href: "/vs-formspree", status: true },
|
||||
{ name: "vs. OhMyForm", href: "/vs-ohmyform", status: true },
|
||||
],
|
||||
footernav: [
|
||||
other: [
|
||||
{ name: "Community", href: "/community", status: true },
|
||||
{ name: "Pricing", href: "/pricing", status: true },
|
||||
{ name: "Blog", href: "/blog", status: true },
|
||||
{ name: "Docs", href: "/blog", status: true },
|
||||
],
|
||||
legal: [
|
||||
{ name: "Imprint", href: "/imprint", status: true },
|
||||
{ name: "Privacy Policy", href: "/privacy", status: true },
|
||||
{ name: "Terms", href: "/terms", status: true },
|
||||
{ name: "OSS Friends", href: "/oss-friends", status: true },
|
||||
{ name: "GDPR FAQ", href: "/gdpr", status: true },
|
||||
{ name: "GDPR Guide", href: "/gdpr-guide", status: true },
|
||||
],
|
||||
bestPractices: [
|
||||
{ name: "Interview Prompt", href: "/interview-prompt", status: true },
|
||||
{ name: "PMF Survey", href: "/measure-product-market-fit", status: true },
|
||||
{ name: "Onboarding Segments", href: "/onboarding-segmentation", status: true },
|
||||
{ name: "Learn from Churn", href: "/learn-from-churn", status: true },
|
||||
{ name: "Improve Trial CR", href: "/improve-trial-conversion", status: true },
|
||||
{ name: "Docs Feedback", href: "/docs-feedback", status: true },
|
||||
{ name: "Feature Chaser", href: "/feature-chaser", status: true },
|
||||
{ name: "Feedback Box", href: "/feedback-box", status: true },
|
||||
],
|
||||
social: [
|
||||
{
|
||||
name: "Twitter",
|
||||
@@ -65,84 +39,27 @@ export default function Footer() {
|
||||
<h2 id="footer-heading" className="sr-only">
|
||||
Footer
|
||||
</h2>
|
||||
<div className="mx-auto grid max-w-7xl content-center gap-12 px-4 py-12 md:grid-cols-2 lg:grid-cols-3 lg:py-16">
|
||||
<div className="space-y-6">
|
||||
<Link href="/">
|
||||
<span className="sr-only">Formbricks</span>
|
||||
<FooterLogo className="h-8 w-auto sm:h-10" />
|
||||
</Link>
|
||||
<p className="text-base text-slate-500 dark:text-slate-400">Privacy-first Experience Management</p>
|
||||
<div className="border-slate-500">
|
||||
<p className="text-sm text-slate-400 dark:text-slate-500">
|
||||
Formbricks GmbH © {currentYear}. All rights reserved.
|
||||
<br />
|
||||
<Link href="/imprint">Imprint</Link> | <Link href="/privacy">Privacy Policy</Link> |{" "}
|
||||
<Link href="/terms">Terms</Link> | <Link href="/oss-friends">OSS Friends</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-6">
|
||||
{navigation.social.map((item) => (
|
||||
<Link key={item.name} href={item.href} className="text-slate-400 hover:text-slate-500">
|
||||
<span className="sr-only">{item.name}</span>
|
||||
<item.icon className="h-6 w-6" aria-hidden="true" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-7xl flex-col space-y-6 px-4 py-12 text-center sm:px-6 lg:px-8 lg:py-16">
|
||||
<Link href="/">
|
||||
<span className="sr-only">Formbricks</span>
|
||||
<FooterLogo className="mx-auto h-8 w-auto sm:h-10" />
|
||||
</Link>
|
||||
<p className="text-base text-slate-500 dark:text-slate-400">Privacy-first Experience Management</p>
|
||||
<div className="border-slate-500">
|
||||
<p className="text-sm text-slate-400 dark:text-slate-500">
|
||||
Formbricks GmbH © {currentYear}. All rights reserved.
|
||||
<br />
|
||||
<Link href="/imprint">Imprint</Link> | <Link href="/privacy">Privacy Policy</Link> |{" "}
|
||||
<Link href="/terms">Terms</Link> | <Link href="/oss-friends">OSS Friends</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-8 lg:col-span-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-slate-700">Formbricks</h4>
|
||||
{navigation.footernav.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="my-1 block text-slate-500 hover:text-slate-600">
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-slate-700">Product</h4>
|
||||
{navigation.products.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="my-1 block text-slate-500 hover:text-slate-600">
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<h4 className="mb-2 mt-5 font-medium text-slate-700">Comparison</h4>
|
||||
{navigation.comparisons.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="my-1 block text-slate-500 hover:text-slate-600">
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-slate-700">Best Practices</h4>
|
||||
{navigation.bestPractices.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="my-1 block text-slate-500 hover:text-slate-600">
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-slate-700">Legal</h4>
|
||||
{navigation.legal.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="my-1 block text-slate-500 hover:text-slate-600">
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center space-x-6">
|
||||
{navigation.social.map((item) => (
|
||||
<Link key={item.name} href={item.href} className="text-slate-400 hover:text-slate-500">
|
||||
<span className="sr-only">{item.name}</span>
|
||||
<item.icon className="h-6 w-6" aria-hidden="true" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import GitHubMarkWhite from "@/images/github-mark-white.svg";
|
||||
import GitHubMarkDark from "@/images/github-mark.svg";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Bars3Icon, ChevronDownIcon, ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { ChevronDownIcon, ChevronRightIcon, MenuIcon, XIcon } from "lucide-react";
|
||||
import { usePlausible } from "next-plausible";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -136,7 +136,7 @@ export default function Header() {
|
||||
<div className="-my-2 -mr-2 md:hidden">
|
||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-slate-100 p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MenuIcon className="h-6 w-6" aria-hidden="true" />
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
<Popover.Group as="nav" className="hidden space-x-6 md:flex lg:space-x-10">
|
||||
@@ -268,6 +268,12 @@ export default function Header() {
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
{/* <Link
|
||||
href="/community"
|
||||
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
|
||||
Community
|
||||
</Link>
|
||||
*/}
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="text-sm font-medium text-slate-400 hover:text-slate-700 lg:text-base dark:hover:text-slate-300">
|
||||
@@ -288,6 +294,11 @@ export default function Header() {
|
||||
className="text-sm font-medium text-slate-400 hover:text-slate-700 lg:text-base dark:hover:text-slate-300">
|
||||
Blog {/* <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p> */}
|
||||
</Link>
|
||||
{/* <Link
|
||||
href="/careers"
|
||||
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
|
||||
Careers <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p>
|
||||
</Link> */}
|
||||
</Popover.Group>
|
||||
<div className="hidden flex-1 items-center justify-end md:flex">
|
||||
<Button
|
||||
@@ -308,6 +319,11 @@ export default function Header() {
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
</Button>
|
||||
{/* <Button variant="secondary" className="ml-2 px-2" onClick={() => setVideoModal(true)}>
|
||||
<VideoWalkThrough open={videoModal} setOpen={() => setVideoModal(false)} />
|
||||
<PlayCircleIcon className="h-6 w-6" />
|
||||
</Button> */}
|
||||
|
||||
<Button
|
||||
variant="highlight"
|
||||
className="ml-2 text-xs lg:text-sm"
|
||||
@@ -340,7 +356,7 @@ export default function Header() {
|
||||
<div className="-mr-2">
|
||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
|
||||
<span className="sr-only">Close menu</span>
|
||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
interface Props {
|
||||
teaser?: string;
|
||||
heading: React.ReactNode;
|
||||
heading: string;
|
||||
subheading?: string;
|
||||
closer?: boolean;
|
||||
}
|
||||
|
||||
export default function HeadingCentered({ teaser, heading, subheading }: Props) {
|
||||
export default function HeadingCentered({ teaser, heading, subheading, closer }: Props) {
|
||||
return (
|
||||
<div className="mb-12 text-center">
|
||||
<div className={clsx(closer ? "pt-16 lg:pt-24" : "pt-24 lg:pt-40", "px-2 pb-4 text-center md:pb-12")}>
|
||||
<p className="text-md text-brand-dark dark:text-brand-light mx-auto mb-3 max-w-2xl font-semibold uppercase sm:mt-4">
|
||||
{teaser}
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import HeaderLight from "../salespage/HeaderLight";
|
||||
import Footer from "./Footer";
|
||||
import Header from "./Header";
|
||||
import MetaInformation from "./MetaInformation";
|
||||
|
||||
interface LayoutProps {
|
||||
@@ -10,11 +10,11 @@ interface LayoutProps {
|
||||
|
||||
export default function Layout({ title, description, children }: LayoutProps) {
|
||||
return (
|
||||
<div className="mx-auto w-full">
|
||||
<div className="flex h-screen flex-col justify-between">
|
||||
<MetaInformation title={title} description={description} />
|
||||
<HeaderLight />
|
||||
<Header />
|
||||
{
|
||||
<main className="max-w-8xl relative mx-auto flex w-full flex-col justify-center space-y-32 px-6 py-24 lg:px-24 xl:px-36 ">
|
||||
<main className="max-w-8xl relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
|
||||
{children}
|
||||
</main>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import HeaderLight from "@/components/salespage/HeaderLight";
|
||||
import SlideInBanner from "@/components/shared/SlideInBanner";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import Footer from "./Footer";
|
||||
import Header from "./Header";
|
||||
import MetaInformation from "./MetaInformation";
|
||||
import { Prose } from "./Prose";
|
||||
|
||||
@@ -39,7 +39,7 @@ interface Props {
|
||||
export default function LayoutMdx({ meta, children }: Props) {
|
||||
useExternalLinks(".prose a");
|
||||
return (
|
||||
<div className="mx-auto w-full">
|
||||
<div className="flex h-screen flex-col justify-between">
|
||||
<MetaInformation
|
||||
title={meta.title}
|
||||
description={meta.description}
|
||||
@@ -48,7 +48,7 @@ export default function LayoutMdx({ meta, children }: Props) {
|
||||
section={meta.section}
|
||||
tags={meta.tags}
|
||||
/>
|
||||
<HeaderLight />
|
||||
<Header />
|
||||
<main className="min-w-0 max-w-2xl flex-auto px-4 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
|
||||
<article className="mx-auto my-16 max-w-3xl px-2">
|
||||
{meta.title && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -28,7 +28,7 @@ export default function HeadingCentered() {
|
||||
<div className="flex h-20 w-full items-center justify-between rounded-lg bg-slate-300 px-8 text-slate-700 dark:bg-slate-800 dark:text-slate-200 ">
|
||||
<p>npm install @formbricks/react</p>
|
||||
<button onClick={() => navigator.clipboard.writeText("npm install @formbricks/react")}>
|
||||
<CopyIcon className="h-8 w-8" />
|
||||
<DocumentDuplicateIcon className="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,8 @@ export default function MetaInformation({
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const pageTitle = `${title}`;
|
||||
const BASE_URL = `formbricks.com`;
|
||||
const canonicalLink = `https://${BASE_URL}${router.asPath}`;
|
||||
const BASE_URL = `https://${process.env.VERCEL_URL}`;
|
||||
const canonicalLink = `${BASE_URL}${router.asPath}`;
|
||||
return (
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
|
||||
|
||||
@@ -54,9 +54,13 @@ export const PricingTable = ({ leadRow, pricing, endRow }) => {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : feature.free ? (
|
||||
<CheckIcon className=" rounded-full border border-green-300 bg-green-100 p-0.5 text-green-500 dark:border-green-600 dark:bg-green-900 dark:text-green-300" />
|
||||
<div className="h-6 w-6 rounded-full border border-green-300 bg-green-100 p-0.5 dark:border-green-600 dark:bg-green-900">
|
||||
<CheckIcon className=" text-green-500 dark:text-green-300" />
|
||||
</div>
|
||||
) : (
|
||||
<XIcon className="rounded-full border border-red-300 bg-red-100 p-0.5 text-red-500 dark:border-red-500 dark:bg-red-300 dark:text-red-600" />
|
||||
<div className="h-6 w-6 rounded-full border border-red-300 bg-red-100 p-0.5 dark:border-red-500 dark:bg-red-300">
|
||||
<XMarkIcon className="text-red-500 dark:text-red-600" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-1/3 items-center justify-center text-center text-sm text-slate-800 dark:text-slate-100">
|
||||
@@ -74,9 +78,13 @@ export const PricingTable = ({ leadRow, pricing, endRow }) => {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : feature.paid ? (
|
||||
<CheckIcon className=" rounded-full border border-green-300 bg-green-100 p-0.5 text-green-500 dark:border-green-600 dark:bg-green-900 dark:text-green-300" />
|
||||
<div className="h-6 w-6 rounded-full border border-green-300 bg-green-100 p-0.5 dark:border-green-600 dark:bg-green-900">
|
||||
<CheckIcon className="text-green-500 dark:text-green-300" />
|
||||
</div>
|
||||
) : (
|
||||
<XIcon className="rounded-full border border-red-300 bg-red-100 p-0.5 text-red-500 dark:border-red-500 dark:bg-red-300 dark:text-red-600" />
|
||||
<div className="h-6 w-6 rounded-full border border-red-300 bg-red-100 p-0.5 dark:border-red-600 dark:bg-red-900">
|
||||
<XMarkIcon className="text-red-500 dark:text-red-600" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import LFGLuigi from "@/images/blog/lfg-luigi-200px.webp";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import Image from "next/image";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
@@ -70,7 +70,7 @@ const SlideInBanner: React.FC<Props> = ({ delay = 5000, scrollPercentage = 10, U
|
||||
setTimeout(() => setIsDismissed(true), 500);
|
||||
}}
|
||||
className="rounded-full p-2 hover:bg-slate-600 hover:bg-opacity-30">
|
||||
<XIcon className="h-6 w-6" />
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -30,7 +30,7 @@ export default function HeadingCentered() {
|
||||
<div className="flex h-20 w-full items-center justify-between rounded-lg bg-slate-800 px-8 text-slate-100 ">
|
||||
<p>npm install @formbricks/react</p>
|
||||
<button onClick={() => navigator.clipboard.writeText("npm install @formbricks/react")}>
|
||||
<CopyIcon className="h-8 w-8" />
|
||||
<DocumentDuplicateIcon className="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user