Compare commits

..

9 Commits

Author SHA1 Message Date
Matthias Nannt
f88194f770 chore: update docs redirects 2025-02-13 22:20:10 +01:00
Matthias Nannt
0ebbc67dbe update redirects 2025-02-13 21:19:51 +01:00
Matthias Nannt
cb2a73d569 feat: new documentation 2025-02-13 21:11:46 +01:00
Piyush Gupta
22e8a137ef fix: date question accessibility (#4698)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
2025-02-12 10:47:54 +00:00
github-actions[bot]
a9fe05d64a chore: bump version to v3.1.5 (#4729)
Co-authored-by: GitHub Actions <github-actions@github.com>
2025-02-12 09:50:13 +01:00
Yannick Torrès
5219065b8e fix: fr-FR translations (#4667)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-02-11 05:35:34 +00:00
Dhruwang Jariwala
cb8497229d fix: UI tweaks (#4721) 2025-02-10 04:10:01 +00:00
Dhruwang Jariwala
25b8920d20 docs: basic docs for kubernetes (#4669) 2025-02-08 07:15:00 +00:00
Dhruwang Jariwala
9203db88ab fix: z index issue (#4723) 2025-02-07 10:46:26 +00:00
529 changed files with 20505 additions and 209 deletions

View File

@@ -1,3 +1,5 @@
pnpm lint-staged
pnpm tolgee-pull || true
git add packages/lib/messages/*.json
pnpm tolgee-pull || true
echo "{\"branchName\": \"main\"}" > ../branch.json
git add branch.json packages/lib/messages/*.json

View File

@@ -1,10 +1,10 @@
"use client";
import { Button } from "@/components/button";
import { LoadingSpinner } from "@/components/icons/loading-spinner";
import { useTheme } from "next-themes";
import { useState } from "react";
import { RedocStandalone } from "redoc";
import { LoadingSpinner } from "@/components/icons/loading-spinner";
import { Button } from "@/components/button";
import "./style.css";
export function ApiDocs() {
@@ -61,7 +61,13 @@ export function ApiDocs() {
<Button href="/developer-docs/rest-api" arrow="left" className="mb-4 mt-8">
Back to docs
</Button>
<RedocStandalone specUrl="/docs/openapi.yaml" onLoaded={() => { setLoading(false); }} options={redocTheme} />
<RedocStandalone
specUrl="/docs/openapi.yaml"
onLoaded={() => {
setLoading(false);
}}
options={redocTheme}
/>
{loading ? <LoadingSpinner /> : null}
</div>
);

View File

@@ -1,9 +1,9 @@
import Image from "next/image";
import { Button } from "@/components/button";
import logoHtml from "@/images/frameworks/html5.svg";
import logoNextjs from "@/images/frameworks/nextjs.svg";
import logoReactJs from "@/images/frameworks/reactjs.svg";
import logoVueJs from "@/images/frameworks/vuejs.svg";
import Image from "next/image";
const libraries = [
{

View File

@@ -18,25 +18,27 @@ const jost = Jost({ subsets: ["latin"] });
async function RootLayout({ children }: { children: React.ReactNode }) {
const pages = await glob("**/*.mdx", { cwd: "src/app" });
const allSectionsEntries: [string, Section[]][] = (await Promise.all(
const allSectionsEntries: [string, Section[]][] = await Promise.all(
pages.map(async (filename) => [
`/${filename.replace(/(?:^|\/)page\.mdx$/, "")}`,
(await import(`./${filename}`) as { sections: Section[] }).sections,
((await import(`./${filename}`)) as { sections: Section[] }).sections,
])
));
);
const allSections = Object.fromEntries(allSectionsEntries);
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<head>
{process.env.NEXT_PUBLIC_LAYER_API_KEY ? <Script
strategy="afterInteractive"
src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js"
primary-color="#00C4B8"
api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY}
walkthrough-enabled="false"
design-style="copilot"
/> : null}
{process.env.NEXT_PUBLIC_LAYER_API_KEY ? (
<Script
strategy="afterInteractive"
src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js"
primary-color="#00C4B8"
api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY}
walkthrough-enabled="false"
design-style="copilot"
/>
) : null}
</head>
<body className={`flex min-h-full bg-white antialiased dark:bg-zinc-900 ${jost.className}`}>
<Providers>

View File

@@ -0,0 +1,158 @@
export const metadata = {
title: "Kubernetes Deployment",
description: "Deploy Formbricks on a Kubernetes cluster using Helm.",
};
# Deploying Formbricks on Kubernetes
This guide explains how to deploy Formbricks on a **Kubernetes cluster** using **Helm**. It assumes that:
- You **already have a Kubernetes cluster** running (e.g., DigitalOcean, GKE, AWS, Minikube).
- An **Ingress controller** (e.g., Traefik, Nginx) is configured.
- You have **Helm installed** on your local machine.
---
## 🚀 **Step 1: Install Formbricks with Helm**
### **1⃣ Clone the Formbricks Helm Chart**
```sh
git clone https://github.com/formbricks/formbricks.git
cd formbricks/helm-chart
```
### **2⃣ Deploy Formbricks**
```sh
helm install my-formbricks ./ \
--namespace formbricks \
--create-namespace \
--set replicaCount=2
```
## 🎯 **Step 2: Verify and Access Formbricks**
### **Check the Running Services**
```sh
kubectl get pods -n formbricks
kubectl get svc -n formbricks
kubectl get ingress -n formbricks
```
### **Access Formbricks**
- If running locally with **Minikube**:
```sh
minikube service my-formbricks -n formbricks
```
- If deployed on a **cloud cluster**, visit:
```
https://formbricks.example.com
```
---
## Upgrading Formbricks
This section provides guidance on how to upgrade your Formbricks deployment using Helm, including examples of common upgrade scenarios.
### Upgrade Process
To upgrade your Formbricks deployment when using a local chart (e.g., with Minikube), use:
```bash
# From the helm-chart directory
helm upgrade my-formbricks ./ --namespace formbricks
```
For installations from the Helm repository (typically for production deployments):
```bash
helm repo update
helm upgrade my-formbricks formbricks/formbricks --namespace formbricks
```
### Common Upgrade Scenarios
#### 1. Updating Environment Variables
To update or add new environment variables, use the `--set` flag with the `env` prefix:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set env.SMTP_HOST=new-smtp.example.com \
--set env.SMTP_PORT=587 \
--set env.NEW_CUSTOM_VAR=newvalue
```
This command updates the SMTP host and port, and adds a new custom environment variable.
#### 2. Enabling or Disabling Features
You can enable or disable features by updating their respective values:
```bash
# Disable Redis
helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=false
# Enable Redis
helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=true
```
#### 3. Scaling Resources
To adjust resource allocation:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set resources.limits.cpu=1 \
--set resources.limits.memory=2Gi \
--set resources.requests.cpu=500m \
--set resources.requests.memory=1Gi
```
#### 4. Updating Autoscaling Configuration
To modify autoscaling settings:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set autoscaling.minReplicas=3 \
--set autoscaling.maxReplicas=10 \
--set autoscaling.metrics[0].resource.target.averageUtilization=75
```
#### 5. Changing Database Credentials
To update PostgreSQL database credentials:
To switch from the built-in PostgreSQL to an external database or update the external database credentials:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set postgresql.enabled=false \
--set postgresql.externalUrl="postgresql://newuser:newpassword@external-postgres-host:5432/newdatabase"
```
This command disables the built-in PostgreSQL and configures Formbricks to use an external PostgreSQL database. Make sure your external database is set up and accessible before making this change.
## Full Values Documentation
Below is a comprehensive list of all configurable values in the Formbricks Helm chart:
| Field | Description | Default |
| ----------------------------------------------------------- | ------------------------------------------ | ------------------------------- |
| `image.repository` | Docker image repository for Formbricks | `ghcr.io/formbricks/formbricks` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `image.tag` | Docker image tag | `"2.6.0"` |
| `service.type` | Kubernetes service type | `ClusterIP` |
| `service.port` | Kubernetes service port | `80` |
| `service.targetPort` | Container port to expose | `3000` |
| `resources.limits.cpu` | CPU resource limit | `500m` |
| `resources.limits.memory` | Memory resource limit | `1Gi` |
| `resources.requests.cpu` | Memory resource request | `null` |
| `resources.requests.memory` | Memory resource request | `null` |
| `autoscaling.enabled` | Enable autoscaling | `false` |
| `autoscaling.minReplicas` | Minimum number of replicas | `1` |
| `autoscaling.maxReplicas` | Maximum number of replicas | `5` |
| `autoscaling.metrics[0].type` | Type of metric for autoscaling | `Resource` |
| `autoscaling.metrics[0].resource.name` | Resource name for autoscaling metric | `cpu` |
| `autoscaling.metrics[0].resource.target.type` | Target type for autoscaling | `Utilization` |
| `autoscaling.metrics[0].resource.target.averageUtilization` | Average utilization target for autoscaling | `80`

View File

@@ -30,11 +30,17 @@ type ButtonProps = {
variant?: keyof typeof variantStyles;
arrow?: "left" | "right";
} & (
| React.ComponentPropsWithoutRef<typeof Link>
| (React.ComponentPropsWithoutRef<"button"> & { href?: undefined })
);
| React.ComponentPropsWithoutRef<typeof Link>
| (React.ComponentPropsWithoutRef<"button"> & { href?: undefined })
);
export function Button({ variant = "primary", className, children, arrow, ...props }: ButtonProps): React.JSX.Element {
export function Button({
variant = "primary",
className,
children,
arrow,
...props
}: ButtonProps): React.JSX.Element {
const buttonClassName = clsx(
"inline-flex gap-0.5 justify-center items-center overflow-hidden font-medium transition text-center",
variantStyles[variant],

View File

@@ -1,10 +1,10 @@
"use client";
import { Tag } from "@/components/tag";
import { Tab } from "@headlessui/react";
import clsx from "clsx";
import { Children, createContext, isValidElement, useContext, useEffect, useRef, useState } from "react";
import { create } from "zustand";
import { Tag } from "@/components/tag";
const languageNames: Record<string, string> = {
js: "JavaScript",
@@ -49,7 +49,9 @@ function CopyButton({ code }: { code: string }) {
useEffect(() => {
if (copyCount > 0) {
const timeout = setTimeout(() => { setCopyCount(0); }, 1000);
const timeout = setTimeout(() => {
setCopyCount(0);
}, 1000);
return () => {
clearTimeout(timeout);
};
@@ -98,9 +100,11 @@ function CodePanelHeader({ tag, label }: { tag?: string; label?: string }): Reac
return (
<div className="border-b-white/7.5 bg-white/2.5 dark:bg-white/1 flex h-9 items-center gap-2 border-y border-t-transparent bg-slate-900 px-4 dark:border-b-white/5">
{tag ? <div className="dark flex">
<Tag variant="small">{tag}</Tag>
</div> : null}
{tag ? (
<div className="dark flex">
<Tag variant="small">{tag}</Tag>
</div>
) : null}
{tag && label ? <span className="h-0.5 w-0.5 rounded-full bg-slate-500" /> : null}
{label ? <span className="font-mono text-xs text-slate-400">{label}</span> : null}
</div>
@@ -162,30 +166,34 @@ function CodeGroupHeader({
return (
<div className="flex min-h-[calc(theme(spacing.12)+1px)] flex-wrap items-start gap-x-4 border-b border-slate-700 bg-slate-800 px-4 dark:border-slate-800 dark:bg-transparent">
{title ? <h3 className="mr-auto pt-3 text-xs font-semibold text-white">{title}</h3> : null}
{hasTabs ? <Tab.List className="-mb-px flex gap-4 text-xs font-medium">
{Children.map(children, (child, childIndex) => {
if (isValidElement(child)) {
return (
<Tab
className={clsx(
"ui-not-focus-visible:outline-none border-b py-3 transition",
childIndex === selectedIndex
? "border-teal-500 text-teal-400"
: "border-transparent text-slate-400 hover:text-slate-300"
)}
>
{getPanelTitle(child.props as { title?: string; language?: string })}
</Tab>
);
}
return null;
})}
</Tab.List> : null}
{hasTabs ? (
<Tab.List className="-mb-px flex gap-4 text-xs font-medium">
{Children.map(children, (child, childIndex) => {
if (isValidElement(child)) {
return (
<Tab
className={clsx(
"ui-not-focus-visible:outline-none border-b py-3 transition",
childIndex === selectedIndex
? "border-teal-500 text-teal-400"
: "border-transparent text-slate-400 hover:text-slate-300"
)}>
{getPanelTitle(child.props as { title?: string; language?: string })}
</Tab>
);
}
return null;
})}
</Tab.List>
) : null}
</div>
);
}
function CodeGroupPanels({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodePanel>): React.JSX.Element {
function CodeGroupPanels({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodePanel>): React.JSX.Element {
const hasTabs = Children.count(children) >= 1;
if (hasTabs) {
@@ -264,7 +272,9 @@ const useTabGroupProps = (availableLanguages: string[]) => {
const { positionRef, preventLayoutShift } = usePreventLayoutShift();
const onChange = (index: number) => {
preventLayoutShift(() => { addPreferredLanguage(availableLanguages[index] ?? ""); });
preventLayoutShift(() => {
addPreferredLanguage(availableLanguages[index] ?? "");
});
};
return {
@@ -331,7 +341,10 @@ export function Code({ children, ...props }: React.ComponentPropsWithoutRef<"cod
return <code {...props}>{children}</code>;
}
export function Pre({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodeGroup>): React.ReactNode {
export function Pre({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodeGroup>): React.ReactNode {
const isGrouped = useContext(CodeGroupContext);
if (isGrouped) {

View File

@@ -18,7 +18,9 @@ function CheckIcon(props: React.ComponentPropsWithoutRef<"svg">): React.JSX.Elem
);
}
function FeedbackButton(props: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "className">): React.JSX.Element {
function FeedbackButton(
props: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "className">
): React.JSX.Element {
return (
<button
type="submit"
@@ -49,16 +51,18 @@ const FeedbackForm = forwardRef<
FeedbackForm.displayName = "FeedbackForm";
const FeedbackThanks = forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>((_props, ref): React.JSX.Element => {
return (
<div ref={ref} className="absolute inset-0 flex justify-center md:justify-start">
<div className="flex items-center gap-3 rounded-full bg-teal-50/50 py-1 pl-1.5 pr-3 text-sm text-teal-900 ring-1 ring-inset ring-teal-500/20 dark:bg-teal-500/5 dark:text-teal-200 dark:ring-teal-500/30">
<CheckIcon className="h-5 w-5 flex-none fill-teal-500 stroke-white dark:fill-teal-200/20 dark:stroke-teal-200" />
Thanks for your feedback!
const FeedbackThanks = forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
(_props, ref): React.JSX.Element => {
return (
<div ref={ref} className="absolute inset-0 flex justify-center md:justify-start">
<div className="flex items-center gap-3 rounded-full bg-teal-50/50 py-1 pl-1.5 pr-3 text-sm text-teal-900 ring-1 ring-inset ring-teal-500/20 dark:bg-teal-500/5 dark:text-teal-200 dark:ring-teal-500/30">
<CheckIcon className="h-5 w-5 flex-none fill-teal-500 stroke-white dark:fill-teal-200/20 dark:stroke-teal-200" />
Thanks for your feedback!
</div>
</div>
</div>
);
});
);
}
);
FeedbackThanks.displayName = "FeedbackThanks";

View File

@@ -1,8 +1,8 @@
"use client";
import { navigation } from "@/lib/navigation";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navigation } from "@/lib/navigation";
import { Button } from "./button";
import { DiscordIcon } from "./icons/discord-icon";
import { GithubIcon } from "./icons/github-icon";

View File

@@ -24,18 +24,20 @@ export function GridPattern({
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${patternId})`} />
{squares.length > 0 ? <svg x={x} y={y} className="overflow-visible">
{squares.map(([sqX, sqY]) => (
<rect
strokeWidth="0"
key={`${sqX.toString()}-${sqY.toString()}`}
width={width + 1}
height={height + 1}
x={sqX * width}
y={sqY * height}
/>
))}
</svg> : null}
{squares.length > 0 ? (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([sqX, sqY]) => (
<rect
strokeWidth="0"
key={`${sqX.toString()}-${sqY.toString()}`}
width={width + 1}
height={height + 1}
x={sqX * width}
y={sqY * height}
/>
))}
</svg>
) : null}
</svg>
);
}

View File

@@ -1,16 +1,16 @@
"use client";
import { Logo } from "@/components/logo";
import { Navigation } from "@/components/navigation";
import { Search } from "@/components/search";
import { useIsInsideMobileNavigation, useMobileNavigationStore } from "@/hooks/use-mobile-navigation";
import clsx from "clsx";
import { type MotionStyle, motion, useScroll, useTransform } from "framer-motion";
import Link from "next/link";
import { forwardRef } from "react";
import { Search } from "@/components/search";
import { Logo } from "@/components/logo";
import { Button } from "./button";
import { MobileNavigation } from "./mobile-navigation";
import { ThemeToggle } from "./theme-toggle";
import { Navigation } from "@/components/navigation";
import { useIsInsideMobileNavigation, useMobileNavigationStore } from "@/hooks/use-mobile-navigation";
function TopLevelNavItem({ href, children }: { href: string; children: React.ReactNode }): React.JSX.Element {
return (

View File

@@ -1,11 +1,11 @@
"use client";
import { useInView } from "framer-motion";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { useSectionStore } from "@/components/section-provider";
import { Tag } from "@/components/tag";
import { remToPx } from "@/lib/rem-to-px";
import { useInView } from "framer-motion";
import Link from "next/link";
import { useEffect, useRef } from "react";
function AnchorIcon(props: React.ComponentPropsWithoutRef<"svg">): React.JSX.Element {
return (
@@ -29,14 +29,24 @@ function Eyebrow({ tag, label }: { tag?: string; label?: string }): React.JSX.El
);
}
function Anchor({ id, inView, children }: { id: string; inView: boolean; children: React.ReactNode }): React.JSX.Element {
function Anchor({
id,
inView,
children,
}: {
id: string;
inView: boolean;
children: React.ReactNode;
}): React.JSX.Element {
return (
<Link href={`#${id}`} className="group text-inherit no-underline hover:text-inherit">
{inView ? <div className="absolute ml-[calc(-1*var(--width))] mt-1 hidden w-[var(--width)] opacity-0 transition [--width:calc(1.35rem+0.85px+38%-min(38%,calc(theme(maxWidth.lg)+theme(spacing.2))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]">
<div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-inset ring-zinc-300 transition hover:ring-zinc-500 dark:bg-zinc-800 dark:ring-zinc-700 dark:hover:bg-zinc-700 dark:hover:ring-zinc-600">
<AnchorIcon className="h-5 w-5 stroke-zinc-500 transition dark:stroke-zinc-400 dark:group-hover/anchor:stroke-white" />
{inView ? (
<div className="absolute ml-[calc(-1*var(--width))] mt-1 hidden w-[var(--width)] opacity-0 transition [--width:calc(1.35rem+0.85px+38%-min(38%,calc(theme(maxWidth.lg)+theme(spacing.2))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]">
<div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-inset ring-zinc-300 transition hover:ring-zinc-500 dark:bg-zinc-800 dark:ring-zinc-700 dark:hover:bg-zinc-700 dark:hover:ring-zinc-600">
<AnchorIcon className="h-5 w-5 stroke-zinc-500 transition dark:stroke-zinc-400 dark:group-hover/anchor:stroke-white" />
</div>
</div>
</div> : null}
) : null}
{children}
</Link>
);
@@ -67,7 +77,7 @@ export function Heading<Level extends 2 | 3 | 4>({
const ref = useRef<HTMLHeadingElement>(null);
const registerHeading = useSectionStore((s) => s.registerHeading);
const topMargin = remToPx(-3.5)
const topMargin = remToPx(-3.5);
const inView = useInView(ref, {
margin: `${topMargin}px 0px 0px 0px`,
amount: "all",
@@ -75,18 +85,18 @@ export function Heading<Level extends 2 | 3 | 4>({
useEffect(() => {
if (headingLevel === 2) {
registerHeading({ id: props.id, ref, offsetRem: tag ?? label ? 8 : 6 });
registerHeading({ id: props.id, ref, offsetRem: (tag ?? label) ? 8 : 6 });
} else if (headingLevel === 3) {
registerHeading({ id: props.id, ref, offsetRem: tag ?? label ? 7 : 5 });
registerHeading({ id: props.id, ref, offsetRem: (tag ?? label) ? 7 : 5 });
} else if (headingLevel === 4) {
registerHeading({ id: props.id, ref, offsetRem: tag ?? label ? 6 : 4 });
registerHeading({ id: props.id, ref, offsetRem: (tag ?? label) ? 6 : 4 });
}
}, [label, headingLevel, props.id, registerHeading, tag]);
return (
<>
<Eyebrow tag={tag} label={label} />
<Component ref={ref} className={tag ?? label ? "mt-2 scroll-mt-32" : "scroll-mt-24"} {...props}>
<Component ref={ref} className={(tag ?? label) ? "mt-2 scroll-mt-32" : "scroll-mt-24"} {...props}>
{anchor ? (
<Anchor id={props.id} inView={inView}>
{children}

View File

@@ -12,4 +12,4 @@ export function GithubIcon(props: React.ComponentPropsWithoutRef<"svg">): React.
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg>
);
};
}

View File

@@ -7,4 +7,3 @@ export function LoadingSpinner(props: React.ComponentPropsWithoutRef<"div">): Re
</div>
);
}

View File

@@ -12,4 +12,4 @@ export function TwitterIcon(props: React.ComponentPropsWithoutRef<"svg">): React
<path d="M403.229 0h78.506L310.219 196.04 512 462.799H354.002L230.261 301.007 88.669 462.799h-78.56l183.455-209.683L0 0h161.999l111.856 147.88L403.229 0zm-27.556 415.805h43.505L138.363 44.527h-46.68l283.99 371.278z" />
</svg>
);
};
}

View File

@@ -1,11 +1,11 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Logo } from "@/components/logo";
import { Navigation } from "@/components/navigation";
import { SideNavigation } from "@/components/side-navigation";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Footer } from "./footer";
import { Header } from "./header";
import { type Section, SectionProvider } from "./section-provider";

View File

@@ -1,15 +1,23 @@
import Image from "next/image";
import logoDark from "@/images/logo/logo-dark.svg";
import logoLight from "@/images/logo/logo-light.svg";
import Image from "next/image";
export function Logo({ className }: { className?: string }) {
return (
<div>
<div className="block dark:hidden">
<Image className={className} src={logoLight as string} alt="Formbricks Open source Forms & Surveys Logo" />
<Image
className={className}
src={logoLight as string}
alt="Formbricks Open source Forms & Surveys Logo"
/>
</div>
<div className="hidden dark:block">
<Image className={className} src={logoDark as string} alt="Formbricks Open source Forms & Surveys Logo" />
<Image
className={className}
src={logoDark as string}
alt="Formbricks Open source Forms & Surveys Logo"
/>
</div>
</div>
);

View File

@@ -57,7 +57,7 @@ function MobileNavigationDialog({
if (
link &&
link.pathname + link.search + link.hash ===
window.location.pathname + window.location.search + window.location.hash
window.location.pathname + window.location.search + window.location.hash
) {
close();
}

View File

@@ -1,15 +1,15 @@
"use client";
import { useIsInsideMobileNavigation } from "@/hooks/use-mobile-navigation";
import { navigation } from "@/lib/navigation";
import { remToPx } from "@/lib/rem-to-px";
import clsx from "clsx";
import { AnimatePresence, motion, useIsPresent } from "framer-motion";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { remToPx } from "@/lib/rem-to-px";
import { navigation } from "@/lib/navigation";
import { Button } from "./button";
import { useIsInsideMobileNavigation } from "@/hooks/use-mobile-navigation";
import { useSectionStore } from "./section-provider";
export interface BaseLink {
@@ -79,7 +79,6 @@ function NavLink({
<span className="flex w-full truncate">{children}</span>
</div>
);
}
function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathname: string }) {
@@ -97,7 +96,7 @@ function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathnam
const activePageIndex = group.links.findIndex(
(link) =>
(link.href && pathname.startsWith(link.href)) ??
(link.children?.some((child) => pathname.startsWith(child.href)))
link.children?.some((child) => pathname.startsWith(child.href))
);
const height = isPresent ? Math.max(1, visibleSections.length) * itemHeight : itemHeight;
@@ -116,13 +115,19 @@ function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathnam
);
}
function ActivePageMarker({ group, pathname }: { group: NavGroup; pathname: string }): React.JSX.Element | null {
function ActivePageMarker({
group,
pathname,
}: {
group: NavGroup;
pathname: string;
}): React.JSX.Element | null {
const itemHeight = remToPx(2);
const offset = remToPx(0.25);
const activePageIndex = group.links.findIndex(
(link) =>
(link.href && pathname.startsWith(link.href)) ??
(link.children?.some((child) => pathname.startsWith(child.href)))
link.children?.some((child) => pathname.startsWith(child.href))
);
if (activePageIndex === -1) return null;
const top = offset + activePageIndex * itemHeight;
@@ -228,21 +233,25 @@ function NavigationGroup({
key={link.title}
layout="position"
className="relative"
onClick={() => { setIsActiveGroup(true); }}>
onClick={() => {
setIsActiveGroup(true);
}}>
{link.href ? (
<NavLink
href={link.href}
active={Boolean(pathname.startsWith(link.href))}>
<NavLink href={link.href} active={Boolean(pathname.startsWith(link.href))}>
{link.title}
</NavLink>
) : (
<button onClick={() => { toggleParentTitle(`${group.title}-${link.title}`); }} className="w-full">
<button
onClick={() => {
toggleParentTitle(`${group.title}-${link.title}`);
}}
className="w-full">
<NavLink
href={!isMobile ? link.children?.[0]?.href ?? "" : undefined}
active={
Boolean(isParentOpen(`${group.title}-${link.title}`) &&
link.children?.some((child) => pathname.startsWith(child.href)))
}>
href={!isMobile ? (link.children?.[0]?.href ?? "") : undefined}
active={Boolean(
isParentOpen(`${group.title}-${link.title}`) &&
link.children?.some((child) => pathname.startsWith(child.href))
)}>
<span className="flex w-full justify-between">
{link.title}
{isParentOpen(`${group.title}-${link.title}`) ? (
@@ -255,19 +264,24 @@ function NavigationGroup({
</button>
)}
<AnimatePresence mode="popLayout" initial={false}>
{isActiveGroup && link.children && isParentOpen(`${group.title}-${link.title}`) ? <motion.ul
role="list"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.1 } }}
exit={{ opacity: 0, transition: { duration: 0.15 } }}>
{link.children.map((child) => (
<li key={child.href}>
<NavLink href={child.href} isAnchorLink active={Boolean(pathname.startsWith(child.href))}>
{child.title}
</NavLink>
</li>
))}
</motion.ul> : null}
{isActiveGroup && link.children && isParentOpen(`${group.title}-${link.title}`) ? (
<motion.ul
role="list"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.1 } }}
exit={{ opacity: 0, transition: { duration: 0.15 } }}>
{link.children.map((child) => (
<li key={child.href}>
<NavLink
href={child.href}
isAnchorLink
active={Boolean(pathname.startsWith(child.href))}>
{child.title}
</NavLink>
</li>
))}
</motion.ul>
) : null}
</AnimatePresence>
</motion.li>
))}
@@ -306,7 +320,7 @@ export function Navigation({ isMobile, ...props }: NavigationProps) {
return (
<nav {...props}>
<ul >
<ul>
{navigation.map((group, groupIndex) => (
<NavigationGroup
key={group.title}

View File

@@ -1,13 +1,13 @@
"use client";
import { type MotionValue, motion, useMotionTemplate, useMotionValue } from "framer-motion";
import Link from "next/link";
import { GridPattern } from "@/components/grid-pattern";
import { Heading } from "@/components/heading";
import { ChatBubbleIcon } from "@/components/icons/chat-bubble-icon";
import { EnvelopeIcon } from "@/components/icons/envelope-icon";
import { UserIcon } from "@/components/icons/user-icon";
import { UsersIcon } from "@/components/icons/users-icon";
import { type MotionValue, motion, useMotionTemplate, useMotionValue } from "framer-motion";
import Link from "next/link";
interface TResource {
href: string;

View File

@@ -10,7 +10,8 @@ export function ResponsiveVideo({ src, title }: { src: string; title: string }):
className="absolute left-0 top-0 h-full w-full"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen />
allowFullScreen
/>
</div>
</div>
);

View File

@@ -54,7 +54,11 @@ export function Search(): React.JSX.Element {
useDocSearchKeyboardEvents({
isOpen,
onOpen: isSearchDisabled ? () => { return void 0 } : onOpen,
onOpen: isSearchDisabled
? () => {
return void 0;
}
: onOpen,
onClose,
});
@@ -111,7 +115,6 @@ export function Search(): React.JSX.Element {
};
}, [isLightMode]);
return (
<>
<button

View File

@@ -1,8 +1,8 @@
"use client";
import { remToPx } from "@/lib/rem-to-px";
import { createContext, useContext, useEffect, useLayoutEffect, useState } from "react";
import { type StoreApi, createStore, useStore } from "zustand";
import { remToPx } from "@/lib/rem-to-px";
export interface Section {
id: string;
@@ -31,7 +31,9 @@ const createSectionStore = (sections: Section[]) => {
return createStore<SectionState>()((set) => ({
sections,
visibleSections: [],
setVisibleSections: (visibleSections) => { set((state) => (state.visibleSections.join() === visibleSections.join() ? {} : { visibleSections })); },
setVisibleSections: (visibleSections) => {
set((state) => (state.visibleSections.join() === visibleSections.join() ? {} : { visibleSections }));
},
registerHeading: ({ id, ref, offsetRem }) => {
set((state) => {
return {
@@ -92,7 +94,9 @@ const useVisibleSections = (sectionStore: StoreApi<SectionState>) => {
setVisibleSections(newVisibleSections);
};
const raf = window.requestAnimationFrame(() => { checkVisibleSections(); });
const raf = window.requestAnimationFrame(() => {
checkVisibleSections();
});
window.addEventListener("scroll", checkVisibleSections, { passive: true });
window.addEventListener("resize", checkVisibleSections);
@@ -108,13 +112,7 @@ const SectionStoreContext = createContext<StoreApi<SectionState> | null>(null);
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
export function SectionProvider({
sections,
children,
}: {
sections: Section[];
children: React.ReactNode;
}) {
export function SectionProvider({ sections, children }: { sections: Section[]; children: React.ReactNode }) {
const [sectionStore] = useState(() => createSectionStore(sections));
useVisibleSections(sectionStore);

View File

@@ -48,7 +48,7 @@ export function SideNavigation({ pathname }: { pathname: string }): React.JSX.El
return (
<li
key={heading.text}
className={clsx(`mb-4 text-slate-900 dark:text-white ml-4`, {
className={clsx(`mb-4 ml-4 text-slate-900 dark:text-white`, {
"ml-0": heading.level === 2,
"ml-4": heading.level === 3,
"ml-6": heading.level === 4,

View File

@@ -22,4 +22,3 @@ export default function SurveyEmbed({ surveyUrl }: SurveyEmbedProps): React.JSX.
</div>
);
}

View File

@@ -12,7 +12,8 @@ export function TellaVideo({ tellaVideoIdentifier }: { tellaVideoIdentifier: str
}}
src={`https://www.tella.tv/video/${tellaVideoIdentifier}/embed?b=0&title=0&a=1&loop=0&autoPlay=true&t=0&muted=1&wt=0`}
allowFullScreen
title="Tella Video Help" />
title="Tella Video Help"
/>
</div>
);
}

View File

@@ -35,7 +35,9 @@ export function ThemeToggle(): React.JSX.Element {
type="button"
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
aria-label={mounted ? `Switch to ${otherTheme} theme` : "Toggle theme"}
onClick={() => { setTheme(otherTheme); }}>
onClick={() => {
setTheme(otherTheme);
}}>
<SunIcon className="h-5 w-5 stroke-zinc-900 dark:hidden" />
<MoonIcon className="hidden h-5 w-5 stroke-white dark:block" />
</button>

View File

@@ -14,7 +14,13 @@ export const useMobileNavigationStore = create<{
toggle: () => void;
}>()((set) => ({
isOpen: false,
open: () => { set({ isOpen: true }); },
close: () => { set({ isOpen: false }); },
toggle: () => { set((state) => ({ isOpen: !state.isOpen })); },
open: () => {
set({ isOpen: true });
},
close: () => {
set({ isOpen: false });
},
toggle: () => {
set((state) => ({ isOpen: !state.isOpen }));
},
}));

View File

@@ -24,12 +24,9 @@ export const useTableContentObserver = (setActiveId: (id: string) => void, pathn
useEffect(() => {
const callback = (headings: HeadingElement[]) => {
// Create a map of heading elements, where the key is the heading's ID and the value is the heading element
headingElementsRef.current = headings.reduce(
(map, headingElement) => {
return { ...map, [headingElement.target.id]: headingElement };
},
{}
);
headingElementsRef.current = headings.reduce((map, headingElement) => {
return { ...map, [headingElement.target.id]: headingElement };
}, {});
// Find the visible headings (i.e., headings that are currently intersecting with the viewport)
const visibleHeadings: HeadingElement[] = [];

View File

@@ -146,6 +146,7 @@ export const navigation: NavGroup[] = [
{ title: "License", href: "/self-hosting/license" },
{ title: "Cluster Setup", href: "/self-hosting/cluster-setup" },
{ title: "Rate Limiting", href: "/self-hosting/rate-limiting" },
{ title: "Kubernetes", href: "/self-hosting/kubernetes" },
],
},
{

View File

@@ -1,7 +1,9 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { AlertTriangleIcon, CheckIcon } from "lucide-react";
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
@@ -11,6 +13,7 @@ interface WidgetStatusIndicatorProps {
export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProps) => {
const { t } = useTranslate();
const router = useRouter();
const stati = {
notImplemented: {
icon: AlertTriangleIcon,
@@ -51,6 +54,12 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
</div>
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
{status === "notImplemented" && (
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
<RotateCcwIcon />
{t("environments.project.app-connection.recheck")}
</Button>
)}
</div>
);
};

View File

@@ -60,8 +60,7 @@ export const AIToggle = ({ organization, isOwnerOrManager }: AIToggleProps) => {
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="formbricks-ai-toggle" className="cursor-pointer">
{isAIEnabled ? t("common.disable") : t("common.enable")}{" "}
{t("environments.settings.general.formbricks_ai")}
{t("environments.settings.general.enable_formbricks_ai")}
</Label>
<Switch
id="formbricks-ai-toggle"

View File

@@ -1,5 +1,6 @@
"use client";
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
import { InsightView } from "@/modules/ee/insights/components/insights-view";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
@@ -122,7 +123,11 @@ export const OpenTextSummary = ({
</div>
)}
</TableCell>
<TableCell className="font-medium">{response.value}</TableCell>
<TableCell className="font-medium">
{typeof response.value === "string"
? renderHyperlinkedContent(response.value)
: response.value}
</TableCell>
<TableCell width={120}>
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
</TableCell>

View File

@@ -85,11 +85,11 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
case TSurveyQuestionTypeEnum.Consent:
return <CheckIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.PictureSelection:
return <ImageIcon width={18} className="text-white" />;
return <ImageIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.Matrix:
return <GridIcon width={18} className="text-white" />;
return <GridIcon width={18} height={18} className="text-white" />;
case TSurveyQuestionTypeEnum.Ranking:
return <ListOrderedIcon width={18} className="text-white" />;
return <ListOrderedIcon width={18} height={18} className="text-white" />;
}
case OptionsType.ATTRIBUTES:
return <User width={18} height={18} className="text-white" />;
@@ -115,7 +115,7 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
return <LanguagesIcon width={18} height={18} className="text-white" />;
}
case OptionsType.TAGS:
return <HashIcon width={18} className="text-white" />;
return <HashIcon width={18} height={18} className="text-white" />;
}
};
@@ -133,7 +133,7 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
return (
<div className="flex h-5 w-[12rem] items-center sm:w-4/5">
<span className={clsx("rounded-md p-1", getColor())}>{getIconType()}</span>
<p className="ml-3 truncate text-base text-slate-600">
<p className="ml-3 truncate text-sm text-slate-600">
{typeof label === "string" ? label : getLocalizedValue(label, "default")}
</p>
</div>

View File

@@ -1,3 +1,4 @@
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
import { ArrayResponse } from "@/modules/ui/components/array-response";
import { FileUploadResponse } from "@/modules/ui/components/file-upload-response";
import { PictureSelectionResponse } from "@/modules/ui/components/picture-selection-response";
@@ -173,7 +174,11 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
"ph-no-capture my-1 truncate font-normal text-slate-700",
isExpanded ? "whitespace-pre-line" : "whitespace-nowrap"
)}>
{Array.isArray(responseData) ? handleArray(responseData) : responseData}
{typeof responseData === "string"
? renderHyperlinkedContent(responseData)
: Array.isArray(responseData)
? handleArray(responseData)
: responseData}
</p>
);
}

View File

@@ -0,0 +1,26 @@
// Utility function to render hyperlinked content
export const renderHyperlinkedContent = (data: string): JSX.Element[] => {
// More specific URL pattern
const urlPattern =
/(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)/g;
const parts = data.split(urlPattern);
const isValidUrl = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
return parts.map((part, index) =>
part.match(urlPattern) && isValidUrl(part) ? (
<a key={index} href={part} target="_blank" rel="noopener noreferrer" className="text-blue-500">
{part}
</a>
) : (
<span key={index}>{part}</span>
)
);
};

View File

@@ -44,11 +44,11 @@ export function LanguageIndicator({
});
return (
<div className="absolute right-2 top-2 z-10">
<div className="absolute right-2 top-2">
<button
aria-expanded={showLanguageDropdown}
aria-haspopup="true"
className="flex items-center justify-center rounded-md bg-slate-900 p-1 px-2 text-xs text-white hover:bg-slate-700"
className="relative z-20 flex items-center justify-center rounded-md bg-slate-900 p-1 px-2 text-xs text-white hover:bg-slate-700"
onClick={toggleDropdown}
tabIndex={-1}
type="button">

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "3.1.4",
"version": "3.1.5",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",

32
docs/README.md Normal file
View File

@@ -0,0 +1,32 @@
# Mintlify Starter Kit
Click on `Use this template` to copy the Mintlify starter kit. The starter kit contains examples including
- Guide pages
- Navigation
- Customizations
- API Reference pages
- Use of popular components
### Development
Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command
```
npm i -g mintlify
```
Run the following command at the root of your documentation (where mint.json is)
```
mintlify dev
```
### Publishing Changes
Install our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
#### Troubleshooting
- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
- Page loads as a 404 - Make sure you are running in a folder with `mint.json`

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/client/{environmentId}/displays
---

View File

@@ -0,0 +1,3 @@
---
openapi: put /api/v1/client/{environmentId}/displays/{displayId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/client/{environmentId}/people
---

View File

@@ -0,0 +1,3 @@
---
openapi: put /api/v1/client/{environmentId}/people/{userId}/attributes
---

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/client/{environmentId}/responses
---

View File

@@ -0,0 +1,3 @@
---
openapi: put /api/v1/client/responses/{responseId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /health
---

View File

@@ -0,0 +1,14 @@
---
title: "API v1.0.0"
icon: "code-compare"
---
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.
### API Key Setup
Checkout the [API Key Setup](/development/rest-api) to access the Management APIs with an API Key.
If youve forked the collection and are running it, update the `apiKey` and `environmentId` in the collection variables with your values. We also provide post-run scripts to help auto-assign variables when running scripts.
Need more help? Visit our [Website](https://formbricks.com/) or join our [Discord](https://formbricks.com/discord)!

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/management/action-classes
---

View File

@@ -0,0 +1,3 @@
---
openapi: delete /api/v1/management/action-classes/{actionClassId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/action-classes/{actionClassId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/action-classes
---

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/management/attribute-classes
---

View File

@@ -0,0 +1,3 @@
---
openapi: delete /api/v1/management/attribute-classes/{attributeClassId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/attribute-classes
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/attribute-classes/{attributeClassId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/contact-attribute-keys
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/contact-attribute-keys/{contactAttributeKeyId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/contact-attributes
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/contacts
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/contacts/{contactId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/me
---

View File

@@ -0,0 +1,3 @@
---
openapi: delete /api/v1/management/people/{personId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/people
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/people/{personId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/management/responses
---

View File

@@ -0,0 +1,3 @@
---
openapi: delete /api/v1/management/responses/{responseId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/responses/{responseId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/responses
---

View File

@@ -0,0 +1,3 @@
---
openapi: put /api/v1/management/responses/{responseId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/management/surveys
---

View File

@@ -0,0 +1,3 @@
---
openapi: delete /api/v1/management/surveys/{surveyId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/surveys
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/surveys/{surveyId}/singleUseIds
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/management/surveys/{surveyId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: put /api/v1/management/surveys/{surveyId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: post /api/v1/webhooks
---

View File

@@ -0,0 +1,3 @@
---
openapi: delete /api/v1/webhooks/{webhookId}
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/webhooks
---

View File

@@ -0,0 +1,3 @@
---
openapi: get /api/v1/webhooks/{webhookId}
---

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,129 @@
---
title: "REST API"
icon: "code"
description: "
Formbricks provides two APIs: the Public Client API for frontend survey interactions and the Management API for backend management tasks."
---
<Info>
View our [API Documentation](/api-reference) in more than 30 frameworks and languages.
</Info>
## Public Client API
The **Public Client API** is used by our SDKs and doesnt require authentication, making it ideal for client-side interactions without exposing sensitive data.
We currently have the following Client API methods exposed and below is their documentation attached in Postman:
- [Displays API](/api-reference/client-api->-display/create-display) - Mark a survey as displayed or link a display to a response for a person.
- [People API](/api-reference/client-api->-people/create-person) - Create & Update a Person (e.g., attributes, email, userId, etc.)
- [Responses API](/api-reference/client-api->-response/create-response) - Create & Update a Response for a Survey.
## Management API
The **Management API** gives full access to all data and settings available in your Formbricks account. It requires a personal API Key for authentication, which you can generate and manage in the Settings section of the Formbricks app.
We currently have the following Management API methods exposed and below is their documentation attached in Postman:
- [Action Class API](/api-reference/management-api->-action-class/get-all-action-classes) - Create, List, and Delete Action Classes
- [Attribute Class API ](/api-reference/management-api->-attribute-class/get-all-attribute-classes)- Create, List, and Delete Attribute Classes
- [Me API](/api-reference/management-api->-me/me) - Retrieve Account Information
- [People API](/api-reference/management-api->-people/get-all-persons) - List and Delete People
- [Response API](/api-reference/management-api->-response/get-survey-responses) - List, List by Survey, Update, and Delete Responses
- [Survey API](/api-reference/management-api->-survey/get-all-surveys) - List, Create, Update, generate multiple suId, and Delete Surveys
- [Webhook API](/api-reference/management-api->-webhook/get-all-webhooks) - List, Create, and Delete Webhooks
## How to Generate an API key
API requests require a personal API key for authorization. This API key gives you the same rights as if you were logged in at Formbricks UI - **keep it private!**
- Go to your settings on [Formbricks UI](https://app.formbricks.com).
- Go to page “API keys”
![API Keys](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738097810/image_jvhqsd.jpg)
- Create a key for the development or production environment.
- Copy the key immediately. You wont be able to see it again.
![API Key Label](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738098072/image_zjkvok.jpg)
<Note>
**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
Get the project details and environment type of your account.
### Mandatory Headers
| Name | x-Api-Key |
| --------------- | ------------------------ |
| **Type** | string |
| **Description** | Your Formbricks API key. |
### Request
```bash cURL
GET - /api/v1/me
curl --location \
'https://app.formbricks.com/api/v1/me' \
--header \
'x-api-key: <your-api-key>'
```
### Response
<CodeGroup>
```bash 200 (Success)
{
"id": "cll2m30r70004mx0huqkitgqv",
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"project": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Project"
},
"appSetupCompleted": false,
"websiteSetupCompleted": false,
}
```
```bash 401 (Not Authenticated)
Not authenticated
```
</CodeGroup>
### Delete a personal API key
- Go to settings on [app.formbricks.com](https://app.formbricks.com/).
- Go to the page “API keys”.
- Find the key you wish to revoke and select “Delete”.
- Your API key will stop working immediately.
---
**Cant figure it out?** Get help in [GitHub Discussions](https://github.com/formbricks/formbricks/discussions).

View File

@@ -0,0 +1,43 @@
---
title: "Contributing to Formbricks 🤗"
description: "How to contribute to Formbricks"
icon: "code"
---
Were excited that you want to contribute to Formbricks! There are many ways to help, including reporting issues, fixing bugs, adding new features, or improving documentation.
#### How to Contribute
- **Issues:** Found a bug? Facing deployment problems? Have user feedback? Report an issue for the fastest response.
- **Feature Requests:** Have an idea? Open an issue, tag it as an **Enhancement**, and clearly describe the issue you're solving.
- **Pull Requests (PRs):** Fork the repo, make your changes, and submit a PR.
- For small fixes, go ahead!
- For bigger features, please discuss them with us first to ensure they align with our roadmap.
#### Talk to Us First
We highly recommend engaging with us on [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions) before submitting contributions.
This helps improve the chances of your PR being accepted while avoiding unnecessary work.
#### Contributor License Agreement (CLA)
To keep Formbricks sustainable, we require a **CLA** from all contributors.
Once you open a PR, our **CLA bot** will prompt you to sign the agreement. We can only merge contributions after the CLA is signed.
#### Setting Up Your Development Environment
You can set up your environment using:
- [**Gitpod**](/development/local-setup/gitpod)
- [**GitHub Codespaces**](/development/local-setup/github-codespaces)
- [**Local Machine Setup**](/development/local-setup)
For junior developers, **Gitpod or GitHub Codespaces** are recommended as they allow you to start coding in minutes.

View File

@@ -0,0 +1,55 @@
---
title: GitHub Codespaces
description: How to set up Formbricks in a GitHub Codespaces environment
icon: "github"
---
### GitHub Codespaces Setup
<Info>
This guide outlines how to set up Formbricks in a **GitHub Codespaces** environment.
</Info>
**Requirements:**
- A GitHub Codespace that has support for Node.JS, pnpm, and Docker.
**Steps:**
1. **Open your repository in GitHub Codespaces. If needed, clone the repository:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm (if not already configured):**
```bash
nvm install && nvm use
```
3. **Install the dependencies:**
```bash
pnpm install
```
4. **Create a `.env` file from the template:**
```bash
cp .env.example .env
```
5. **Generate & set the required secrets:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Launch the development setup:**
```bash
pnpm go
```
Use the Codespaces port forwarding to access Formbricks at [http://localhost:3000](http://localhost:3000).
<Tip>
Make sure your Codespaces port configuration is set to allow access to the app.
</Tip>

View File

@@ -0,0 +1,55 @@
---
title: Gitpod
description: How to set up Formbricks in a Gitpod workspace
icon: "code"
---
### Gitpod Setup
<Info>
This guide explains how to set up Formbricks in a **Gitpod** workspace.
</Info>
**Requirements:**
- A Gitpod workspace with Node.JS, pnpm, and Docker support.
**Steps:**
1. **Open the repository in Gitpod. The workspace typically clones the repo automatically. If not:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm:**
```bash
nvm install && nvm use
```
3. **Install dependencies:**
```bash
pnpm install
```
4. **Create a `.env` file:**
```bash
cp .env.example .env
```
5. **Generate & set secret values:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Run the development setup:**
```bash
pnpm go
```
Access the running app via the forwarded port (typically [http://localhost:3000](http://localhost:3000) inside Gitpod).
<Tip>
Check your Gitpod settings to ensure Docker is enabled if required.
</Tip>

View File

@@ -0,0 +1,57 @@
---
title: Linux
description: How to set up Formbricks on a Linux machine
icon: "linux"
---
### Local Machine Setup - Linux
<Info>
This guide is recommended for advanced users setting up Formbricks on a **Linux** machine.
</Info>
Here are the requirements for setting up Formbricks on Linux:
- Node.JS (v20 recommended)
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/) (to run PostgreSQL/MailHog)
**Steps:**
1. **Clone the project & move into the directory:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm:**
```bash
nvm install && nvm use
```
3. **Install NodeJS packages via pnpm:**
```bash
pnpm install
```
4. **Create a `.env` file based on `.env.example`:**
```bash
cp .env.example .env
```
5. **Generate & set the secret values:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Start the development setup:**
```bash
pnpm go
```
You can now access Formbricks at [http://localhost:3000](http://localhost:3000).
<Tip>
Create a new account on first login as no default account is available.
</Tip>

View File

@@ -0,0 +1,57 @@
---
title: Mac
description: How to set up Formbricks on a Mac machine
icon: "apple"
---
### Local Machine Setup - Mac
<Info>
This guide is recommended for advanced users setting up Formbricks on a **Mac** machine.
</Info>
**Requirements:**
- Node.JS (v20 recommended)
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/)
**Steps:**
1. **Clone the project & change directory:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm:**
```bash
nvm install && nvm use
```
3. **Install NodeJS packages with pnpm:**
```bash
pnpm install
```
4. **Create a `.env` file from the example:**
```bash
cp .env.example .env
```
5. **Generate & set secret values (using BSD sed syntax for macOS):**
```bash
sed -i '' '/^ENCRYPTION_KEY=/s|.*|ENCRYPTION_KEY='$(openssl rand -hex 32)'|' .env
sed -i '' '/^NEXTAUTH_SECRET=/s|.*|NEXTAUTH_SECRET='$(openssl rand -hex 32)'|' .env
sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env
```
6. **Start the development setup:**
```bash
pnpm go
```
Visit [http://localhost:3000](http://localhost:3000) to access Formbricks.
<Tip>
Ensure you create a new account at first login.
</Tip>

View File

@@ -0,0 +1,57 @@
---
title: Windows
description: How to set up Formbricks on a Windows machine
icon: "windows"
---
### Local Machine Setup - Windows
<Info>
This guide is intended for **Windows** users. For the best experience, use **WSL2** since pure Windows is not fully supported.
</Info>
**Requirements:**
- Node.JS (v20 recommended) via WSL2
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/) (ensure Docker Desktop is installed with WSL2 integration enabled)
**Steps (Using WSL2):**
1. **Open your WSL2 terminal and clone the project:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm in WSL2:**
```bash
nvm install && nvm use
```
3. **Install packages using pnpm:**
```bash
pnpm install
```
4. **Create a `.env` file:**
```bash
cp .env.example .env
```
5. **Generate & set secret values (Linux commands work in WSL2):**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Start the development setup:**
```bash
pnpm go
```
Access Formbricks at [http://localhost:3000](http://localhost:3000).
<Tip>
If you run into conflicts, ensure any local services (like PostgreSQL) are stopped.
</Tip>

View File

@@ -0,0 +1,16 @@
---
title: Development
description: Learn how to setup formbricks locally and develop on it
icon: "code"
---
Welcome to the Development section of Formbricks! This guide is designed to help you get started with setting up the project locally, contributing to the Formbricks codebase, and customizing it to suit your needs.
Whether you're a seasoned developer or just getting started, you'll find valuable information on how to:
- **Set Up Locally**: Step-by-step instructions to clone the repository, install dependencies, and run Formbricks on your local machine.
- **Contribute**: Guidelines on how to contribute to the project, including coding standards, submitting pull requests, and collaborating with other developers.
- **Customize**: Tips and tricks for customizing Formbricks to better fit your specific use cases, including modifying components and extending functionality.
Dive in and start building with Formbricks today!

View File

@@ -0,0 +1,38 @@
---
title: "Troubleshooting"
description: "Here, you'll find help with common issues."
icon: "wrench"
---
## "The app doesn't work after Prisma migration"
If the app doesnt work after a Prisma migration, clear your browsers storage and reload the page. This will force the app to fetch data from the server again. ![prisma](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738108186/image_dwm9hp.jpg)
## "I ran 'pnpm i' but there seems to be an error with the packages"
If you run `pnpm i` and get an error with the packages, try running `pnpm clean` followed by `pnpm i` again. This often solves the problem.
## "I get a full-screen error with cryptic strings"
This usually happens when the Formbricks Widget isn't correctly or completely built.
```bash
pnpm build --filter=@formbricks/js
// Run the app again
pnpm dev
```
## "My machine struggles with the repository"
Since we're working with a monorepo structure, the repository can get quite big. If you're having trouble working with the repository, try the following:
```bash helloWorld.js
pnpm dev --filter=@formbricks/web...
```
Its better to use a single terminal with `pnpm dev` rather than having multiple open (one with the Formbricks app and one with the demo).
## Error: "Uncaught (in promise) SyntaxError: Unexpected token !DOCTYPE ... is not valid JSON"![Syntax Error](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738109837/image_wbxv8k.jpg)
If you see this error, it happens when the person connected to the widget is deleted. To fix it, log out of the test person and reload the page.![Reset person](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738110212/image_nvkpku.jpg)

73
docs/images/favicon.svg Normal file
View File

@@ -0,0 +1,73 @@
<svg width="198" height="263" viewBox="0 0 198 263" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 183.149H79.2V222.749C79.2 244.619 61.4705 262.349 39.6 262.349C17.7295 262.349 0 244.619 0 222.749V183.149Z" fill="url(#paint0_linear_301_72)"/>
<path d="M0 91.5747H158.4C180.27 91.5747 198 109.304 198 131.175C198 153.045 180.27 170.775 158.4 170.775H0V91.5747Z" fill="url(#paint1_linear_301_72)"/>
<path d="M0 64.9181C0 29.0648 29.0648 0 64.918 0H158.4C180.27 0 198 17.7295 198 39.6C198 61.4705 180.27 79.2 158.4 79.2H0V64.9181Z" fill="url(#paint2_linear_301_72)"/>
<mask id="mask0_301_72" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="198" height="263">
<path d="M0 183.151H79.2V222.751C79.2 244.621 61.4705 262.351 39.6 262.351C17.7295 262.351 0 244.621 0 222.751V183.151Z" fill="url(#paint3_linear_301_72)"/>
<path d="M0 91.5762H158.4C180.27 91.5762 198 109.306 198 131.176C198 153.047 180.27 170.776 158.4 170.776H0V91.5762Z" fill="url(#paint4_linear_301_72)"/>
<path d="M0 64.9181C0 29.0648 29.0648 0 64.918 0H158.4C180.27 0 198 17.7295 198 39.6C198 61.4705 180.27 79.2 158.4 79.2H0V64.9181Z" fill="url(#paint5_linear_301_72)"/>
</mask>
<g mask="url(#mask0_301_72)">
<g filter="url(#filter0_d_301_72)">
<mask id="mask1_301_72" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="198" height="263">
<path d="M0 183.151H79.2V222.751C79.2 244.621 61.4705 262.351 39.6 262.351C17.7295 262.351 0 244.621 0 222.751V183.151Z" fill="black" fill-opacity="0.1"/>
<path d="M0 64.9181C0 29.0648 29.0648 0 64.918 0H158.4C180.27 0 198 17.7295 198 39.6C198 61.4705 180.27 79.2 158.4 79.2H0V64.9181Z" fill="black" fill-opacity="0.1"/>
<path d="M0 91.5762H158.4C180.27 91.5762 198 109.306 198 131.176C198 153.047 180.27 170.776 158.4 170.776H0V91.5762Z" fill="black" fill-opacity="0.1"/>
</mask>
<g mask="url(#mask1_301_72)">
<path d="M4.1553 -68.2164C35.1799 -98.4956 113.85 -68.2164 113.85 -68.2164H4.1553C-3.4647 -60.7794 -8.21048 -49.6893 -8.21048 -33.6001C-8.21048 47.996 80.1781 77.6677 80.1781 134.538C80.1781 190.209 -4.52334 224.555 -8.09431 300.203H113.85C113.85 300.203 -8.21048 384.271 -8.21048 305.148C-8.21048 303.48 -8.17121 301.832 -8.09431 300.203H-61.875L-51.3525 -68.2164H4.1553Z" fill="black" fill-opacity="0.1"/>
</g>
</g>
<g filter="url(#filter1_f_301_72)">
<circle cx="-24.75" cy="227.7" r="74.25" fill="#00C4B8"/>
</g>
<g filter="url(#filter2_f_301_72)">
<circle cx="-24.75" cy="39.6006" r="74.25" fill="#00C4B8"/>
</g>
</g>
<defs>
<filter id="filter0_d_301_72" x="-6.4918" y="-38.9508" width="191.752" height="340.253" 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="32.459"/>
<feGaussianBlur stdDeviation="19.4754"/>
<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_301_72"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_301_72" result="shape"/>
</filter>
<filter id="filter1_f_301_72" x="-163.918" y="88.5317" width="278.336" height="278.336" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="32.459" result="effect1_foregroundBlur_301_72"/>
</filter>
<filter id="filter2_f_301_72" x="-163.918" y="-99.5675" width="278.336" height="278.336" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="32.459" result="effect1_foregroundBlur_301_72"/>
</filter>
<linearGradient id="paint0_linear_301_72" x1="79.5444" y1="221.314" x2="-0.00681719" y2="221.636" gradientUnits="userSpaceOnUse">
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint1_linear_301_72" x1="198.861" y1="129.74" x2="2.49336e-08" y2="131.749" gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint2_linear_301_72" x1="198.861" y1="38.1652" x2="2.49336e-08" y2="40.1739" gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint3_linear_301_72" x1="79.5444" y1="221.316" x2="-0.00681719" y2="221.638" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint4_linear_301_72" x1="198.861" y1="129.741" x2="2.49336e-08" y2="131.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint5_linear_301_72" x1="198.861" y1="38.1652" x2="2.49336e-08" y2="40.1739" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

76
docs/images/logo-dark.svg Normal file
View File

@@ -0,0 +1,76 @@
<svg width="725" height="150" viewBox="0 0 725 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143 116H157V87.4H183.6V75H157V58.6H184.1V46H143V116ZM217.309 117.3C233.409 117.3 245.009 105.1 245.009 90.1C245.009 75.1 233.409 62.8 217.309 62.8C201.209 62.8 189.609 75.1 189.609 90.1C189.609 105.1 201.209 117.3 217.309 117.3ZM203.109 90.1C203.109 82 208.709 75.1 217.309 75.1C225.909 75.1 231.509 82 231.509 90.1C231.509 98.2 225.909 105 217.309 105C208.709 105 203.109 98.2 203.109 90.1ZM250.984 116H264.484V89.4C264.484 80.7 270.484 75.5 278.284 75.5H280.184V62.8C271.184 62.3 265.884 66.4 264.084 73.9V64H250.984V116ZM288.676 116H302.176V87.5C302.176 78.5 307.276 74.6 312.976 74.6C319.676 74.6 324.176 78.5 324.176 87.5V116H337.676V87.5C337.676 77.5 341.976 74.6 348.276 74.6C354.376 74.6 358.676 78.5 358.676 87.5V116H372.176V84.2C372.176 71.1 363.576 62.8 352.376 62.8C343.276 62.8 338.276 66.3 334.776 72.6C331.376 66.3 325.176 62.8 317.876 62.8C309.876 62.8 304.676 65.8 301.976 71.6V64H288.676V116ZM413.591 117.3C427.891 117.3 439.291 105.2 439.291 90.2C439.291 75.2 427.891 62.9 413.591 62.9C404.691 62.9 399.991 66.5 397.191 72V43H383.691V116H396.791V107.5C399.591 113.4 404.291 117.3 413.591 117.3ZM396.791 90.2C396.791 82.1 402.491 75.1 411.391 75.1C419.991 75.1 425.791 82 425.791 90.1C425.791 98 419.991 105.1 411.391 105.1C402.491 105.1 396.791 98.3 396.791 90.2ZM447.313 116H460.813V89.4C460.813 80.7 466.813 75.5 474.613 75.5H476.513V62.8C467.513 62.3 462.213 66.4 460.413 73.9V64H447.313V116ZM485.004 116H498.504V64H485.004V116ZM483.604 51.7C483.604 56.1 487.104 59.7 491.704 59.7C496.404 59.7 499.804 56.2 499.804 51.7C499.804 47.4 496.404 43.8 491.704 43.8C487.104 43.8 483.604 47.4 483.604 51.7ZM531.288 117.3C541.488 117.3 548.688 113.6 553.588 107.3L543.788 98.8C541.088 102.4 537.688 105 532.488 105C523.888 105 517.988 98.1 517.988 90C517.988 81.9 523.588 75 532.188 75C536.788 75 540.788 77.3 543.188 80.9L553.588 72.4C549.188 66.3 541.488 62.6 531.988 62.7C516.088 62.8 504.488 74.9 504.488 90C504.488 105.1 516.388 117.3 531.288 117.3ZM593.33 116H611.23L586.63 88.2L611.13 64H591.93L572.43 85V43H558.93V116H572.43V91.3L593.33 116ZM633.562 117.3C645.062 117.3 653.762 110.1 653.762 100.5C653.762 92.7 650.262 87.1 635.162 83.2C628.962 81.6 628.162 80 628.162 78.2C628.162 75 630.962 73.6 634.662 73.6C638.162 73.6 641.362 75.2 643.862 79.2L653.662 71.7C648.362 64.8 642.762 62.7 634.362 62.7C622.162 62.7 614.962 69.5 614.962 78.6C614.962 85.2 617.062 91.5 630.962 94.8C639.162 96.7 640.462 98.2 640.462 100.9C640.462 104 637.862 106.3 633.262 106.3C628.862 106.3 624.862 104.2 621.062 99.9L611.962 108.2C617.662 114.3 624.762 117.3 633.562 117.3Z" fill="#F1F5F9"/>
<path d="M0 98.6416H25.3585V111.321C25.3585 118.323 19.6818 124 12.6792 124V124C5.67669 124 0 118.323 0 111.321V98.6416Z" fill="url(#paint0_linear_2626_5860)"/>
<path d="M0 69.3208H50.717C57.7195 69.3208 63.3962 74.9975 63.3962 82V82C63.3962 89.0026 57.7195 94.6793 50.717 94.6793H0V69.3208Z" fill="url(#paint1_linear_2626_5860)"/>
<path d="M0 60C0 48.9543 8.9543 40 20 40H50.717C57.7195 40 63.3962 45.6767 63.3962 52.6792V52.6792C63.3962 59.6818 57.7195 65.3585 50.717 65.3585H0V60Z" fill="url(#paint2_linear_2626_5860)"/>
<mask id="mask0_2626_5860" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="40" width="64" height="84">
<path d="M0 98.6416H25.3585V111.321C25.3585 118.323 19.6818 124 12.6792 124V124C5.67669 124 0 118.323 0 111.321V98.6416Z" fill="url(#paint3_linear_2626_5860)"/>
<path d="M0 69.3208H50.717C57.7195 69.3208 63.3962 74.9975 63.3962 82V82C63.3962 89.0026 57.7195 94.6793 50.717 94.6793H0V69.3208Z" fill="url(#paint4_linear_2626_5860)"/>
<path d="M0 60C0 48.9543 8.9543 40 20 40H50.717C57.7195 40 63.3962 45.6767 63.3962 52.6792V52.6792C63.3962 59.6818 57.7195 65.3585 50.717 65.3585H0V60Z" fill="url(#paint5_linear_2626_5860)"/>
</mask>
<g mask="url(#mask0_2626_5860)">
<g filter="url(#filter0_d_2626_5860)">
<mask id="mask1_2626_5860" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="40" width="64" height="84">
<path d="M0 98.6416H25.3585V111.321C25.3585 118.323 19.6818 124 12.6792 124V124C5.67669 124 0 118.323 0 111.321V98.6416Z" fill="black" fill-opacity="0.1"/>
<path d="M0 60C0 48.9543 8.9543 40 20 40H50.717C57.7195 40 63.3962 45.6767 63.3962 52.6792V52.6792C63.3962 59.6818 57.7195 65.3585 50.717 65.3585H0V60Z" fill="black" fill-opacity="0.1"/>
<path d="M0 69.3208H50.717C57.7195 69.3208 63.3962 74.9975 63.3962 82V82C63.3962 89.0026 57.7195 94.6793 50.717 94.6793H0V69.3208Z" fill="black" fill-opacity="0.1"/>
</mask>
<g mask="url(#mask1_2626_5860)">
<path d="M1.33026 18.158C11.2638 8.46307 36.4526 18.158 36.4526 18.158H1.33026C-1.10954 20.5391 -2.62906 24.09 -2.62906 29.2415C-2.62906 55.3672 25.6715 64.8676 25.6715 83.0764C25.6715 100.901 -1.4485 111.898 -2.59186 136.119H36.4526C36.4526 136.119 -2.62906 163.037 -2.62906 137.703C-2.62906 137.169 -2.61648 136.641 -2.59186 136.119H-19.8115L-16.4424 18.158H1.33026Z" fill="black" fill-opacity="0.1"/>
</g>
</g>
<g filter="url(#filter1_f_2626_5860)">
<circle cx="-7.92441" cy="112.906" r="23.7736" fill="#00C4B8"/>
</g>
<g filter="url(#filter2_f_2626_5860)">
<circle cx="-7.92441" cy="52.6793" r="23.7736" fill="#00C4B8"/>
</g>
</g>
<line x1="102.75" y1="38" x2="102.75" y2="125" stroke="#475569" stroke-width="1.5"/>
<defs>
<filter id="filter0_d_2626_5860" x="-2" y="28" width="60.4526" height="108" 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="10"/>
<feGaussianBlur stdDeviation="6"/>
<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_2626_5860"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2626_5860" result="shape"/>
</filter>
<filter id="filter1_f_2626_5860" x="-51.698" y="69.1321" width="87.5471" height="87.5471" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="10" result="effect1_foregroundBlur_2626_5860"/>
</filter>
<filter id="filter2_f_2626_5860" x="-51.698" y="8.90576" width="87.5471" height="87.5471" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="10" result="effect1_foregroundBlur_2626_5860"/>
</filter>
<linearGradient id="paint0_linear_2626_5860" x1="25.4688" y1="110.861" x2="-0.00218275" y2="110.964" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E6CA"/>
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint1_linear_2626_5860" x1="63.6719" y1="81.5407" x2="7.75704e-09" y2="82.1838" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E6CA"/>
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint2_linear_2626_5860" x1="63.6719" y1="52.2199" x2="7.75704e-09" y2="52.863" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E6CA"/>
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint3_linear_2626_5860" x1="25.4688" y1="110.861" x2="-0.00218275" y2="110.964" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint4_linear_2626_5860" x1="63.6719" y1="81.5407" x2="7.75704e-09" y2="82.1838" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint5_linear_2626_5860" x1="63.6719" y1="52.2199" x2="7.75704e-09" y2="52.863" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,76 @@
<svg width="725" height="150" viewBox="0 0 725 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143 116H157V87.4H183.6V75H157V58.6H184.1V46H143V116ZM217.309 117.3C233.409 117.3 245.009 105.1 245.009 90.1C245.009 75.1 233.409 62.8 217.309 62.8C201.209 62.8 189.609 75.1 189.609 90.1C189.609 105.1 201.209 117.3 217.309 117.3ZM203.109 90.1C203.109 82 208.709 75.1 217.309 75.1C225.909 75.1 231.509 82 231.509 90.1C231.509 98.2 225.909 105 217.309 105C208.709 105 203.109 98.2 203.109 90.1ZM250.984 116H264.484V89.4C264.484 80.7 270.484 75.5 278.284 75.5H280.184V62.8C271.184 62.3 265.884 66.4 264.084 73.9V64H250.984V116ZM288.676 116H302.176V87.5C302.176 78.5 307.276 74.6 312.976 74.6C319.676 74.6 324.176 78.5 324.176 87.5V116H337.676V87.5C337.676 77.5 341.976 74.6 348.276 74.6C354.376 74.6 358.676 78.5 358.676 87.5V116H372.176V84.2C372.176 71.1 363.576 62.8 352.376 62.8C343.276 62.8 338.276 66.3 334.776 72.6C331.376 66.3 325.176 62.8 317.876 62.8C309.876 62.8 304.676 65.8 301.976 71.6V64H288.676V116ZM413.591 117.3C427.891 117.3 439.291 105.2 439.291 90.2C439.291 75.2 427.891 62.9 413.591 62.9C404.691 62.9 399.991 66.5 397.191 72V43H383.691V116H396.791V107.5C399.591 113.4 404.291 117.3 413.591 117.3ZM396.791 90.2C396.791 82.1 402.491 75.1 411.391 75.1C419.991 75.1 425.791 82 425.791 90.1C425.791 98 419.991 105.1 411.391 105.1C402.491 105.1 396.791 98.3 396.791 90.2ZM447.313 116H460.813V89.4C460.813 80.7 466.813 75.5 474.613 75.5H476.513V62.8C467.513 62.3 462.213 66.4 460.413 73.9V64H447.313V116ZM485.004 116H498.504V64H485.004V116ZM483.604 51.7C483.604 56.1 487.104 59.7 491.704 59.7C496.404 59.7 499.804 56.2 499.804 51.7C499.804 47.4 496.404 43.8 491.704 43.8C487.104 43.8 483.604 47.4 483.604 51.7ZM531.288 117.3C541.488 117.3 548.688 113.6 553.588 107.3L543.788 98.8C541.088 102.4 537.688 105 532.488 105C523.888 105 517.988 98.1 517.988 90C517.988 81.9 523.588 75 532.188 75C536.788 75 540.788 77.3 543.188 80.9L553.588 72.4C549.188 66.3 541.488 62.6 531.988 62.7C516.088 62.8 504.488 74.9 504.488 90C504.488 105.1 516.388 117.3 531.288 117.3ZM593.33 116H611.23L586.63 88.2L611.13 64H591.93L572.43 85V43H558.93V116H572.43V91.3L593.33 116ZM633.562 117.3C645.062 117.3 653.762 110.1 653.762 100.5C653.762 92.7 650.262 87.1 635.162 83.2C628.962 81.6 628.162 80 628.162 78.2C628.162 75 630.962 73.6 634.662 73.6C638.162 73.6 641.362 75.2 643.862 79.2L653.662 71.7C648.362 64.8 642.762 62.7 634.362 62.7C622.162 62.7 614.962 69.5 614.962 78.6C614.962 85.2 617.062 91.5 630.962 94.8C639.162 96.7 640.462 98.2 640.462 100.9C640.462 104 637.862 106.3 633.262 106.3C628.862 106.3 624.862 104.2 621.062 99.9L611.962 108.2C617.662 114.3 624.762 117.3 633.562 117.3Z" fill="#0F172A"/>
<path d="M0 98.6416H25.3585V111.321C25.3585 118.323 19.6818 124 12.6792 124V124C5.67669 124 0 118.323 0 111.321V98.6416Z" fill="url(#paint0_linear_2625_5904)"/>
<path d="M0 69.3208H50.717C57.7195 69.3208 63.3962 74.9975 63.3962 82V82C63.3962 89.0026 57.7195 94.6793 50.717 94.6793H0V69.3208Z" fill="url(#paint1_linear_2625_5904)"/>
<path d="M0 60C0 48.9543 8.9543 40 20 40H50.717C57.7195 40 63.3962 45.6767 63.3962 52.6792V52.6792C63.3962 59.6818 57.7195 65.3585 50.717 65.3585H0V60Z" fill="url(#paint2_linear_2625_5904)"/>
<mask id="mask0_2625_5904" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="40" width="64" height="84">
<path d="M0 98.6416H25.3585V111.321C25.3585 118.323 19.6818 124 12.6792 124V124C5.67669 124 0 118.323 0 111.321V98.6416Z" fill="url(#paint3_linear_2625_5904)"/>
<path d="M0 69.3208H50.717C57.7195 69.3208 63.3962 74.9975 63.3962 82V82C63.3962 89.0026 57.7195 94.6793 50.717 94.6793H0V69.3208Z" fill="url(#paint4_linear_2625_5904)"/>
<path d="M0 60C0 48.9543 8.9543 40 20 40H50.717C57.7195 40 63.3962 45.6767 63.3962 52.6792V52.6792C63.3962 59.6818 57.7195 65.3585 50.717 65.3585H0V60Z" fill="url(#paint5_linear_2625_5904)"/>
</mask>
<g mask="url(#mask0_2625_5904)">
<g filter="url(#filter0_d_2625_5904)">
<mask id="mask1_2625_5904" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="40" width="64" height="84">
<path d="M0 98.6416H25.3585V111.321C25.3585 118.323 19.6818 124 12.6792 124V124C5.67669 124 0 118.323 0 111.321V98.6416Z" fill="black" fill-opacity="0.1"/>
<path d="M0 60C0 48.9543 8.9543 40 20 40H50.717C57.7195 40 63.3962 45.6767 63.3962 52.6792V52.6792C63.3962 59.6818 57.7195 65.3585 50.717 65.3585H0V60Z" fill="black" fill-opacity="0.1"/>
<path d="M0 69.3208H50.717C57.7195 69.3208 63.3962 74.9975 63.3962 82V82C63.3962 89.0026 57.7195 94.6793 50.717 94.6793H0V69.3208Z" fill="black" fill-opacity="0.1"/>
</mask>
<g mask="url(#mask1_2625_5904)">
<path d="M1.33026 18.158C11.2638 8.46307 36.4526 18.158 36.4526 18.158H1.33026C-1.10954 20.5391 -2.62906 24.09 -2.62906 29.2415C-2.62906 55.3672 25.6715 64.8676 25.6715 83.0764C25.6715 100.901 -1.4485 111.898 -2.59186 136.119H36.4526C36.4526 136.119 -2.62906 163.037 -2.62906 137.703C-2.62906 137.169 -2.61648 136.641 -2.59186 136.119H-19.8115L-16.4424 18.158H1.33026Z" fill="black" fill-opacity="0.1"/>
</g>
</g>
<g filter="url(#filter1_f_2625_5904)">
<circle cx="-7.92441" cy="112.906" r="23.7736" fill="#00C4B8"/>
</g>
<g filter="url(#filter2_f_2625_5904)">
<circle cx="-7.92441" cy="52.6793" r="23.7736" fill="#00C4B8"/>
</g>
</g>
<line x1="102.75" y1="38" x2="102.75" y2="125" stroke="#CBD5E1" stroke-width="1.5"/>
<defs>
<filter id="filter0_d_2625_5904" x="-2" y="28" width="60.4526" height="108" 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="10"/>
<feGaussianBlur stdDeviation="6"/>
<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_2625_5904"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2625_5904" result="shape"/>
</filter>
<filter id="filter1_f_2625_5904" x="-51.698" y="69.1321" width="87.5471" height="87.5471" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="10" result="effect1_foregroundBlur_2625_5904"/>
</filter>
<filter id="filter2_f_2625_5904" x="-51.698" y="8.90576" width="87.5471" height="87.5471" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="10" result="effect1_foregroundBlur_2625_5904"/>
</filter>
<linearGradient id="paint0_linear_2625_5904" x1="25.4688" y1="110.861" x2="-0.00218275" y2="110.964" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E6CA"/>
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint1_linear_2625_5904" x1="63.6719" y1="81.5407" x2="7.75704e-09" y2="82.1838" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E6CA"/>
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint2_linear_2625_5904" x1="63.6719" y1="52.2199" x2="7.75704e-09" y2="52.863" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E6CA"/>
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint3_linear_2625_5904" x1="25.4688" y1="110.861" x2="-0.00218275" y2="110.964" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint4_linear_2625_5904" x1="63.6719" y1="81.5407" x2="7.75704e-09" y2="82.1838" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint5_linear_2625_5904" x1="63.6719" y1="52.2199" x2="7.75704e-09" y2="52.863" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

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