Merge branch 'main' of github.com:formbricks/formbricks into shubham/for-1150-validate-survey-editor-forms
@@ -47,7 +47,7 @@ NEXTAUTH_URL=http://localhost:3000
|
||||
# SMTP_HOST=localhost
|
||||
# SMTP_PORT=1025
|
||||
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
|
||||
# SMTP_SECURE_ENABLED=0 # Enable for TLS (port 465)
|
||||
# SMTP_SECURE_ENABLED=0
|
||||
# SMTP_USER=smtpUser
|
||||
# SMTP_PASSWORD=smtpPassword
|
||||
|
||||
@@ -100,4 +100,4 @@ GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Cron Secret
|
||||
CRON_SECRET=
|
||||
CRON_SECRET=
|
||||
|
||||
6
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
"❗️ migrations":
|
||||
- packages/database/migrations/**/migration.sql
|
||||
|
||||
"❗️ .env changes":
|
||||
- .env.example
|
||||
- .env.docker
|
||||
74
.github/workflows/apply-issue-labels-to-pr.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: "Apply issue labels to PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
label_on_pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
async function getLinkedIssues(owner, repo, prNumber) {
|
||||
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
closingIssuesReferences(first: 10) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const variables = {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
prNumber: prNumber,
|
||||
};
|
||||
|
||||
const result = await github.graphql(query, variables);
|
||||
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
||||
}
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
const linkedIssues = await getLinkedIssues(
|
||||
context.repo.owner,
|
||||
context.repo.repo,
|
||||
pr.number
|
||||
);
|
||||
|
||||
const labelsToAdd = new Set();
|
||||
for (const issue of linkedIssues) {
|
||||
if (issue.labels && issue.labels.nodes) {
|
||||
for (const label of issue.labels.nodes) {
|
||||
labelsToAdd.add(label.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (labelsToAdd.size) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: Array.from(labelsToAdd),
|
||||
});
|
||||
}
|
||||
33
.github/workflows/build-production.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Tests
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Build formbricks-js dependencies
|
||||
run: pnpm build --filter=web...
|
||||
18
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
30
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Lint
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
98
.github/workflows/nextjs-bundle-analysis.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: "Next.js Bundle Analysis"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Production build
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: ./.github/workflows/build-production.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Upload bundle
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: bundle
|
||||
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||
|
||||
- name: Analyze bundle
|
||||
run: |
|
||||
cd apps/web
|
||||
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||
|
||||
- name: Upload bundle
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: bundle
|
||||
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||
|
||||
- name: Download base branch bundle stats
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
if: success() && github.event.number
|
||||
with:
|
||||
workflow: nextjs-bundle-analysis.yml
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
path: apps/web/.next/analyze/base
|
||||
|
||||
# And here's the second place - this runs after we have both the current and
|
||||
# base branch bundle stats, and will compare them to determine what changed.
|
||||
# There are two configurable arguments that come from package.json:
|
||||
#
|
||||
# - budget: optional, set a budget (bytes) against which size changes are measured
|
||||
# it's set to 350kb here by default, as informed by the following piece:
|
||||
# https://infrequently.org/2021/03/the-performance-inequality-gap/
|
||||
#
|
||||
# - red-status-percentage: sets the percent size increase where you get a red
|
||||
# status indicator, defaults to 20%
|
||||
#
|
||||
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
|
||||
# entry in your package.json file.
|
||||
- name: Compare with base branch bundle
|
||||
if: success() && github.event.number
|
||||
run: |
|
||||
cd apps/web
|
||||
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||
|
||||
- name: Get comment body
|
||||
id: get-comment-body
|
||||
if: success() && github.event.number
|
||||
run: |
|
||||
cd apps/web
|
||||
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
if: success() && github.event.number
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||
|
||||
- name: Create Comment
|
||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: ${{ steps.get-comment-body.outputs.body }}
|
||||
|
||||
- name: Update Comment
|
||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: ${{ steps.get-comment-body.outputs.body }}
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
45
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: PR Update
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Unit tests
|
||||
uses: ./.github/workflows/test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build-production:
|
||||
name: Production build (without database)
|
||||
uses: ./.github/workflows/build-production.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build-production
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, test, build-production]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release
|
||||
name: Release Changesets
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,10 +7,15 @@ on:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release Formbricks Image on Dockerhub
|
||||
name: Release on Dockerhub
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,8 +7,11 @@ on:
|
||||
|
||||
jobs:
|
||||
release-image-on-dockerhub:
|
||||
name: Release
|
||||
name: Release on Dockerhub
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
47
.github/workflows/semantic-pull-requests.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: "Validate PRs"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
validate-pr:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
# When the previous steps fails, the workflow would stop. By adding this
|
||||
# condition you can continue the execution with the populated error message.
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Hey there and thank you for opening this pull request! 👋🏼
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions for pull request titles! 🙏
|
||||
@@ -1,9 +1,11 @@
|
||||
name: Checks
|
||||
on: [push]
|
||||
name: Tests
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
@@ -24,14 +26,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Build formbricks-js dependencies
|
||||
run: pnpm build --filter=js
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
- name: Build formbricks-js dependencies
|
||||
run: pnpm build --filter=js
|
||||
|
||||
#- name: Test
|
||||
# run: pnpm test
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
27
.github/workflows/welcome-new-contributors.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "Welcome new contributors"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: opened
|
||||
pull_request:
|
||||
types: opened
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
welcome-message:
|
||||
name: Welcoming New Users
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-message: |-
|
||||
Thank you so much for making your first Pull Request and taking the time to improve Formbricks! 🚀🙏❤️
|
||||
Feel free to join the conversation at [Discord](https://formbricks.com/discord)
|
||||
issue-message: |
|
||||
Thank you for opening your first issue! 🙏❤️ One of our team members will review it and get back to you as soon as it possible. 😊
|
||||
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm lint-staged
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/formbricks/formbricks">
|
||||
<img src="https://user-images.githubusercontent.com/675065/203262290-3c2bc5b8-839c-468a-b675-e26a369c7fe2.png" alt="Logo" width="500">
|
||||
<a href="https://formbricks.com">
|
||||
<img width="120" alt="Open Source 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>
|
||||
|
||||
@@ -48,7 +48,7 @@ Formbricks helps you apply best practices from data-driven work and experience m
|
||||
- 👩🏻 Launch and **target your surveys to specific user groups** without changing your application code
|
||||
- 🔗 Create shareable **link surveys**
|
||||
- 👨👩👦 Invite your team members to **collaborate** on your surveys
|
||||
- 🔌 Integrate Formbricks with **Slack, Posthog, Zapier and more**
|
||||
- 🔌 Integrate Formbricks with **Slack, Posthog, Zapier, n8n and more**
|
||||
- 🔒 All **open source**, transparent and self-hostable
|
||||
|
||||
### Built on Open Source
|
||||
|
||||
@@ -32,11 +32,69 @@ The API requests are authorized with a personal API key. This API key gives you
|
||||
/>
|
||||
|
||||
<Note>
|
||||
## Store API key safely {{ class: "text-white" }}
|
||||
Anyone who has your API key has full control over your account. For security reasons, you cannot view the API
|
||||
key again.
|
||||
### Store API key safely
|
||||
Anyone who has your API key has full control over your account.
|
||||
For security reasons, you cannot view the API key again.
|
||||
</Note>
|
||||
|
||||
### Test your API Key
|
||||
|
||||
Hit the below request to verify that you are authenticated with your API Key and the server is responding.
|
||||
|
||||
## Get My Profile {{ tag: 'GET', label: '/api/v1/me' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get the product details and environment type of your account.
|
||||
|
||||
### 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/me">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/me' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"id": "cll2m30r70004mx0huqkitgqv",
|
||||
"createdAt": "2023-08-08T18:04:59.922Z",
|
||||
"updatedAt": "2023-08-08T18:04:59.922Z",
|
||||
"type": "production",
|
||||
"product": {
|
||||
"id": "cll2m30r60003mx0hnemjfckr",
|
||||
"name": "My Product"
|
||||
},
|
||||
"widgetSetupCompleted": false
|
||||
}
|
||||
```
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
Not authenticated
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
### Delete a personal API key
|
||||
|
||||
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/).
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
export const meta = {
|
||||
title: "API Overview",
|
||||
description:
|
||||
"Explore Formbricks' APIs: Public Client API for client-side tasks, and User API for account management with secure API Key authentication.",
|
||||
"Explore Formbricks' APIs: Public Client API for client-side tasks, and Management API for account management with secure API Key authentication.",
|
||||
};
|
||||
|
||||
#### API
|
||||
|
||||
# API Overview
|
||||
|
||||
Formbricks offers two types of APIs: the **Public Client API** and the **User API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
|
||||
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
|
||||
|
||||
## Public Client API
|
||||
|
||||
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
|
||||
|
||||
## User API
|
||||
## Management API
|
||||
|
||||
The User API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the User API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed.
|
||||
The Management API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the Management API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed.
|
||||
|
||||
**Auth:** Personal API Key
|
||||
|
||||
API requests made to the User API are authorized using a personal API key. This key grants the same rights and access as if you were logged in at formbricks.com. It's essential to keep your API key secure and not share it with others.
|
||||
API requests made to the Management API are authorized using a personal API key. This key grants the same rights and access as if you were logged in at formbricks.com. It's essential to keep your API key secure and not share it with others.
|
||||
|
||||
To generate, store, or delete an API key, follow the instructions provided on the following page [API Key](/docs/api/api-key-setup).
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export const meta = {
|
||||
"Explore the Formbricks Public Client API for client-side tasks and integration into your website.",
|
||||
};
|
||||
|
||||
#### Client API
|
||||
#### Management API
|
||||
|
||||
# Responses API
|
||||
|
||||
|
||||
180
apps/formbricks-com/app/docs/api/surveys/page.mdx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Surveys API",
|
||||
description:
|
||||
"Explore the Formbricks Public Client API for client-side tasks and integration into your website.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# Surveys API
|
||||
|
||||
The Survey API currently has one endpoint that allows you to get all the surveys you have in your Formbricks environment. You will need the [API Key](/docs/api/api-key-setup) to access the same!
|
||||
|
||||
---
|
||||
|
||||
## List all surveys {{ tag: 'GET', label: '/api/v1/surveys' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the surveys you have for the environment.
|
||||
|
||||
### 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/surveys">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/surveys' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "cllnfy2780fromy0hy7uoxvtn",
|
||||
"createdAt": "2023-08-23T07:56:20.516Z",
|
||||
"updatedAt": "2023-08-23T07:56:26.947Z",
|
||||
"name": "Product Market Fit (Superhuman)",
|
||||
"type": "link",
|
||||
"environmentId": "cll2m30r70004mx0huqkitgqv",
|
||||
"status": "inProgress",
|
||||
"attributeFilters": [],
|
||||
"displayOption": "displayOnce",
|
||||
"autoClose": null,
|
||||
"triggers": [],
|
||||
"redirectUrl": null,
|
||||
"recontactDays": null,
|
||||
"questions": [
|
||||
{
|
||||
"id": "gml6mgy71efgtq8np3s9je5p",
|
||||
"type": "cta",
|
||||
"headline": "You are one of our power users! Do you have 5 minutes?",
|
||||
"required": false,
|
||||
"buttonLabel": "Happy to help!",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "skipped",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We would love to understand your user experience better. Sharing your insight helps a lot!</span></p>",
|
||||
"buttonExternal": false,
|
||||
"dismissButtonLabel": "No, thanks."
|
||||
},
|
||||
{
|
||||
"id": "kp62fbqe8cfzmvy8qwpr81b2",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "How disappointed would you be if you could no longer use My Product?",
|
||||
"subheader": "Please select one of the following options:",
|
||||
"required": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "bdgy1hnwd7uwmfxk1ljqp1n5",
|
||||
"label": "Not at all disappointed"
|
||||
},
|
||||
{
|
||||
"id": "poabnvgtwenp8rb2v70gj4hj",
|
||||
"label": "Somewhat disappointed"
|
||||
},
|
||||
{
|
||||
"id": "opfiqyqz8wrqn0i0f7t24d3n",
|
||||
"label": "Very disappointed"
|
||||
}
|
||||
],
|
||||
"shuffleOption": "none"
|
||||
},
|
||||
{
|
||||
"id": "klvpwd4x08x8quesihvw5l92",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "What is your role?",
|
||||
"subheader": "Please select one of the following options:",
|
||||
"required": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "c8nerw6l9gpsxcmqkn10f9hy",
|
||||
"label": "Founder"
|
||||
},
|
||||
{
|
||||
"id": "ebjqezei6a2axtuq86cleetn",
|
||||
"label": "Executive"
|
||||
},
|
||||
{
|
||||
"id": "ctiijjblyhlp22snypfamqt1",
|
||||
"label": "Product Manager"
|
||||
},
|
||||
{
|
||||
"id": "ibalyr0mhemfkkr82vypmg40",
|
||||
"label": "Product Owner"
|
||||
},
|
||||
{
|
||||
"id": "fipk606aegslbd0e7yhc0xjx",
|
||||
"label": "Software Engineer"
|
||||
}
|
||||
],
|
||||
"shuffleOption": "none"
|
||||
},
|
||||
{
|
||||
"id": "ryo75306flyg72iaeditbv51",
|
||||
"type": "openText",
|
||||
"headline": "What type of people do you think would most benefit from My Product?",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "lkjaxb73ulydzeumhd51sx9g",
|
||||
"type": "openText",
|
||||
"headline": "What is the main benefit your receive from My Product?",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "ec7agikkr58j8uonhioinkyk",
|
||||
"type": "openText",
|
||||
"headline": "How can we improve My Product for you?",
|
||||
"subheader": "Please be as specific as possible.",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"thankYouCard": {
|
||||
"enabled": true,
|
||||
"headline": "Thank you!",
|
||||
"subheader": "We appreciate your feedback."
|
||||
},
|
||||
"delay": 0,
|
||||
"autoComplete": null,
|
||||
"closeOnDate": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```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>
|
||||
|
||||
---
|
||||
@@ -45,7 +45,7 @@ To run the Churn Survey in your app you want to proceed as follows:
|
||||
4. Prevent that churn!
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages
|
||||
and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
@@ -118,7 +118,7 @@ Whenever a user visits this page, matches the filter conditions above and the re
|
||||
Here is our complete [Actions manual](/docs/actions/why) covering [Code](/docs/actions/code) and [No-Code](/docs/actions/no-code) Actions.
|
||||
|
||||
<Note>
|
||||
## Pre-churn flow coming soon {{ class: "text-white" }}
|
||||
## Pre-churn flow coming soon
|
||||
We’re currently building full-screen survey pop-ups. You’ll be able to prevent users from closing the survey
|
||||
unless they respond to it. It’s certainly debatable if you want that but you could force them to click through
|
||||
the survey before letting them cancel 🤷
|
||||
@@ -156,7 +156,7 @@ These settings make sure the survey is always displayed, when a user wants to Ca
|
||||
/>
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
You need to have the Formbricks Widget installed to display the Churn Survey in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
</Note>
|
||||
|
||||
@@ -72,7 +72,7 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
|
||||
5. In the same way, you can change the Internal Question ID of the _Please elaborate_ question to **“additionalFeedback”** and the one of the _Page URL_ question to **“pageUrl”**.
|
||||
|
||||
<Note>
|
||||
## Answers need to be identical {{ class: "text-white" }}
|
||||
## Answers need to be identical
|
||||
If you want different answers than “Yes 👍” and “No 👎” you need update the choices accordingly. They have to
|
||||
be identical to the frontend we're building in the next step.
|
||||
</Note>
|
||||
@@ -110,7 +110,7 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
|
||||
### 2. Build the frontend
|
||||
|
||||
<Note>
|
||||
## Your frontend might work differently {{ class: "text-white" }}
|
||||
## Your frontend might work differently
|
||||
Your frontend likely looks and works differently. This is an example specific to our tech stack. We want to illustrate
|
||||
what you should consider building yours 😊
|
||||
</Note>
|
||||
|
||||
@@ -42,7 +42,7 @@ To run the Feature Chaser survey in your app you want to proceed as follows:
|
||||
2. Setup a user action to display survey at the right point in time
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages
|
||||
and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
@@ -126,7 +126,7 @@ Lastly, scroll down to “Recontact Options”. Here you have full freedom to de
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
You need to have the Formbricks Widget installed to display the Feature Chaser in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
</Note>
|
||||
|
||||
@@ -74,7 +74,7 @@ Go to the “Audience” tab, find the “When to send” card and choose “Add
|
||||
<Image src={AddAction} alt="Add action" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
|
||||
<Note>
|
||||
## You can also add actions in your code {{ class: "text-white" }}
|
||||
## You can also add actions in your code
|
||||
You can also create [Code Actions](/docs/actions/code) using `formbricks.track("Eventname")` - they will automatically
|
||||
appear in your Actions overview as long as the SDK is embedded.
|
||||
</Note>
|
||||
@@ -84,19 +84,9 @@ We have two options to track the Feedback Button in your application: innerText
|
||||
1. **innerText:** This means that whenever a user clicks any HTML item in your app which has an `innerText` of `Feedback` the Feedback Box will be displayed.
|
||||
2. **CSS-Selector:** This means that when an element with a specific CSS-Selector like `#feedback-button` is clicked, your Feedback Box is triggered.
|
||||
|
||||
<div className="grid grid-cols-2 space-x-2">
|
||||
<Image
|
||||
src={AddHTMLAction}
|
||||
alt="Add HTML action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
<Image
|
||||
src={AddCSSAction}
|
||||
alt="Add CSS action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
<div className="grid grid-cols-2 space-x-2 max-w-full sm:max-w-3xl">
|
||||
<Image src={AddHTMLAction} alt="Add HTML action" quality="100" className="rounded-lg" />
|
||||
<Image src={AddCSSAction} alt="Add CSS action" quality="100" className="rounded-lg" />
|
||||
</div>
|
||||
|
||||
### 4. Select action in the “When to ask” card
|
||||
@@ -131,7 +121,7 @@ Scroll down to “Recontact Options”. Here you have to choose the right settin
|
||||
## Setting up the Widget
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
</Note>
|
||||
|
||||
@@ -42,7 +42,7 @@ To display the Trial Conversion Survey in your app you want to proceed as follow
|
||||
3. Print that 💸
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages
|
||||
and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
@@ -78,7 +78,7 @@ Save, and move over to the “Audience” tab.
|
||||
### 3. Pre-segment your audience (coming soon)
|
||||
|
||||
<Note>
|
||||
## Filter by attribute coming soon {{ class: "text-white" }}
|
||||
## Filter by attribute coming soon
|
||||
We're working on pre-segmenting users by attributes. We will update this manual in the next days.
|
||||
</Note>
|
||||
|
||||
@@ -135,7 +135,7 @@ Lastly, scroll down to “Recontact Options”. Here you have to choose the corr
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
</Note>
|
||||
|
||||
@@ -47,7 +47,7 @@ To display an Interview Prompt in your app you want to proceed as follows:
|
||||
3. That’s it! 🎉
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages
|
||||
and surveys in your app. If not, please follow the [Quick Start Guide (15mins).](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
@@ -90,7 +90,7 @@ Save, and move over to the “Audience” tab.
|
||||
### 3. Pre-segment your audience (coming soon)
|
||||
|
||||
<Note>
|
||||
## Filter by attribute coming soon {{ class: "text-white" }}
|
||||
## Filter by attribute coming soon
|
||||
We're working on pre-segmenting users by attributes. We will update this manual in the next few days.
|
||||
</Note>
|
||||
|
||||
@@ -107,7 +107,7 @@ To create the trigger to show your Interview Prompt, go to the “Audience” ta
|
||||
<Image src={AddAction} alt="Add action" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
|
||||
<Note>
|
||||
## You can also add actions in your code {{ class: "text-white" }}
|
||||
## You can also add actions in your code
|
||||
You can also create [Code Actions](/docs/actions/code) using `formbricks.track("Eventname")` - they will automatically
|
||||
appear in your Actions overview as long as the SDK is embedded.
|
||||
</Note>
|
||||
@@ -160,7 +160,7 @@ Scroll down to “Recontact Options”. Here you have to choose the correct sett
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
</Note>
|
||||
|
||||
@@ -41,7 +41,7 @@ To display the Product-Market Fit survey in your app you want to proceed as foll
|
||||
3. Setup the user action to display survey at good point in time
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages
|
||||
and surveys in your app. If not, please follow the [Quick Start Guide (15mins).](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
@@ -77,7 +77,7 @@ Save, and move over to where the magic happens: The “Audience” tab.
|
||||
### 3. Pre-segment your audience (coming soon)
|
||||
|
||||
<Note>
|
||||
## Filter by attribute coming soon {{ class: "text-white" }}
|
||||
## Filter by attribute coming soon
|
||||
We're working on pre-segmenting users by attributes. We will update this manual in the next days.
|
||||
</Note>
|
||||
|
||||
@@ -132,7 +132,7 @@ Lastly, scroll down to “Recontact Options”. Here you have to choose the corr
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running? {{ class: "text-white" }}
|
||||
## Formbricks Widget running?
|
||||
You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
</Note>
|
||||
|
||||
@@ -35,39 +35,17 @@ Before getting started, make sure you have:
|
||||
|
||||
---
|
||||
|
||||
## Validate your setup
|
||||
|
||||
Once you have completed the steps above, you can validate your setup by checking the **Setup Checklist** in the Settings. Your widget status indicator should go from this:
|
||||
|
||||
<Image
|
||||
src={WidgetNotConnected}
|
||||
alt="Widget isnt connected"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
To this:
|
||||
|
||||
<Image
|
||||
src={WidgetConnected}
|
||||
alt="Widget is connected"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
**Can’t figure it out? [Join our Discord!](https://formbricks.com/discord)**
|
||||
|
||||
---
|
||||
|
||||
## HTML
|
||||
|
||||
All you need to do is copy a `<script>` tag to your HTML head, and that’s about it!
|
||||
|
||||
<CodeGroup title="HTML">
|
||||
```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="./dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init("<environment-id>","<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.0.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks=window.js;window.formbricks.init({environmentId: "<your-environment-id>", apiHost: "<api-host>"})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -84,7 +62,7 @@ All you need to do is copy a `<script>` tag to your HTML head, and that’s abou
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
|
||||
Refer our [Example HTML project](https://github.com/formbricks/examples/tree/main/html) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
|
||||
|
||||
---
|
||||
|
||||
@@ -107,7 +85,7 @@ yarn add @formbricks/js
|
||||
|
||||
Now, update your App.js/ts file to initialise Formbricks.
|
||||
|
||||
<CodeGroup title="app.js">
|
||||
<CodeGroup title="src/App.js">
|
||||
|
||||
```js
|
||||
// other imports
|
||||
@@ -151,10 +129,10 @@ The app initializes 'formbricks' when it's loaded in a browser environment (due
|
||||
src={ReactApp}
|
||||
alt="In app survey in React app for micro surveys"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-lg"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
|
||||
Refer our [Example ReactJs project](https://github.com/formbricks/examples/tree/main/reactjs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
|
||||
|
||||
---
|
||||
|
||||
@@ -192,22 +170,22 @@ import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function FormbricksProvider() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
formbricks?.registerRouteChange();
|
||||
}, [pathname, searchParams]);
|
||||
useEffect(() => {
|
||||
formbricks?.registerRouteChange();
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
````
|
||||
@@ -215,77 +193,80 @@ return null;
|
||||
|
||||
<CodeGroup title="app/layout.tsx">
|
||||
```tsx {{title: 'Typescript'}}
|
||||
// other imports
|
||||
import FormbricksProvider from "./formbricks";
|
||||
import "./globals.css";
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<FormbricksProvider />
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
return (
|
||||
<html lang="en">
|
||||
<FormbricksProvider />
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
````
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Refer our [Example NextJS App Directory project](https://github.com/formbricks/examples/tree/main/nextjs-app) for more help!
|
||||
|
||||
### Pages Directory
|
||||
|
||||
<CodeGroup title="_app.tsx">
|
||||
<CodeGroup title="src/pages/_app.tsx">
|
||||
|
||||
```tsx {{ title: 'Typescript' }}
|
||||
import "@/styles/globals.css";
|
||||
import type { PagesProps } from "next/app";
|
||||
// other import
|
||||
import formbricks from "@formbricks/js";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "your-environment-id",
|
||||
apiHost: "your-api-host", // e.g. https://app.formbricks.com
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}
|
||||
|
||||
export default function Pages({ Component, pageProps }: PagesProps) {
|
||||
const router = useRouter();
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Connect next.js router to Formbricks
|
||||
const handleRouteChange = formbricks?.registerRouteChange;
|
||||
router.events.on("routeChangeComplete", handleRouteChange);
|
||||
useEffect(() => {
|
||||
// Connect next.js router to Formbricks
|
||||
const handleRouteChange = formbricks?.registerRouteChange;
|
||||
router.events.on("routeChangeComplete", handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handleRouteChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <Component {...pageProps} />;
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handleRouteChange);
|
||||
};
|
||||
}, []);
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
```
|
||||
|
||||
````
|
||||
</CodeGroup>
|
||||
|
||||
Refer our [Example NextJS Pages Directory project](https://github.com/formbricks/examples/tree/main/nextjs-pages) for more help!
|
||||
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
Formbricks Environment ID.
|
||||
</Property>
|
||||
<Property name="environment-id" type="string">
|
||||
Formbricks Environment ID.
|
||||
</Property>
|
||||
</Properties>
|
||||
<Properties>
|
||||
<Property name="api-host" type="string">
|
||||
URL of the hosted Formbricks instance.
|
||||
</Property>
|
||||
<Property name="api-host" type="string">
|
||||
URL of the hosted Formbricks instance.
|
||||
</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>
|
||||
<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?
|
||||
@@ -318,32 +299,34 @@ yarn add @formbricks/js
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="src/formbricks.js">
|
||||
|
||||
```js
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
});
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
});
|
||||
}
|
||||
|
||||
export default formbricks;
|
||||
```
|
||||
|
||||
```js {{ title: 'main.js' }}
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="src/main.js">
|
||||
|
||||
```js
|
||||
// other imports
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import formbricks from "@/formbricks";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const app = createApp(App);
|
||||
|
||||
const router = new VueRouter({
|
||||
// Your router configuration here
|
||||
});
|
||||
app.use(router);
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
// Add a global navigation guard
|
||||
router.afterEach((to, from) => {
|
||||
if (typeof formbricks !== "undefined") {
|
||||
formbricks.registerRouteChange();
|
||||
@@ -374,4 +357,28 @@ router.afterEach((to, from) => {
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
|
||||
Refer 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
|
||||
|
||||
Once you have completed the steps above, you can validate your setup by checking the **Setup Checklist** in the Settings. Your widget status indicator should go from this:
|
||||
|
||||
<Image
|
||||
src={WidgetNotConnected}
|
||||
alt="Widget isnt connected"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
To this:
|
||||
|
||||
<Image
|
||||
src={WidgetConnected}
|
||||
alt="Widget is connected"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
**Can’t figure it out? [Join our Discord!](https://formbricks.com/discord)**
|
||||
|
||||
---
|
||||
BIN
apps/formbricks-com/app/docs/integrations/n8n/add-api-key.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
apps/formbricks-com/app/docs/integrations/n8n/add-discord.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 46 KiB |
126
apps/formbricks-com/app/docs/integrations/n8n/page.mdx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
import { Callout } from "@/components/shared/Callout";
|
||||
import Image from "next/image";
|
||||
|
||||
import AddFormbricksTrigger from "./add-formbricks-trigger.png";
|
||||
import CreateNewCredentialBtn from "./create-new-credential-btn.png";
|
||||
import AddApiKey from "./add-api-key.png";
|
||||
import DuplicateSurvey from "./duplicate-survey.png";
|
||||
import SelectEvent from "./select-event.png";
|
||||
import SelectSurvey from "./select-survey.png";
|
||||
import SelectedSurveys from "./selected-surveys.png";
|
||||
import ListenForEvent from "./listen-for-event.png";
|
||||
import TestResponseSuccess from "./test-response-success.png";
|
||||
import AddDiscord from "./add-discord.png";
|
||||
import FillDiscordDetails from "./fill-discord-details.png";
|
||||
import DiscordResponse from "./discord-response.png";
|
||||
import SubmitTestResponse from "./submit-test-response.png";
|
||||
import SuccessConnection from "./success-connection.png";
|
||||
import UpdateQuestionId from "./update-question-id.png";
|
||||
|
||||
export const meta = {
|
||||
title: "n8n Setup",
|
||||
description: "Wire up Formbricks with n8n and 350+ other apps",
|
||||
};
|
||||
|
||||
#### Integrations
|
||||
|
||||
# n8n Setup
|
||||
|
||||
n8n allows you to build flexible workflows focused on deep data integration. And with sharable templates and a user-friendly UI, the less technical people on your team can collaborate on them too. Unlike other tools, complexity is not a limitation. So you can build whatever you want — without stressing over budget. Hook up Formbricks with n8n and you can send your data to 350+ other apps. Here is how to do it.
|
||||
|
||||
<Note>
|
||||
### Nail down your survey first {{ class: "text-white" }}
|
||||
Any changes in the survey cause additional work in the n8n node. It makes sense to first settle on the survey
|
||||
you want to run and then get to setting up n8n.
|
||||
</Note>
|
||||
|
||||
## Step 1: Setup your survey incl. `questionId` for every question
|
||||
|
||||
When setting up the node your life will be easier when you change the `questionId`s of your survey questions. You can only do so **before** you publish your survey.
|
||||
|
||||
<Image src={UpdateQuestionId} alt="Update Question ID" quality="100" className="rounded-lg" />
|
||||
|
||||
_In every question card in the Advanced Settings you find the Question ID field. Update it so that you'll recognize the response tied to this question._
|
||||
|
||||
<Note>
|
||||
### Already published? Duplicate survey {{ class: "text-white" }}
|
||||
You can only update the questionId when the survey was not yet published. Already published it? Just **duplicate
|
||||
it** to update the questionIds.
|
||||
<Image
|
||||
src={DuplicateSurvey}
|
||||
alt="Duplicate Survey"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
</Note>
|
||||
|
||||
## Step 2: Setup your n8n workflow
|
||||
|
||||
Go to [n8n.io](https://n8n.io) and create a new workflow. Search for “Formbricks” to get started:
|
||||
|
||||
<Image src={AddFormbricksTrigger} alt="Add Formbricks Trigger" quality="100" className="rounded-lg" />
|
||||
|
||||
## Step 3: Connect Formbricks with n8n
|
||||
|
||||
Now, you have to connect n8n with Formbricks via an API Key:
|
||||
|
||||
<Image src={CreateNewCredentialBtn} alt="Create new credential button" quality="100" className="rounded-lg" />
|
||||
|
||||
Click on Create New Credentail button to add your host and API Key
|
||||
|
||||
<Image src={AddApiKey} alt="Add host and api key" quality="100" className="rounded-lg" />
|
||||
|
||||
Now you need an API key. Please refer to the [API Key Setup](/docs/api/api-key-setup) page to learn how to create one.
|
||||
|
||||
Once you copied it in the API Key field, hit Save button to test the connection and save the credentials.
|
||||
|
||||
<Image src={SuccessConnection} alt="Successful Connection" quality="100" className="rounded-lg" />
|
||||
|
||||
## Step 4: Select Event
|
||||
|
||||
Next, you can choose the event you want to trigger the node on. You can select multiple events:
|
||||
|
||||
<Image src={SelectEvent} alt="Select Event" quality="100" className="rounded-lg" />
|
||||
|
||||
Here, we are adding `Response Finished` as an event, which will trigger when the survey has been filled out.
|
||||
|
||||
## Step 5: Select Survey
|
||||
|
||||
Next, you can choose from all the surveys you have created in this environment. You can select multiple surveys:
|
||||
|
||||
<Image src={SelectSurvey} alt="Select Survey" quality="100" className="rounded-lg" />
|
||||
|
||||
Here, we are selecting two surveys.
|
||||
|
||||
<Image src={SelectedSurveys} alt="Selected Surveys" quality="100" className="rounded-lg" />
|
||||
|
||||
## Step 6: Test your trigger
|
||||
|
||||
In order to set up n8n you'll need a test response in the selected survey. This allows you to select the individual values of each response in your workflow. If you have Formbricks running locally and you want to set up an in-app survey, you can use our [Demo App](/docs/contributing/demo) to trigger a survey and submit a response.
|
||||
|
||||
<Image src={SubmitTestResponse} alt="Submit Test Response" quality="100" className="rounded-lg" />
|
||||
|
||||
Next, click on Listen for event button.
|
||||
|
||||
<Image src={ListenForEvent} alt="Listen for event" quality="100" className="rounded-lg" />
|
||||
|
||||
Then, go to the survey which you selected. Fill it out, and wait for the particular event to trigger (in this case it's `Response Finished`). Once the event is triggered you will see the response that you filled out in the survey.
|
||||
|
||||
<Image src={TestResponseSuccess} alt="Test Response Success" quality="100" className="rounded-lg" />
|
||||
|
||||
Now you have all the data you need at hand. The next steps depend on what you want to do with it. In this tutorial, we will send submissions to a discord channel:
|
||||
|
||||
## Step 7: Add discord to your workflow
|
||||
|
||||
Click on the plus and search `Discord`.
|
||||
|
||||
<Image src={AddDiscord} alt="Add Discord" quality="100" className="rounded-lg" />
|
||||
|
||||
Fill in the `Webhook URL` and the `Content` that you want to receive in the respective discord channel. Next, click on `Execute Node` button to test the node.
|
||||
|
||||
<Image src={FillDiscordDetails} alt="Fill Discord Details" quality="100" className="rounded-lg" />
|
||||
|
||||
Once the execution is successful, you'll receive the content in the discord channel.
|
||||
|
||||
<Image src={DiscordResponse} alt="Discord Response" quality="100" className="rounded-lg" />
|
||||
BIN
apps/formbricks-com/app/docs/integrations/n8n/select-event.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
apps/formbricks-com/app/docs/integrations/n8n/select-survey.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -26,7 +26,7 @@ export const meta = {
|
||||
Zapier is a powerful ally. Hook up Formbricks with Zapier and you can send your data to 5000+ other apps. Here is how to do it.
|
||||
|
||||
<Note>
|
||||
### Nail down your survey first? {{ class: "text-white" }}
|
||||
### Nail down your survey first?
|
||||
Any changes in the survey cause additional work in the Zap. It makes sense to first settle on the survey you
|
||||
want to run and then get to setting up Zapier.
|
||||
</Note>
|
||||
@@ -45,7 +45,7 @@ When setting up the Zap your life will be easier when you change the `questionId
|
||||
_In every question card in the Advanced Settings you find the Question ID field. Update it so that you’ll recognize the response tied to this question._
|
||||
|
||||
<Note>
|
||||
### Already published? Duplicate survey {{ class: "text-white" }}
|
||||
### Already published? Duplicate survey
|
||||
You can only update the questionId when the survey was not yet published. Already published it? Just **duplicate
|
||||
it** to update the questionIds.
|
||||
<Image
|
||||
|
||||
@@ -38,7 +38,7 @@ To prefill the first question of a survey, append `?question_id=answer` at the e
|
||||
Please make sure the answer is [URL encoded](https://www.urlencoder.org/).
|
||||
|
||||
<Note>
|
||||
## Prefill only the first question {{ class: "text-white" }}
|
||||
## Prefill only the first question
|
||||
Currently, you can only prefill the first question of a link survey.
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -165,4 +165,12 @@ formbricks-quickstart-formbricks-1 | Listening on port 3000 url: http://<random
|
||||
|
||||
You can close the logs again with `CTRL + C`.
|
||||
|
||||
<Note>
|
||||
## Customizing the Build Time variables
|
||||
|
||||
To edit any of the variables that start with `NEXT_PUBLIC_`, you need to rebuild the Formbricks Docker from source! It is just one
|
||||
more added step and is not as much of a hassle as you think! Please check the [Building from Source](/docs/self-hosting/from-source) section!
|
||||
|
||||
</Note>
|
||||
|
||||
Still facing issues? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you!
|
||||
|
||||
@@ -32,6 +32,7 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are
|
||||
2. **Modify the `.env.docker` file as required by your setup.** <br/> This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`)
|
||||
|
||||
<Note>
|
||||
## Editing a NEXT_PUBLIC_* variable?
|
||||
Note: All environment variables starting with `NEXT_PUBLIC_` are used at build time. When you change
|
||||
environment variables later, you need to rebuild the image with `docker compose build` for the changes
|
||||
to take effect.
|
||||
|
||||
@@ -168,6 +168,14 @@ cd formbricks && docker compose logs -f
|
||||
|
||||
You can close the logs again with `CTRL + C`.
|
||||
|
||||
<Note>
|
||||
## Customizing the Build Time variables
|
||||
|
||||
To edit any of the variables that start with `NEXT_PUBLIC_`, you need to rebuild the Formbricks Docker from source! It is just one
|
||||
more added step and is not as much of a hassle as you think! Please check the [Building from Source](/docs/self-hosting/from-source) section!
|
||||
|
||||
</Note>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues, consider the following steps:
|
||||
@@ -180,4 +188,4 @@ If you encounter any issues, consider the following steps:
|
||||
|
||||
- **Check Formbricks Logs**: Run `cd formbricks && docker compose logs` to check the logs of the Formbricks stack.
|
||||
|
||||
- **Still can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
**Still can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { Button } from "./Button";
|
||||
import logoHtml from "@/images/logos/html5.svg";
|
||||
import logoNextjs from "@/images/logos/nextjs.svg";
|
||||
import logoReactJs from "@/images/logos/reactjs.svg";
|
||||
import logoVueJs from "@/images/logos/vuejs.svg";
|
||||
import { Button } from "./Button";
|
||||
|
||||
const libraries = [
|
||||
{
|
||||
@@ -39,7 +39,10 @@ export function Libraries() {
|
||||
<div className="my-16 xl:max-w-none">
|
||||
<div className="not-prose mt-4 grid grid-cols-1 gap-x-6 gap-y-10 border-slate-900/5 dark:border-white/5 sm:grid-cols-2 xl:max-w-none xl:grid-cols-3">
|
||||
{libraries.map((library) => (
|
||||
<div key={library.name} className="flex flex-row-reverse gap-6">
|
||||
<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">
|
||||
<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>
|
||||
@@ -50,7 +53,7 @@ export function Libraries() {
|
||||
</p>
|
||||
</div>
|
||||
<Image src={library.logo} alt="" className="h-12 w-12" unoptimized />
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -225,7 +225,10 @@ export const navigation: Array<NavGroup> = [
|
||||
},
|
||||
{
|
||||
title: "Integrations",
|
||||
links: [{ title: "Zapier", href: "/docs/integrations/zapier" }],
|
||||
links: [
|
||||
{ title: "Zapier", href: "/docs/integrations/zapier" },
|
||||
{ title: "n8n", href: "/docs/integrations/n8n" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Link Surveys",
|
||||
@@ -258,6 +261,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Overview", href: "/docs/api/overview" },
|
||||
{ title: "API Key Setup", href: "/docs/api/api-key-setup" },
|
||||
{ title: "Responses", href: "/docs/api/responses" },
|
||||
{ title: "Surveys", href: "/docs/api/surveys" },
|
||||
{ title: "Webhook", href: "/docs/api/webhooks" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -264,11 +264,11 @@ export default function Header() {
|
||||
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
|
||||
Blog {/* <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p> */}
|
||||
</Link>
|
||||
{/* <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">2</p>
|
||||
</Link> */}
|
||||
Careers <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/concierge"
|
||||
@@ -372,7 +372,7 @@ export default function Header() {
|
||||
<Link href="#pricing">Pricing</Link>
|
||||
<Link href="/docs">Docs</Link>
|
||||
<Link href="/blog">Blog</Link>
|
||||
{/* <Link href="/careers">Careers</Link> */}
|
||||
<Link href="/careers">Careers</Link>
|
||||
<Button
|
||||
variant="secondary"
|
||||
EndIcon={GitHubIcon}
|
||||
|
||||
@@ -59,7 +59,9 @@ export default function LayoutMdx({ meta, children }: Props) {
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
<Prose className="">{children}</Prose>
|
||||
<Prose className="prose-h2:text-2xl prose-h3:text-xl prose-a:text-slate-900 prose-a:hover:text-slate-900 prose-a:text-decoration-brand prose-a:not-italic ">
|
||||
{children}
|
||||
</Prose>
|
||||
</article>
|
||||
<SlideInBanner delay={5000} scrollPercentage={10} UTMSource="learnArticle" />
|
||||
</main>
|
||||
|
||||
@@ -46,7 +46,10 @@ const SlideInBanner: React.FC<Props> = ({ delay = 5000, scrollPercentage = 10, U
|
||||
Did you know? Formbricks is the only open source Experience Management solution: free &
|
||||
privacy-first!
|
||||
</p>
|
||||
<Button size="sm" href={`https://formbricks.com?utm_source=${UTMSource}`}>
|
||||
<Button
|
||||
size="sm"
|
||||
href={`https://formbricks.com?utm_source=${UTMSource}`}
|
||||
className="whitespace-nowrap">
|
||||
Learn more
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -93,6 +93,11 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.",
|
||||
href: "https://openbb.co",
|
||||
},
|
||||
{
|
||||
name: "Rivet",
|
||||
description: "Open-source solution to deploy, scale, and operate your multiplayer game.",
|
||||
href: "https://rivet.gg",
|
||||
},
|
||||
{
|
||||
name: "Sniffnet",
|
||||
description:
|
||||
@@ -102,7 +107,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
{
|
||||
name: "Tolgee",
|
||||
description: "Software localization from A to Z made really easy.",
|
||||
href: "https://tolgee.io/",
|
||||
href: "https://tolgee.io",
|
||||
},
|
||||
{
|
||||
name: "Trigger.dev",
|
||||
|
||||
@@ -26,7 +26,7 @@ _Most open source projects get abandoned after a while. But these 5 open source
|
||||
<Image
|
||||
src={HeaderImage}
|
||||
alt="Open source survey tool self-hostable: Find the 5 best (and maintained) open source survey tool 2023."
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
Looking for the perfect open source survey tool to help you gather valuable insights and improve your business? Look no further!
|
||||
@@ -38,7 +38,7 @@ We've compiled a list of the top 5 open source form and survey tools that are st
|
||||
<Image
|
||||
src={Formbricks}
|
||||
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
Formbricks is a powerful open source survey tool designed to help you get better experience data for your business. This tool allows you to survey specific customer segments at any point in the user journey, providing you with invaluable insights into what your customers think and feel about your product.
|
||||
@@ -56,7 +56,7 @@ Formbricks is a powerful open source survey tool designed to help you get better
|
||||
<Image
|
||||
src={SurveyJS}
|
||||
alt="SurveyJS is a comprehensive JS library to build your own form or survey application."
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
SurveyJS is a collection of JavaScript Librarys to build forms. Building your own form management system has never been easier than with SurveyJS. It packs:
|
||||
@@ -74,7 +74,7 @@ SurveyJS is a collection of JavaScript Librarys to build forms. Building your ow
|
||||
<Image
|
||||
src={Typebot}
|
||||
alt="Open source survey and form builder SurveyJS lets you build surveys fast"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
Coming in at number three on our list is Typebot, that makes it really easy to create conversational forms and surveys. Typebot helps you engage with your audience in a more interactive way, leading to higher response rates and better data. It comes with:
|
||||
@@ -92,7 +92,7 @@ Coming in at number three on our list is Typebot, that makes it really easy to c
|
||||
<Image
|
||||
src={OpnForm}
|
||||
alt="OpnForm is an open source form builder for experience management"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
OpnForms is a flexible and powerful open source form and survey tool designed to make data collection easy and efficient. OpnForm packs lots of features, especially for a Beta:
|
||||
@@ -109,7 +109,7 @@ OpnForms is a flexible and powerful open source form and survey tool designed to
|
||||
<Image
|
||||
src={LimeSurvey}
|
||||
alt="LimeSurvey is open source survey builder to manage experiences with forms"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
LimeSurvey has been around for at least a decade. It's a powerful survey tool made for more classical, scientific surveying. It packs:
|
||||
|
||||
@@ -31,7 +31,7 @@ Funnily enough, it started with a tweet:
|
||||
<Image
|
||||
src={TweetPeer}
|
||||
alt="Peer tweets about the best open-source Typeform alternative"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
When I read this tweet, I was sitting in a WeWork in Mexico City, feeling a bit guilty for being such a cliché digital nomad. Sipping on my decaffeinated matcha with lactose-free goat milk, I slid into Matti's DMs. I knew he had built an open-source survey tool a few months ago and suggested he comment on the tweet. We started chatting.
|
||||
@@ -43,7 +43,7 @@ So we chatted about the opportunity of building a commercial open-source alterna
|
||||
<Image
|
||||
src={SnoopForms}
|
||||
alt="SnoopForms was the OS Typeform alternative with a twist"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
### **snoopForms and why OS Typeform isn’t a good business**
|
||||
@@ -80,7 +80,7 @@ Following Paul Graham's advice to start with a **[narrow but deep hole](http://p
|
||||
<Image
|
||||
src={PMFDashboard}
|
||||
alt="Peer tweets about the best open-source Typeform alternative"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
_We built a custom dashboard just to visualize the PMF survey results_
|
||||
|
||||
@@ -108,7 +108,7 @@ March and the first half of April flew by as we hacked together the MVP of what
|
||||
<Image
|
||||
src={TitleImage}
|
||||
alt="GitHub sponsors Formbricks to join their open-source accelerator program"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
A welcomed motivator came in the form of an email from GitHub: We we were chosen to take part in the first-ever batch of the GitHub Open-Source Accelerator! It was a really fun programme and we learned a lot! You can read about our experience [here.](https://formbricks.com/blog/github-accelerator-experience)
|
||||
@@ -122,7 +122,7 @@ A couple of weeks later, Peer from [Cal.com](http://Cal.com) came back to northe
|
||||
<Image
|
||||
src={TwitterResult}
|
||||
alt="SnoopForms was the OS Typeform alternative with a twist"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
We wanted to compare how the publicly asked survey (Twitter) compared to a survey among the community of users. The results were eye-opening:
|
||||
@@ -130,7 +130,7 @@ We wanted to compare how the publicly asked survey (Twitter) compared to a surve
|
||||
<Image
|
||||
src={EmailResult}
|
||||
alt="SnoopForms was the OS Typeform alternative with a twist"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg h-full"
|
||||
/>
|
||||
_Anything above 40% is considered PMF, 60% is 🔥🔥🔥_
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ _We were among the first 20 teams ever to run through the Open-Source Accelerato
|
||||
<Image
|
||||
src={Demo}
|
||||
alt="GitHub sponsors Formbricks to join their open-source accelerator program"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
## Hey there,
|
||||
@@ -39,7 +39,7 @@ January and February came and went. On the 22nd of March, we received an email f
|
||||
<Image
|
||||
src={Mail}
|
||||
alt="GitHub invited us to join the GitHub Accelerator and share our experience"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
Needless to say, we were thrilled! We were selected from over 1000 open-source projects, alongside renowned and popular projects like [Nuxt](https://github.com/nuxt/nuxt), [TRPC](https://github.com/trpc/trpc), and [Responsively App](https://github.com/responsively-org/responsively-app). Here is a summary of what we got:
|
||||
@@ -67,7 +67,7 @@ I mean look at all these happy people:
|
||||
<Image
|
||||
src={Teams}
|
||||
alt="GitHub invited us to join the GitHub Accelerator and share our experience"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
Here's an overview of the ten sessions and their relevance to us as a venture-focused startup:
|
||||
@@ -121,7 +121,7 @@ Yes, absolutely. The sessions were excellent, we met a handful of inspiring buil
|
||||
<Image
|
||||
src={TitleImage}
|
||||
alt="GitHub sponsors Formbricks to join their open-source accelerator program"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
<NewsletterSignup />
|
||||
|
||||
@@ -22,7 +22,7 @@ _We're getting ready to take our open-source experience management platform to n
|
||||
<Image
|
||||
src={TitleImage}
|
||||
alt="GitHub sponsors Formbricks to join their open-source accelerator program"
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
## Hey there,
|
||||
|
||||
@@ -18,7 +18,7 @@ export const meta = {
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={RobinHoodMeme} alt="Robin Hood Meme" className="rounded-lg" />
|
||||
<Image src={RobinHoodMeme} alt="Robin Hood Meme" className="rounded-lg w-full" />
|
||||
|
||||
_What motivates us to build open source tech in such a crowded space? What do we see what others might not? And how do we understand the relationship between free open source tech and a commercial complement?_
|
||||
|
||||
@@ -30,7 +30,7 @@ Before we talk about why the open source mentality is growing on Matti and I, le
|
||||
|
||||
Forms are an essential part of web infrastructure, every application has one or more. Whenever someone wants to get data from a brain into a computer, forms come into play:
|
||||
|
||||
<Image src={WhyWeDoIt} alt="Why we do it" className="rounded-lg" />
|
||||
<Image src={WhyWeDoIt} alt="Why we do it" className="rounded-lg w-full" />
|
||||
|
||||
Secondly, forms are the starting point of many deeper interactions. Lead generation forms, application forms, immigration forms collect lots of meaningful data. It gets processed and enriched, often in the process of building a lasting customer relationship. This implies more interaction, more data input and management, more opportunities to grow. Owning the starting point of this relationship is inherently valuable.
|
||||
|
||||
@@ -47,7 +47,7 @@ Why are large corporations like Google, Facebook, IBM, etc. investing $$$ in OSS
|
||||
<ResponsiveEmbed
|
||||
src="https://www.youtube-nocookie.com/embed/j4ovbmsp6p0?start=9"
|
||||
allowFullScreen
|
||||
className="rounded-lg"
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
_"I doubt it."_
|
||||
@@ -78,7 +78,7 @@ To break this down, let's look at the challenges commercial OSS companies face.
|
||||
|
||||
Matti and I are not just motivated by the chance to gain economic security for our families over the course of the next decade. We also believe in and want to contribute to the positive sum thinking of the open source community. Building a commercial open source software company presents a model Robin Hood would be proud of:
|
||||
|
||||
<Image src={RobinHoodMeme} alt="Robin Hood Meme" className="rounded-lg" />
|
||||
<Image src={RobinHoodMeme} alt="Robin Hood Meme" className="rounded-lg w-full" />
|
||||
|
||||
A COSS company builds cool tech and shares it freely. It uses its own tech to offer a product to mostly enterprise customers and thereby finances the further development of the technology everyone can use - to a large extent - for free.
|
||||
|
||||
@@ -92,7 +92,7 @@ But there is more.
|
||||
|
||||
Growing up in the longest period without war in Europe as well as the longest "up only"-market in recent history, 2022 feels utterly unsettling. War, inflation, recession, heat records, wildfires, outrageous inequality, hate, weak democracies -- everything goes to sh\*t everywhere, all at once.
|
||||
|
||||
<Image src={EverythinEverywhereAllAtOnce} alt="Everything everywhere all at once" className="rounded-lg" />
|
||||
<Image src={EverythinEverywhereAllAtOnce} alt="Everything everywhere all at once" className="rounded-lg w-full" />
|
||||
|
||||
If you haven't seen "Everything, everywhere, all at once" I highly recommend it.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const meta = {
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={Wrestling} alt="Why we do it" className="rounded-lg" />
|
||||
<Image src={Wrestling} alt="Why we do it" className="rounded-lg w-full" />
|
||||
|
||||
_In September, we kicked it off with a Typeform open-source alternative. As we build and learn, our focus is shifting. We talk about how we look at form and survey tools today, why experience management not only matters for enterprise and why the endgame looks a lot more like open-source Qualtrics. Qualtrics? What happened to Typeform?_
|
||||
|
||||
@@ -32,13 +32,13 @@ Let’s dive in 👇
|
||||
|
||||
## How it started
|
||||
|
||||
<Image src={Snoopforms} alt="How it began - snoopforms landing page" className="rounded-lg" />
|
||||
<Image src={Snoopforms} alt="How it began - snoopforms landing page" className="rounded-lg w-full" />
|
||||
|
||||
Matti and I kicked off our work on [snoopForms](https://snoopforms.com) as the “Open source Typeform Alternative” - and we weren’t the first to try this:
|
||||
|
||||
<Image src={Ohmyform} alt="Ohmyform" className="rounded-lg" />
|
||||
<Image src={Tellform} alt="Tellform" className="rounded-lg" />
|
||||
<Image src={Tripetto} alt="Tripetto" className="rounded-lg" />
|
||||
<Image src={Tellform} alt="Tellform" className="rounded-lg w-full" />
|
||||
<Image src={Ohmyform} alt="Ohmyform" className="rounded-lg w-full" />
|
||||
<Image src={Tripetto} alt="Tripetto" className="rounded-lg w-full" />
|
||||
|
||||
TellForm and OhMyForm were given up, Tripetto niched down to the WordPress ecosystem and are doing quite well selling $99 / year licenses.
|
||||
|
||||
@@ -65,7 +65,7 @@ The first group isn’t a tempting cohort to build for: They are very price-sens
|
||||
|
||||
Typeform makes forms look pretty and thereby claims to achieve a higher conversion rate than other form tools. Here is the key value proposition from typeform.com (24.03.23):
|
||||
|
||||
<Image src={TypeformValue} alt="Why we do it" className="rounded-lg" />
|
||||
<Image src={TypeformValue} alt="Why we do it" className="rounded-lg w-full" />
|
||||
|
||||
The average conversion rate of an emailed out Typeform is 5%. In-app surveys for example average out at [30% so 6x better.](https://news.ycombinator.com/item?id=30377349) Typeforms are the most pretty forms out there and likely to convert better than a Google Form but are by no means the best way to get insights from your audience.
|
||||
|
||||
@@ -167,7 +167,7 @@ You can create events either in your code base or with our No Code Event creator
|
||||
|
||||
There is so much you can do with it, here is a sneakpeek:
|
||||
|
||||
<Image src={FormbricksSneak} alt="Formbricks Sneak Preview" className="rounded-lg" />
|
||||
<Image src={FormbricksSneak} alt="Formbricks Sneak Preview" className="rounded-lg w-full" />
|
||||
|
||||
What all of this looks like in action we’ll share in a product walkthrough video soon. We are super excited to get the new Formbricks into your hands asap.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export const meta = {
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={HeaderImage} alt="Formbricks - Open Source Forms and Surveys" className="rounded-lg" />
|
||||
<Image src={HeaderImage} alt="Formbricks - Open Source Forms and Surveys" className="rounded-lg w-full" />
|
||||
|
||||
_It has been quiet in the past weeks, but we didn't spend our days sitting around. Find out what we were up to and where we are taking Formbricks from here._
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const meta = {
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={TitleImage} alt="Title Image" className="rounded-lg" />
|
||||
<Image src={TitleImage} alt="Title Image" className="rounded-lg w-full" />
|
||||
|
||||
_Open source software (OSS) beats out proprietary software in every regard - except for value capturing. No-Code tools shorten the feedback loop between builders and consumers, kicking productivity through the roof. Here is why a no-code interface is cheatcode for OSS and why particularly large corporations and governments are to benefit the most._
|
||||
|
||||
@@ -51,7 +51,7 @@ The idea of making and selling proprietary software is simple: If you cannot see
|
||||
|
||||
OSS doesn't have these restrictions. Unlike cable TV, you know exactly what you're getting, because the source code is openly accessible. If you are unhappy with the integrations built by the community or company developing the tool, you can build your own. OSS is the monthly subscription you can cancel once you binge watched the whole library. **It's better for the consumer and this is why it's the winning model.**
|
||||
|
||||
<Image src={ProprietaryDependence} alt="Depend on proprietary software meme" className="rounded-lg" />
|
||||
<Image src={ProprietaryDependence} alt="Depend on proprietary software meme" className="rounded-lg w-full" />
|
||||
|
||||
But no lock-in is only one key advantage, **data ownership is equally important**. In times of GDPR, CCPA, YMCA, etc. owning your data is essential to be fully compliant. Currently, there a lot of legal uncertainty for European institutions to use any SaaS tool where data flows to servers located in the United States. You can neither use Google Analytics nor Zapier. The solution, in many cases, is to neither analyze nor automate. Governmental institutions or larger companies cannot afford the reputational risk and attached fines of being exposed as non-compliant with privacy legislation.
|
||||
|
||||
@@ -156,7 +156,7 @@ The OSS Renaissance is in full swing. The argument laid out above is just one of
|
||||
|
||||
---
|
||||
|
||||
<Image src={HeaderImage} alt="Formbricks - Open Source Forms & Surveys" className="" />
|
||||
<Image src={HeaderImage} alt="Formbricks - Open Source Forms & Surveys" className="w-full" />
|
||||
|
||||
Finally good open source forms
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import HeroTitle from "@/components/shared/HeroTitle";
|
||||
import Layout from "@/components/shared/Layout";
|
||||
import Link from "next/link";
|
||||
|
||||
/* const Roles = [
|
||||
const Roles = [
|
||||
{
|
||||
name: "Full-Stack Engineer",
|
||||
description: "Join early and be a part of our journey from start to IPO 🚀",
|
||||
name: "(Senior) Full-Stack Engineer",
|
||||
description: "Join early and be a part of our journey right from the start 🚀",
|
||||
location: "Worldwide",
|
||||
workplace: "Remote",
|
||||
},
|
||||
} /*
|
||||
{
|
||||
name: "Junior Full-Stack Engineer",
|
||||
description: "All you want is write code and learn? You're exactly right!",
|
||||
location: "Worldwide",
|
||||
workplace: "Remote",
|
||||
},
|
||||
]; */
|
||||
},*/,
|
||||
];
|
||||
|
||||
export default function CareersPage() {
|
||||
return (
|
||||
<Layout
|
||||
title="Careers"
|
||||
description="Work with us on helping teams make customer-centric decisions - all privacy-focused.">
|
||||
description="Work with us on helping teams make truly customer-centric decisions - all privacy-focused.">
|
||||
<HeroTitle
|
||||
headingPt1="Help teams make"
|
||||
headingTeal="customer-centric"
|
||||
headingPt2="decisions."
|
||||
subheading="We are currently not hiring. Contributions are always welcome!"
|
||||
headingPt1="Life is too short for"
|
||||
headingTeal="mediocre products."
|
||||
headingPt2=""
|
||||
subheading="Empower teams to build exactly what their users need."
|
||||
/>
|
||||
{/*
|
||||
|
||||
<div className="mx-auto w-3/4">
|
||||
|
||||
{Roles.map((role) => (
|
||||
{Roles.map((role) => (
|
||||
<Link
|
||||
href="https://formbricks.notion.site/Work-at-Formbricks-6c3ad218b2c7461ca2714ce2101730e4?pvs=4"
|
||||
target="_blank"
|
||||
@@ -41,7 +41,7 @@ export default function CareersPage() {
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>*/}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function ArticlePage({ article = {} }: ArticlePageProps) {
|
||||
// Use next/image to render images in markdown
|
||||
const renderers = {
|
||||
img: (image) => {
|
||||
return <Image src={image.src} alt={image.alt} width={1000} height={500} />;
|
||||
return <Image src={image.src} alt={image.alt} width={1000} height={500} className="rounded-lg" />;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -67,4 +67,4 @@ export default {
|
||||
},
|
||||
},
|
||||
plugins: [typographyPlugin, headlessuiPlugin, forms],
|
||||
} satisfies Config;
|
||||
} satisfies Config;
|
||||
|
||||
@@ -163,6 +163,10 @@ export async function duplicateSurveyAction(environmentId: string, surveyId: str
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage
|
||||
? JSON.parse(JSON.stringify(existingSurvey.surveyClosedMessage))
|
||||
: prismaClient.JsonNull,
|
||||
|
||||
verifyEmail: existingSurvey.verifyEmail
|
||||
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
||||
: prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
return newSurvey;
|
||||
@@ -290,6 +294,7 @@ export async function copyToOtherEnvironmentAction(
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
return newSurvey;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import JsLogo from "@/images/jslogo.png";
|
||||
import WebhookLogo from "@/images/webhook.png";
|
||||
import ZapierLogo from "@/images/zapier-small.png";
|
||||
import n8nLogo from "@/images/n8n.png";
|
||||
import { Card } from "@formbricks/ui";
|
||||
import Image from "next/image";
|
||||
|
||||
@@ -40,6 +41,17 @@ export default function IntegrationsPage({ params }) {
|
||||
description="Trigger Webhooks based on actions in your surveys"
|
||||
icon={<Image src={WebhookLogo} alt="Webhook Logo" />}
|
||||
/>
|
||||
<Card
|
||||
docsHref="https://formbricks.com/docs/integrations/n8n"
|
||||
docsText="Docs"
|
||||
docsNewTab={true}
|
||||
connectHref="https://n8n.io"
|
||||
connectText="Connect"
|
||||
connectNewTab={true}
|
||||
label="n8n"
|
||||
description="Integrate Formbricks with 350+ apps via n8n"
|
||||
icon={<Image src={n8nLogo} alt="n8n Logo" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,10 +20,17 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
|
||||
const [redirectUrl, setRedirectUrl] = useState<string | null>("");
|
||||
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
|
||||
const [verifyEmailToggle, setVerifyEmailToggle] = useState(false);
|
||||
|
||||
const [surveyClosedMessage, setSurveyClosedMessage] = useState({
|
||||
heading: "Survey Completed",
|
||||
subheading: "This free & open-source survey has been closed",
|
||||
});
|
||||
|
||||
const [verifyEmailSurveyDetails, setVerifyEmailSurveyDetails] = useState({
|
||||
name: "",
|
||||
subheading: "",
|
||||
});
|
||||
const [closeOnDate, setCloseOnDate] = useState<Date>();
|
||||
|
||||
const handleRedirectCheckMark = () => {
|
||||
@@ -63,6 +70,14 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyEmailToogle = () => {
|
||||
setVerifyEmailToggle((prev) => !prev);
|
||||
|
||||
if (verifyEmailToggle && localSurvey.verifyEmail) {
|
||||
setLocalSurvey({ ...localSurvey, verifyEmail: null });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseOnDateChange = (date: Date) => {
|
||||
const equivalentDate = date?.getDate();
|
||||
date?.setUTCHours(0, 0, 0, 0);
|
||||
@@ -88,6 +103,22 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
setLocalSurvey({ ...localSurvey, surveyClosedMessage: message });
|
||||
};
|
||||
|
||||
const handleVerifyEmailSurveyDetailsChange = ({
|
||||
name,
|
||||
subheading,
|
||||
}: {
|
||||
name?: string;
|
||||
subheading?: string;
|
||||
}) => {
|
||||
const message = {
|
||||
name: name || verifyEmailSurveyDetails.name,
|
||||
subheading: subheading || verifyEmailSurveyDetails.subheading,
|
||||
};
|
||||
|
||||
setVerifyEmailSurveyDetails(message);
|
||||
setLocalSurvey({ ...localSurvey, verifyEmail: message });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.redirectUrl) {
|
||||
setRedirectUrl(localSurvey.redirectUrl);
|
||||
@@ -102,6 +133,14 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
setSurveyClosedMessageToggle(true);
|
||||
}
|
||||
|
||||
if (localSurvey.verifyEmail) {
|
||||
setVerifyEmailSurveyDetails({
|
||||
name: localSurvey.verifyEmail.name!,
|
||||
subheading: localSurvey.verifyEmail.subheading!,
|
||||
});
|
||||
setVerifyEmailToggle(true);
|
||||
}
|
||||
|
||||
if (localSurvey.closeOnDate) {
|
||||
setCloseOnDate(localSurvey.closeOnDate);
|
||||
setSurveyCloseOnDateToggle(true);
|
||||
@@ -257,6 +296,43 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
|
||||
<AdvancedOptionToggle
|
||||
htmlId="verifyEmailBeforeSubmission"
|
||||
isChecked={verifyEmailToggle}
|
||||
onToggle={handleVerifyEmailToogle}
|
||||
title="Verify email before submission"
|
||||
description="Only let people with a real email respond."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<p className="text-md font-semibold">How it works</p>
|
||||
<p className="mb-4 mt-2 text-sm text-slate-500">
|
||||
Respondants will receive the survey link via email.
|
||||
</p>
|
||||
<Label htmlFor="headline">Survey Name (Public)</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
id="heading"
|
||||
className="mb-4 mt-2 bg-white"
|
||||
name="heading"
|
||||
placeholder="Job Application Form"
|
||||
defaultValue={verifyEmailSurveyDetails.name}
|
||||
onChange={(e) => handleVerifyEmailSurveyDetailsChange({ name: e.target.value })}
|
||||
/>
|
||||
|
||||
<Label htmlFor="headline">Subheader (Public)</Label>
|
||||
<Input
|
||||
className="mt-2 bg-white"
|
||||
id="subheading"
|
||||
name="subheading"
|
||||
placeholder="Thanks for applying as a full stack engineer"
|
||||
defaultValue={verifyEmailSurveyDetails.subheading}
|
||||
onChange={(e) => handleVerifyEmailSurveyDetailsChange({ subheading: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -159,6 +159,15 @@ export default function SurveyMenuBar({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
survey.redirectUrl &&
|
||||
!survey.redirectUrl.includes("https://") &&
|
||||
!survey.redirectUrl.includes("http://")
|
||||
) {
|
||||
toast.error("Please enter a valid URL for redirecting respondents");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -199,7 +208,8 @@ export default function SurveyMenuBar({
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(`Error saving changes`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -13,6 +13,8 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import Loading from "@/app/s/[surveyId]/loading";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import VerifyEmail from "@/app/s/[surveyId]/VerifyEmail";
|
||||
import { verifyTokenAction } from "@/app/s/[surveyId]/actions";
|
||||
|
||||
interface LinkSurveyProps {
|
||||
survey: TSurvey;
|
||||
@@ -40,6 +42,34 @@ export default function LinkSurvey({ survey, product }: LinkSurveyProps) {
|
||||
// Create a reference to the top element
|
||||
const topRef = useRef<HTMLDivElement>(null);
|
||||
const [autoFocus, setAutofocus] = useState(false);
|
||||
const URLParams = new URLSearchParams(typeof window !== "undefined" ? window.location.search : "");
|
||||
const [shouldRenderVerifyEmail, setShouldRenderVerifyEmail] = useState(false);
|
||||
const [isTokenValid, setIsTokenValid] = useState(true);
|
||||
|
||||
const checkVerifyToken = async (verifyToken: string): Promise<boolean> => {
|
||||
try {
|
||||
const result = await verifyTokenAction(verifyToken, survey.id);
|
||||
return result;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (survey.verifyEmail) {
|
||||
setShouldRenderVerifyEmail(true);
|
||||
}
|
||||
const verifyToken = URLParams.get("verify");
|
||||
if (verifyToken) {
|
||||
checkVerifyToken(verifyToken)
|
||||
.then((result) => {
|
||||
setIsTokenValid(result);
|
||||
setShouldRenderVerifyEmail(!result); // Set shouldRenderVerifyEmail based on result
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error checking verify token:", error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Not in an iframe, enable autofocus on input fields.
|
||||
useEffect(() => {
|
||||
@@ -63,6 +93,13 @@ export default function LinkSurvey({ survey, product }: LinkSurveyProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldRenderVerifyEmail) {
|
||||
if (!isTokenValid) {
|
||||
return <VerifyEmail survey={survey} isErrorComponent={true} />;
|
||||
}
|
||||
return <VerifyEmail survey={survey} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
||||
118
apps/web/app/s/[surveyId]/VerifyEmail.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { EnvelopeIcon } from "@heroicons/react/24/solid";
|
||||
import { Button, Input } from "@formbricks/ui";
|
||||
import { Toaster, toast } from "react-hot-toast";
|
||||
import { sendLinkSurveyEmailAction } from "@/app/s/[surveyId]/actions";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
export default function VerifyEmail({
|
||||
survey,
|
||||
isErrorComponent,
|
||||
}: {
|
||||
survey: TSurvey;
|
||||
isErrorComponent?: boolean;
|
||||
}) {
|
||||
const [showPreviewQuestions, setShowPreviewQuestions] = useState(false);
|
||||
const [email, setEmail] = useState<string | null>(null);
|
||||
const [emailSent, setEmailSent] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const validateEmail = (inputEmail) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inputEmail);
|
||||
|
||||
const submitEmail = async (email) => {
|
||||
setIsLoading(true);
|
||||
if (!validateEmail(email)) {
|
||||
toast.error("Please enter a valid email");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
surveyId: survey.id,
|
||||
email: email,
|
||||
surveyData: survey.verifyEmail,
|
||||
};
|
||||
try {
|
||||
await sendLinkSurveyEmailAction(data);
|
||||
setEmailSent(true);
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handlePreviewClick = () => {
|
||||
setShowPreviewQuestions(!showPreviewQuestions);
|
||||
};
|
||||
|
||||
const handleGoBackClick = () => {
|
||||
setShowPreviewQuestions(false);
|
||||
setEmailSent(false);
|
||||
};
|
||||
|
||||
if (isErrorComponent) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50">
|
||||
<span className="h-24 w-24 rounded-full bg-slate-300 p-6 text-5xl">🤔</span>
|
||||
<p className="mt-8 text-4xl font-bold">This looks fishy.</p>
|
||||
<p className="mt-4 cursor-pointer text-sm text-slate-400" onClick={handleGoBackClick}>
|
||||
Please try again with the original link
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50">
|
||||
<Toaster />
|
||||
{!emailSent && !showPreviewQuestions && (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<EnvelopeIcon className="h-24 w-24 rounded-full bg-slate-300 p-6 text-white" />
|
||||
<p className="mt-8 text-4xl font-bold">Verify your email to respond.</p>
|
||||
<p className="mt-2 text-slate-400">To respond to this survey please verify your email.</p>
|
||||
<div className="mt-6 flex w-full space-x-2">
|
||||
<Input
|
||||
type="string"
|
||||
className="h-full"
|
||||
placeholder="user@gmail.com"
|
||||
value={email || ""}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<Button variant="darkCTA" onClick={() => submitEmail(email)} loading={isLoading}>
|
||||
Verify
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-6 cursor-pointer text-sm text-slate-400" onClick={handlePreviewClick}>
|
||||
Just curious? Preview survey questions.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!emailSent && showPreviewQuestions && (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-4xl font-bold">Question Preview</p>
|
||||
<div className="mt-4 flex w-full flex-col justify-center rounded-lg border border-slate-200 p-8">
|
||||
{survey.questions.map((question, index) => (
|
||||
<p key={index} className="my-1">{`${index + 1}. ${question.headline}`}</p>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-6 cursor-pointer text-sm text-slate-400" onClick={handlePreviewClick}>
|
||||
Want to respond? Verify email.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{emailSent && (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="mt-8 text-4xl font-bold">Survey sent successfully</h1>
|
||||
<p className="mt-4 text-center text-slate-400">
|
||||
We sent an email to <span className="font-semibold italic">{email}</span>. Please click the link
|
||||
in the email to take your survey.
|
||||
</p>
|
||||
<p className="mt-6 cursor-pointer text-sm text-slate-400" onClick={handleGoBackClick}>
|
||||
Go Back
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
apps/web/app/s/[surveyId]/actions.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
"use server";
|
||||
|
||||
interface LinkSurveyEmailData {
|
||||
surveyId: string;
|
||||
email: string;
|
||||
surveyData?: {
|
||||
name?: string;
|
||||
subheading?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
import { sendLinkSurveyToVerifiedEmail } from "@/lib/email";
|
||||
import { verifyTokenForLinkSurvey } from "@/lib/jwt";
|
||||
|
||||
export async function sendLinkSurveyEmailAction(data: LinkSurveyEmailData) {
|
||||
if (!data.surveyData) {
|
||||
throw new Error("No survey data provided");
|
||||
}
|
||||
return await sendLinkSurveyToVerifiedEmail(data);
|
||||
}
|
||||
export async function verifyTokenAction(token: string, surveyId: string): Promise<boolean> {
|
||||
return await verifyTokenForLinkSurvey(token, surveyId);
|
||||
}
|
||||
BIN
apps/web/images/n8n.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
@@ -4,7 +4,7 @@ import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { Question } from "@formbricks/types/questions";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { withEmailTemplate } from "./email-template";
|
||||
import { createInviteToken, createToken } from "./jwt";
|
||||
import { createInviteToken, createToken, createTokenForLinkSurvey } from "./jwt";
|
||||
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
@@ -56,6 +56,26 @@ export const sendVerificationEmail = async (user) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const sendLinkSurveyToVerifiedEmail = async (data) => {
|
||||
const surveyId = data.surveyId;
|
||||
const email = data.email;
|
||||
const surveyData = data.surveyData;
|
||||
const token = createTokenForLinkSurvey(surveyId, email);
|
||||
const surveyLink = `${WEBAPP_URL}/s/${surveyId}?verify=${encodeURIComponent(token)}`;
|
||||
await sendEmail({
|
||||
to: data.email,
|
||||
subject: "Your Formbricks Survey",
|
||||
html: withEmailTemplate(`<h1>Hey 👋</h1>
|
||||
Thanks for validating your email. Here is your Survey.<br/><br/>
|
||||
<strong>${surveyData.name}</strong>
|
||||
<p>${surveyData.subheading}</p>
|
||||
<a class="button" href="${surveyLink}">Take survey</a><br/>
|
||||
<br/>
|
||||
All the best,<br/>
|
||||
Your Formbricks Team 🤍`),
|
||||
});
|
||||
};
|
||||
|
||||
export const sendForgotPasswordEmail = async (user) => {
|
||||
const token = createToken(user.id, user.email, {
|
||||
expiresIn: "1d",
|
||||
|
||||
@@ -5,6 +5,21 @@ import { env } from "@/env.mjs";
|
||||
export function createToken(userId, userEmail, options = {}) {
|
||||
return jwt.sign({ id: userId }, env.NEXTAUTH_SECRET + userEmail, options);
|
||||
}
|
||||
export function createTokenForLinkSurvey(surveyId, userEmail) {
|
||||
return jwt.sign({ email: userEmail }, env.NEXTAUTH_SECRET + surveyId);
|
||||
}
|
||||
|
||||
export function verifyTokenForLinkSurvey(token, surveyId): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
jwt.verify(token, env.NEXTAUTH_SECRET + surveyId, function (err) {
|
||||
if (err) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyToken(token, userEmail = "") {
|
||||
if (!userEmail) {
|
||||
|
||||
@@ -146,6 +146,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -97,6 +97,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
if (!body.surveyClosedMessage) {
|
||||
body.surveyClosedMessage = prismaClient.JsonNull;
|
||||
}
|
||||
|
||||
if (!body.verifyEmail) {
|
||||
body.verifyEmail = prismaClient.JsonNull;
|
||||
}
|
||||
}
|
||||
|
||||
if (body.triggers) {
|
||||
@@ -226,6 +230,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
data.surveyClosedMessage = prismaClient.JsonNull;
|
||||
}
|
||||
|
||||
if (data.verifyEmail === null) {
|
||||
data.verifyEmail = prismaClient.JsonNull;
|
||||
}
|
||||
|
||||
const prismaRes = await prisma.survey.update({
|
||||
where: { id: surveyId },
|
||||
data,
|
||||
|
||||
22
package.json
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "turbo",
|
||||
"name": "formbricks",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
@@ -19,19 +19,35 @@
|
||||
"db:push": "turbo run db:push",
|
||||
"go": "turbo run go --concurrency 16",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"pre-commit": "lint-staged",
|
||||
"start": "turbo run start --parallel",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"generate": "turbo run generate",
|
||||
"lint": "turbo run lint",
|
||||
"release": "turbo run build --filter=js... && changeset publish",
|
||||
"test": "turbo run test"
|
||||
"release": "turbo run build --filter=js... && turbo run build --filter=n8n-node... && changeset publish",
|
||||
"test": "turbo run test",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"rimraf": "^5.0.1",
|
||||
"tsx": "^3.12.7",
|
||||
"turbo": "latest"
|
||||
},
|
||||
"lint-staged": {
|
||||
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.json": [
|
||||
"prettier --write"
|
||||
],
|
||||
"packages/database/schema.prisma": [
|
||||
"prisma format"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses";
|
||||
import { TResponsePersonAttributes, TResponseData, TResponseMeta } from "@formbricks/types/v1/responses";
|
||||
import { TSurveyClosedMessage, TSurveyQuestions, TSurveyThankYouCard } from "@formbricks/types/v1/surveys";
|
||||
import {
|
||||
TSurveyClosedMessage,
|
||||
TSurveyQuestions,
|
||||
TSurveyThankYouCard,
|
||||
TSurveyVerifyEmail,
|
||||
} from "@formbricks/types/v1/surveys";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/v1/users";
|
||||
|
||||
declare global {
|
||||
@@ -13,6 +18,7 @@ declare global {
|
||||
export type SurveyQuestions = TSurveyQuestions;
|
||||
export type SurveyThankYouCard = TSurveyThankYouCard;
|
||||
export type SurveyClosedMessage = TSurveyClosedMessage;
|
||||
export type SurveyVerifyEmail = TSurveyVerifyEmail;
|
||||
export type UserNotificationSettings = TUserNotificationSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Survey" ADD COLUMN "verifyEmail" JSONB;
|
||||
@@ -220,11 +220,9 @@ model Survey {
|
||||
environmentId String
|
||||
status SurveyStatus @default(draft)
|
||||
/// @zod.custom(imports.ZSurveyQuestions)
|
||||
/// @zod.custom(imports.ZSurveyQuestions)
|
||||
/// [SurveyQuestions]
|
||||
questions Json @default("[]")
|
||||
/// @zod.custom(imports.ZSurveyThankYouCard)
|
||||
/// @zod.custom(imports.ZSurveyThankYouCard)
|
||||
/// [SurveyThankYouCard]
|
||||
thankYouCard Json @default("{\"enabled\": false}")
|
||||
responses Response[]
|
||||
@@ -240,6 +238,9 @@ model Survey {
|
||||
/// @zod.custom(imports.ZSurveyClosedMessage)
|
||||
/// [SurveyClosedMessage]
|
||||
surveyClosedMessage Json?
|
||||
/// @zod.custom(imports.ZSurveyVerifyEmail)
|
||||
/// [SurveyVerifyEmail]
|
||||
verifyEmail Json?
|
||||
}
|
||||
|
||||
model Event {
|
||||
|
||||
@@ -5,6 +5,11 @@ export { ZActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses";
|
||||
|
||||
export { ZResponseData, ZResponsePersonAttributes, ZResponseMeta } from "@formbricks/types/v1/responses";
|
||||
|
||||
export { ZSurveyQuestions, ZSurveyThankYouCard, ZSurveyClosedMessage } from "@formbricks/types/v1/surveys";
|
||||
export {
|
||||
ZSurveyQuestions,
|
||||
ZSurveyThankYouCard,
|
||||
ZSurveyClosedMessage,
|
||||
ZSurveyVerifyEmail,
|
||||
} from "@formbricks/types/v1/surveys";
|
||||
|
||||
export { ZUserNotificationSettings } from "@formbricks/types/v1/users";
|
||||
|
||||
@@ -77,6 +77,7 @@ export const selectSurvey = {
|
||||
closeOnDate: true,
|
||||
delay: true,
|
||||
autoComplete: true,
|
||||
verifyEmail: true,
|
||||
redirectUrl: true,
|
||||
triggers: {
|
||||
select: {
|
||||
@@ -152,6 +153,7 @@ export const getSurveyWithAnalytics = cache(
|
||||
const survey = ZSurveyWithAnalytics.parse(transformedSurvey);
|
||||
return survey;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
|
||||
}
|
||||
|
||||
53
packages/n8n-node/.eslintrc.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
parser: "@typescript-eslint/parser",
|
||||
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
sourceType: "module",
|
||||
extraFileExtensions: [".json"],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
|
||||
ignorePatterns: [".eslintrc.js", "**/*.js", "**/node_modules/**", "**/dist/**"],
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ["package.json"],
|
||||
plugins: ["eslint-plugin-n8n-nodes-base"],
|
||||
extends: ["plugin:n8n-nodes-base/community"],
|
||||
rules: {
|
||||
"n8n-nodes-base/community-package-json-name-still-default": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["./credentials/**/*.ts"],
|
||||
plugins: ["eslint-plugin-n8n-nodes-base"],
|
||||
extends: ["plugin:n8n-nodes-base/credentials"],
|
||||
rules: {
|
||||
"n8n-nodes-base/cred-class-field-documentation-url-missing": "off",
|
||||
"n8n-nodes-base/cred-class-field-documentation-url-miscased": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["./nodes/**/*.ts"],
|
||||
plugins: ["eslint-plugin-n8n-nodes-base"],
|
||||
extends: ["plugin:n8n-nodes-base/nodes"],
|
||||
rules: {
|
||||
"n8n-nodes-base/node-execute-block-missing-continue-on-fail": "off",
|
||||
"n8n-nodes-base/node-resource-description-filename-against-convention": "off",
|
||||
"n8n-nodes-base/node-param-fixed-collection-type-unsorted-items": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
16
packages/n8n-node/.eslintrc.prepublish.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: "./.eslintrc.js",
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ["package.json"],
|
||||
plugins: ["eslint-plugin-n8n-nodes-base"],
|
||||
rules: {
|
||||
"n8n-nodes-base/community-package-json-name-still-default": "error",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
8
packages/n8n-node/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.tmp
|
||||
tmp
|
||||
dist
|
||||
npm-debug.log*
|
||||
yarn.lock
|
||||
.vscode/launch.json
|
||||
2
packages/n8n-node/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
*.tsbuildinfo
|
||||
7
packages/n8n-node/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# @formbricks/n8n-nodes-formbricks
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- aa79c4c3: Add new n8n Integration for Formbricks; huge thanks to @PratikAwaik
|
||||
19
packages/n8n-node/LICENSE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2022 n8n
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
39
packages/n8n-node/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# n8n-nodes-formbricks
|
||||
|
||||
This is an n8n community node. It lets you use Formbricks in your n8n workflows.
|
||||
|
||||
Formbricks is an open-source experience management solution that lets you understand what customers think & feel about your product by running highly targeted surveys inside your product.
|
||||
|
||||
[n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform.
|
||||
|
||||
[Installation](#installation)
|
||||
[Operations](#operations)
|
||||
[Credentials](#credentials) <!-- delete if no auth needed -->
|
||||
[Compatibility](#compatibility)
|
||||
[Usage](#usage) <!-- delete if not using this section -->
|
||||
[Resources](#resources)
|
||||
|
||||
## Installation
|
||||
|
||||
Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation.
|
||||
|
||||
## Operations
|
||||
|
||||
Run workflows on new responses you receive for your surveys.
|
||||
|
||||
## Credentials
|
||||
|
||||
You can use this integration in Formbricks Cloud as well as self-hosted instances of Formbricks. You only need a Formbricks API Key for this. Please check out the [Formbricks Docs]() for more information.
|
||||
|
||||
## Compatibility
|
||||
|
||||
This package was developed & tested with n8n > 1.4.0.
|
||||
|
||||
## Usage
|
||||
|
||||
Please check out the [Formbricks Docs](https://formbricks.com/docs/api/api-key-setup) for more information on how to use the integration.
|
||||
|
||||
## Resources
|
||||
|
||||
- [n8n community nodes documentation](https://docs.n8n.io/integrations/community-nodes/)
|
||||
- [Formbricks Docs](https://formbricks.com/docs/integrations/n8n)
|
||||
40
packages/n8n-node/credentials/FormbricksApi.credentials.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from "n8n-workflow";
|
||||
|
||||
export class FormbricksApi implements ICredentialType {
|
||||
name = "formbricksApi";
|
||||
displayName = "Formbricks API";
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: "Host",
|
||||
name: "host",
|
||||
description:
|
||||
'The address of your Formbricks instance. For Formbricks Cloud this is "https://app.formbricks.com". If you are hosting Formbricks yourself, it\'s the address where you can reach your instance.',
|
||||
type: "string",
|
||||
default: "https://app.formbricks.com",
|
||||
},
|
||||
{
|
||||
displayName: "API Key",
|
||||
name: "apiKey",
|
||||
description:
|
||||
'Your Formbricks API-Key. You can create a new API-Key in the Product Settings. Please read our <a href="https://formbricks.com/docs/api/api-key-setup">API Key Docs</a> for more details.',
|
||||
type: "string",
|
||||
typeOptions: { password: true },
|
||||
default: "",
|
||||
},
|
||||
];
|
||||
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
type: "generic",
|
||||
properties: {
|
||||
headers: {
|
||||
"x-Api-Key": "={{$credentials.apiKey}}",
|
||||
},
|
||||
},
|
||||
};
|
||||
test: ICredentialTestRequest | undefined = {
|
||||
request: {
|
||||
baseURL: "={{$credentials.host}}/api/v1",
|
||||
url: "=/me",
|
||||
},
|
||||
};
|
||||
}
|
||||
16
packages/n8n-node/gulpfile.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const path = require("path");
|
||||
const { task, src, dest } = require("gulp");
|
||||
|
||||
task("build:icons", copyIcons);
|
||||
|
||||
function copyIcons() {
|
||||
const nodeSource = path.resolve("nodes", "**", "*.{png,svg}");
|
||||
const nodeDestination = path.resolve("dist", "nodes");
|
||||
|
||||
src(nodeSource).pipe(dest(nodeDestination));
|
||||
|
||||
const credSource = path.resolve("credentials", "**", "*.{png,svg}");
|
||||
const credDestination = path.resolve("dist", "credentials");
|
||||
|
||||
return src(credSource).pipe(dest(credDestination));
|
||||
}
|
||||
0
packages/n8n-node/index.js
Normal file
18
packages/n8n-node/nodes/Formbricks/Formbricks.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.Formbricks",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/formbricks"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.formbricks/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
155
packages/n8n-node/nodes/Formbricks/Formbricks.node.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { INodeType, INodeTypeDescription, IWebhookFunctions, IWebhookResponseData } from "n8n-workflow";
|
||||
|
||||
import { apiRequest, getSurveys } from "./GenericFunctions";
|
||||
import { IHookFunctions } from "n8n-core";
|
||||
|
||||
export class Formbricks implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: "Formbricks",
|
||||
name: "formbricks",
|
||||
icon: "file:formbricks.svg",
|
||||
group: ["trigger"],
|
||||
version: 1,
|
||||
subtitle: '=Surveys: {{$parameter["surveyIds"]}}',
|
||||
description: "Open Source Surveys & Experience Management Solution",
|
||||
defaults: {
|
||||
name: "Formbricks",
|
||||
},
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
|
||||
inputs: [],
|
||||
outputs: ["main"],
|
||||
credentials: [
|
||||
{
|
||||
name: "formbricksApi",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: "default",
|
||||
httpMethod: "POST",
|
||||
responseMode: "onReceived",
|
||||
path: "webhook",
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: "Events",
|
||||
name: "events",
|
||||
type: "multiOptions",
|
||||
options: [
|
||||
{
|
||||
name: "Response Created",
|
||||
value: "responseCreated",
|
||||
description:
|
||||
"Triggers when a new response is created for a survey. Normally triggered after the first question was answered.",
|
||||
},
|
||||
{
|
||||
name: "Response Updated",
|
||||
value: "responseUpdated",
|
||||
description: "Triggers when a response is updated within a survey",
|
||||
},
|
||||
{
|
||||
name: "Response Finished",
|
||||
value: "responseFinished",
|
||||
description: "Triggers when a response is marked as finished",
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
|
||||
displayName: "Survey",
|
||||
name: "surveyIds",
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
|
||||
description:
|
||||
'Survey which should trigger workflow. Only trigger this node for a specific survey within the environment. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
type: "multiOptions",
|
||||
typeOptions: {
|
||||
loadOptionsMethod: "getSurveys",
|
||||
},
|
||||
options: [],
|
||||
default: [],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
methods = {
|
||||
loadOptions: {
|
||||
getSurveys,
|
||||
},
|
||||
};
|
||||
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData("node");
|
||||
const webhookUrl = this.getNodeWebhookUrl("default");
|
||||
const surveyIds = this.getNodeParameter("surveyIds") as Array<string>;
|
||||
|
||||
const endpoint = "/webhooks";
|
||||
|
||||
try {
|
||||
const response = await apiRequest.call(this, "GET", endpoint, {});
|
||||
for (const webhook of response.data) {
|
||||
for (const surveyId of webhook.surveyIds) {
|
||||
if (surveyIds.includes(surveyId) && webhook.url === webhookUrl) {
|
||||
webhookData.webhookId = webhook.id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData("node");
|
||||
const webhookUrl = this.getNodeWebhookUrl("default");
|
||||
const surveyIds = this.getNodeParameter("surveyIds") as Array<string>;
|
||||
const events = this.getNodeParameter("events");
|
||||
|
||||
const body = {
|
||||
url: webhookUrl,
|
||||
triggers: events,
|
||||
surveyIds: surveyIds,
|
||||
};
|
||||
const endpoint = "/webhooks";
|
||||
|
||||
try {
|
||||
const response = await apiRequest.call(this, "POST", endpoint, body);
|
||||
webhookData.webhookId = response.data.id;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData("node");
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
const endpoint = `/webhooks/${webhookData.webhookId}`;
|
||||
|
||||
try {
|
||||
await apiRequest.call(this, "DELETE", endpoint, {});
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
// Remove from the static workflow data so that it is clear
|
||||
// that no webhooks are registered anymore
|
||||
delete webhookData.webhookId;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const bodyData = this.getBodyData();
|
||||
// getting bodyData as string, so need to JSON parse it to convert to an object
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray(JSON.parse(bodyData as any))],
|
||||
};
|
||||
}
|
||||
}
|
||||
69
packages/n8n-node/nodes/Formbricks/GenericFunctions.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { IHookFunctions, ILoadOptionsFunctions } from "n8n-core";
|
||||
import {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHttpRequestMethods,
|
||||
IHttpRequestOptions,
|
||||
INodePropertyOptions,
|
||||
JsonObject,
|
||||
NodeApiError,
|
||||
NodeOperationError,
|
||||
} from "n8n-workflow";
|
||||
|
||||
/**
|
||||
* Make an API request to Formbricks
|
||||
*/
|
||||
export async function apiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: object,
|
||||
query: IDataObject = {},
|
||||
option: IDataObject = {}
|
||||
): Promise<any> {
|
||||
const credentials = await this.getCredentials("formbricksApi");
|
||||
|
||||
let options: IHttpRequestOptions = {
|
||||
baseURL: `${credentials.host}/api/v1`,
|
||||
method,
|
||||
body,
|
||||
qs: query,
|
||||
url: resource,
|
||||
headers: {
|
||||
"x-Api-Key": credentials.apiKey,
|
||||
},
|
||||
};
|
||||
|
||||
if (!Object.keys(query).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
options = Object.assign({}, options, option);
|
||||
try {
|
||||
return await this.helpers.httpRequestWithAuthentication.call(this, "formbricksApi", options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the available surveys
|
||||
*/
|
||||
export async function getSurveys(this: ILoadOptionsFunctions): Promise<any> {
|
||||
const endpoint = "/surveys";
|
||||
const responseData = await apiRequest.call(this, "GET", endpoint, {});
|
||||
|
||||
if (!responseData.data) {
|
||||
throw new NodeOperationError(this.getNode(), "No data got returned");
|
||||
}
|
||||
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
for (const data of responseData.data) {
|
||||
returnData.push({
|
||||
name: data.name,
|
||||
value: data.id,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
75
packages/n8n-node/nodes/Formbricks/formbricks.svg
Normal file
@@ -0,0 +1,75 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="500" height="500" fill="white"/>
|
||||
<path d="M90 334H218V398.716C218 433.667 189.667 462 154.716 462H153.284C118.333 462 90 433.667 90 398.716V334Z" fill="url(#paint0_linear_3927_2011)"/>
|
||||
<path d="M90 186H346.716C381.667 186 410 214.334 410 249.284V250.717C410 285.667 381.667 314 346.716 314H90V186Z" fill="url(#paint1_linear_3927_2011)"/>
|
||||
<path d="M90 101.284C90 66.333 118.333 38 153.284 38H346.716C381.667 38 410 66.333 410 101.284V102.716C410 137.667 381.667 166 346.716 166H90V101.284Z" fill="url(#paint2_linear_3927_2011)"/>
|
||||
<mask id="mask0_3927_2011" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="90" y="38" width="320" height="424">
|
||||
<path d="M90 334H218V398.716C218 433.667 189.667 462 154.716 462H153.284C118.333 462 90 433.667 90 398.716V334Z" fill="url(#paint3_linear_3927_2011)"/>
|
||||
<path d="M90 186H346.716C381.667 186 410 214.334 410 249.284V250.717C410 285.667 381.667 314 346.716 314H90V186Z" fill="url(#paint4_linear_3927_2011)"/>
|
||||
<path d="M90 101.284C90 66.333 118.333 38 153.284 38H346.716C381.667 38 410 66.333 410 101.284V102.716C410 137.667 381.667 166 346.716 166H90V101.284Z" fill="url(#paint5_linear_3927_2011)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3927_2011)">
|
||||
<g filter="url(#filter0_d_3927_2011)">
|
||||
<mask id="mask1_3927_2011" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="90" y="38" width="320" height="424">
|
||||
<path d="M90 334H218V398.716C218 433.667 189.667 462 154.716 462H153.284C118.333 462 90 433.667 90 398.716V334Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M90 101.284C90 66.333 118.333 38 153.284 38H346.716C381.667 38 410 66.333 410 101.284V102.716C410 137.667 381.667 166 346.716 166H90V101.284Z" fill="black" fill-opacity="0.1"/>
|
||||
<path d="M90 186H346.716C381.667 186 410 214.334 410 249.284V250.717C410 285.667 381.667 314 346.716 314H90V186Z" fill="black" fill-opacity="0.1"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_3927_2011)">
|
||||
<path d="M96.7149 -72.2506C146.856 -121.187 273.999 -72.2506 273.999 -72.2506H96.7149C84.3998 -60.2313 76.7298 -42.3079 76.7298 -16.3051C76.7298 115.567 219.58 163.522 219.58 255.433C219.58 345.407 82.6888 400.915 76.9176 523.174H273.999C273.999 523.174 76.7298 659.043 76.7298 531.166C76.7298 528.471 76.7933 525.807 76.9176 523.174H-10.0007L7.00526 -72.2506H96.7149Z" fill="black" fill-opacity="0.1"/>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_3927_2011)">
|
||||
<circle cx="50.0005" cy="406" r="120" fill="#00C4B8"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_3927_2011)">
|
||||
<circle cx="50.0005" cy="102" r="120" fill="#00C4B8"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_3927_2011" x="83.6716" y="0.0298538" width="259.94" height="499.94" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="31.6418"/>
|
||||
<feGaussianBlur stdDeviation="18.9851"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3927_2011"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3927_2011" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_3927_2011" x="-133.283" y="222.717" width="366.567" height="366.567" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="31.6418" result="effect1_foregroundBlur_3927_2011"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_3927_2011" x="-133.283" y="-81.2831" width="366.567" height="366.567" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="31.6418" result="effect1_foregroundBlur_3927_2011"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_3927_2011" x1="218.557" y1="395.681" x2="89.989" y2="396.2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00E6CA"/>
|
||||
<stop offset="1" stop-color="#00C4B8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3927_2011" x1="411.391" y1="247.682" x2="90" y2="250.928" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00E6CA"/>
|
||||
<stop offset="1" stop-color="#00C4B8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_3927_2011" x1="411.391" y1="99.6812" x2="90" y2="102.928" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00E6CA"/>
|
||||
<stop offset="1" stop-color="#00C4B8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_3927_2011" x1="218.557" y1="395.681" x2="89.989" y2="396.2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00FFE1"/>
|
||||
<stop offset="1" stop-color="#01E0C6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_3927_2011" x1="411.391" y1="247.682" x2="90" y2="250.928" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00FFE1"/>
|
||||
<stop offset="1" stop-color="#01E0C6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_3927_2011" x1="411.391" y1="99.6812" x2="90" y2="102.928" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00FFE1"/>
|
||||
<stop offset="1" stop-color="#01E0C6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
54
packages/n8n-node/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@formbricks/n8n-nodes-formbricks",
|
||||
"version": "0.2.0",
|
||||
"description": "A n8n node to connect Formbricks and send survey data to hundreds of other apps.",
|
||||
"keywords": [
|
||||
"n8n-community-node-package",
|
||||
"n8n",
|
||||
"trigger",
|
||||
"Formbricks",
|
||||
"n8n-node",
|
||||
"surveys",
|
||||
"experience management"
|
||||
],
|
||||
"license": "MIT",
|
||||
"homepage": "https://formbricks.com",
|
||||
"author": {
|
||||
"name": "PratikAwaik",
|
||||
"email": "pratikawaik125@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/formbricks/formbricks"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc && gulp build:icons",
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"dev": "tsc --watch",
|
||||
"format": "prettier nodes credentials --write",
|
||||
"lint": "eslint nodes credentials package.json",
|
||||
"lintfix": "eslint nodes credentials package.json --fix",
|
||||
"prepublishOnly": "pnpm run build && pnpm run lint -c .eslintrc.prepublish.js nodes credentials package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"n8n": {
|
||||
"n8nNodesApiVersion": 1,
|
||||
"credentials": [
|
||||
"dist/credentials/FormbricksApi.credentials.js"
|
||||
],
|
||||
"nodes": [
|
||||
"dist/nodes/Formbricks/Formbricks.node.js"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"@typescript-eslint/parser": "~5.45",
|
||||
"eslint-plugin-n8n-nodes-base": "^1.11.0",
|
||||
"gulp": "^4.0.2",
|
||||
"n8n-core": "^1.1.1",
|
||||
"n8n-workflow": "^1.1.1"
|
||||
}
|
||||
}
|
||||
25
packages/n8n-node/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2019",
|
||||
"lib": ["es2019", "es2020", "es2022.error"],
|
||||
"removeComments": true,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"preserveConstEnums": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"incremental": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist/"
|
||||
},
|
||||
"include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"]
|
||||
}
|
||||