Compare commits
9 Commits
tolgee-pul
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f88194f770 | ||
|
|
0ebbc67dbe | ||
|
|
cb2a73d569 | ||
|
|
22e8a137ef | ||
|
|
a9fe05d64a | ||
|
|
5219065b8e | ||
|
|
cb8497229d | ||
|
|
25b8920d20 | ||
|
|
9203db88ab |
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
158
apps/docs/app/self-hosting/kubernetes/page.mdx
Normal 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`
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,4 +7,3 @@ export function LoadingSpinner(props: React.ComponentPropsWithoutRef<"div">): Re
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -22,4 +22,3 @@ export default function SurveyEmbed({ surveyUrl }: SurveyEmbedProps): React.JSX.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
26
apps/web/modules/analysis/utils.tsx
Normal 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>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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`
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/client/{environmentId}/displays
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: put /api/v1/client/{environmentId}/displays/{displayId}
|
||||
---
|
||||
3
docs/api-reference/client-api->-people/create-person.mdx
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/client/{environmentId}/people
|
||||
---
|
||||
3
docs/api-reference/client-api->-people/update-person.mdx
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: put /api/v1/client/{environmentId}/people/{userId}/attributes
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/client/{environmentId}/responses
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: put /api/v1/client/responses/{responseId}
|
||||
---
|
||||
3
docs/api-reference/default/health-check.mdx
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /health
|
||||
---
|
||||
14
docs/api-reference/introduction.mdx
Normal 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 you’ve 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)!
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/management/action-classes
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: delete /api/v1/management/action-classes/{actionClassId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/action-classes/{actionClassId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/action-classes
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/management/attribute-classes
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: delete /api/v1/management/attribute-classes/{attributeClassId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/attribute-classes
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/attribute-classes/{attributeClassId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/contact-attribute-keys
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/contact-attribute-keys/{contactAttributeKeyId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/contact-attributes
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/contacts
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/contacts/{contactId}
|
||||
---
|
||||
3
docs/api-reference/management-api->-me/me.mdx
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/me
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: delete /api/v1/management/people/{personId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/people
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/people/{personId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/management/responses
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: delete /api/v1/management/responses/{responseId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/responses/{responseId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/responses
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: put /api/v1/management/responses/{responseId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/management/surveys
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: delete /api/v1/management/surveys/{surveyId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/surveys
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/surveys/{surveyId}/singleUseIds
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/management/surveys/{surveyId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: put /api/v1/management/surveys/{surveyId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: post /api/v1/webhooks
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: delete /api/v1/webhooks/{webhookId}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/webhooks
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /api/v1/webhooks/{webhookId}
|
||||
---
|
||||
7797
docs/api-reference/openapi.json
Normal file
129
docs/api-reference/rest-api.mdx
Normal 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 doesn’t 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”
|
||||
|
||||

|
||||
|
||||
- Create a key for the development or production environment.
|
||||
|
||||
- Copy the key immediately. You won’t be able to see it again.
|
||||
|
||||

|
||||
|
||||
<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.
|
||||
|
||||
---
|
||||
|
||||
**Can’t figure it out?** Get help in [GitHub Discussions](https://github.com/formbricks/formbricks/discussions).
|
||||
43
docs/development/contribution/contribution.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "Contributing to Formbricks 🤗"
|
||||
description: "How to contribute to Formbricks"
|
||||
icon: "code"
|
||||
---
|
||||
|
||||
We’re 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.
|
||||
|
||||
55
docs/development/local-setup/github-codespaces.mdx
Normal 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>
|
||||
55
docs/development/local-setup/gitpod.mdx
Normal 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>
|
||||
57
docs/development/local-setup/linux.mdx
Normal 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>
|
||||
57
docs/development/local-setup/mac.mdx
Normal 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>
|
||||
57
docs/development/local-setup/windows.mdx
Normal 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>
|
||||
16
docs/development/overview.mdx
Normal 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!
|
||||
|
||||
38
docs/development/support/troubleshooting.mdx
Normal 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 doesn’t work after a Prisma migration, clear your browser’s storage and reload the page. This will force the app to fetch data from the server again. 
|
||||
|
||||
## "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...
|
||||
```
|
||||
|
||||
It’s 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"
|
||||
|
||||
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.
|
||||
73
docs/images/favicon.svg
Normal 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
@@ -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 |
76
docs/images/logo-light.svg
Normal 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 |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 30 KiB |