Compare commits

..

14 Commits

Author SHA1 Message Date
Sudhanshu Pandey
b6985f2920 Update ecs-deployment.yml 2024-02-01 14:45:42 -05:00
Sudhanshu Pandey
942fd89f46 Update ecs-deployment.yml 2024-02-01 14:35:53 -05:00
Sudhanshu Pandey
00c34b80b8 Update ecs-deployment.yml 2024-02-01 14:20:10 -05:00
Sudhanshu Pandey
3b8541426c Update ecs-deployment.yml 2024-02-01 14:04:45 -05:00
Sudhanshu Pandey
400ab64cb4 Update ecs-deployment.yml 2024-02-01 14:02:27 -05:00
Sudhanshu Pandey
4ce977310f Update ecs-deployment.yml 2024-02-01 13:58:44 -05:00
Sudhanshu Pandey
ea7a793a29 Update ecs-deployment.yml 2024-02-01 13:47:39 -05:00
Sudhanshu Pandey
63eb80463d Update ecs-deployment.yml 2024-02-01 13:31:38 -05:00
Sudhanshu Pandey
b23ab5c504 Update ecs-deployment.yml 2024-02-01 13:20:02 -05:00
Sudhanshu Pandey
76f8d32045 Update ecs-deployment.yml 2024-02-01 13:05:29 -05:00
Sudhanshu Pandey
922847d79e Update ecs-deployment.yml 2024-02-01 13:00:52 -05:00
Sudhanshu Pandey
dce3203e15 Update ecs-deployment.yml 2024-02-01 12:55:38 -05:00
Sudhanshu Pandey
960355d748 Update ecs-deployment.yml 2024-02-01 12:41:45 -05:00
Sudhanshu Pandey
58d78ff576 Update ecs-deployment.yml 2024-02-01 12:23:29 -05:00
811 changed files with 17820 additions and 38656 deletions

View File

@@ -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"
}

View File

@@ -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/

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -7,11 +7,14 @@ 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
# github.repository as <account>/<repo>
IMAGE_NAME: formbricks/formbricks-experimental
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
@@ -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 }}
@@ -53,6 +68,7 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,format=long
type=raw,value=latest,enable={{is_default_branch}}
# Build and push Docker image with Buildx
# https://github.com/docker/build-push-action
@@ -73,21 +89,23 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
DATABASE_URL=${{ env.DATABASE_URL }}
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}
NEXT_PUBLIC_SENTRY_DSN=${{ env.NEXT_PUBLIC_SENTRY_DSN }}
- name: Sign the images with GitHub OIDC Token
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
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 }}
# # This will only write to the public Rekor transparency log when the Docker
# # repository is public to avoid leaking data. If you would like to publish
# # transparency data even for private images, pass --force to cosign below.
# # https://github.com/sigstore/cosign
# - name: Sign the published Docker image
# env:
# # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
# TAGS: ${{ 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
@@ -110,7 +128,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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

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

View File

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

View File

@@ -3,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
View File

@@ -51,6 +51,3 @@ Zone.Identifier
/playwright-report/
/blob-report/
/playwright/.cache/
# uploads
packages/lib/uploads

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

108
README.md
View File

@@ -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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://flixbus.com"><img src="https://github.com/formbricks/formbricks/assets/72809645/d6c91d89-7633-4845-ae1e-03bbd2ce0946" height="35px"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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
[![Deploy on Railway](https://railway.app/button.svg)](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
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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>

View File

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

View File

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

View File

@@ -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"

View File

@@ -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":{}
}
```

View File

@@ -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>

View File

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

View File

@@ -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`) shouldnt 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`) shouldnt 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

View File

@@ -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)

View File

@@ -46,13 +46,13 @@ All you need to do is copy a `<script>` tag to your HTML head, and thats 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`):

View File

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

View File

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

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

View File

@@ -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

View File

@@ -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!

View File

@@ -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>
**Cant figure it out?**: [Join our Discord!](https://formbricks.com/discord)

View File

@@ -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!

View File

@@ -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)
**Cant figure it out?**: [Join our Discord!](https://formbricks.com/discord)

View File

@@ -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!

View File

@@ -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:

View File

@@ -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 = {

View File

@@ -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>

View File

@@ -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" },
],
},
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
},
},
];

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,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&apos;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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,6 +79,15 @@ export default function BestPracticeNavigation() {
description: "Give users the chance to share feedback in a single click.",
category: "Boost Retention",
},
{
name: "Improve Newsletter 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 (

View File

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

View File

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

View File

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

View File

@@ -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",
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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