Compare commits

...

8 Commits

Author SHA1 Message Date
pandeymangg
f803022d7c Merge branch 'main' into fix/environment-route-cache 2025-02-13 12:09:02 +05:30
Piyush Gupta
22e8a137ef fix: date question accessibility (#4698)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
2025-02-12 10:47:54 +00:00
github-actions[bot]
a9fe05d64a chore: bump version to v3.1.5 (#4729)
Co-authored-by: GitHub Actions <github-actions@github.com>
2025-02-12 09:50:13 +01:00
pandeymangg
f476693f0d fix 2025-02-11 18:18:14 +05:30
Yannick Torrès
5219065b8e fix: fr-FR translations (#4667)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-02-11 05:35:34 +00:00
Dhruwang Jariwala
cb8497229d fix: UI tweaks (#4721) 2025-02-10 04:10:01 +00:00
Dhruwang Jariwala
25b8920d20 docs: basic docs for kubernetes (#4669) 2025-02-08 07:15:00 +00:00
Dhruwang Jariwala
9203db88ab fix: z index issue (#4723) 2025-02-07 10:46:26 +00:00
52 changed files with 500 additions and 205 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,8 @@ import { environmentCache } from "@formbricks/lib/environment/cache";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZJsSyncInput } from "@formbricks/types/js";
export const fetchCache = "force-no-store";
export const OPTIONS = async (): Promise<Response> => {
return responses.successResponse({}, true);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
/* eslint-disable import/no-relative-packages -- required for importing types */
/* eslint-disable @typescript-eslint/no-namespace -- using namespaces is required for prisma-json-types-generator */
import { type TActionClassNoCodeConfig } from "../types/action-classes";
import { type TIntegrationConfig } from "../types/integration";

View File

@@ -1,7 +1,7 @@
import { createId } from "@paralleldrive/cuid2";
import fs from "node:fs/promises";
import path from "node:path";
import readline from "node:readline";
import { createId } from "@paralleldrive/cuid2";
const rl = readline.createInterface({
input: process.stdin,

View File

@@ -1,8 +1,8 @@
import { type Prisma, PrismaClient } from "@prisma/client";
import { exec } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { type Prisma, PrismaClient } from "@prisma/client";
const execAsync = promisify(exec);

View File

@@ -643,7 +643,7 @@
},
"formbricks_logo": "Formbricks-Logo",
"integrations": {
"activepieces_integration_description": "Stelle deine Umfragen sofort mit Activepieces in Verbindung",
"activepieces_integration_description": "Verbinde Formbricks sofort mit beliebten Apps, um Aufgaben ohne Programmierung zu automatisieren.",
"additional_settings": "Weitere Einstellungen",
"airtable": {
"airtable_base": "Airtable Basis",
@@ -814,6 +814,7 @@
"open_an_issue_on_github": "Eine Issue auf GitHub öffnen",
"open_the_browser_console_to_see_the_logs": "Öffne die Browser Konsole, um die Logs zu sehen.",
"receiving_data": "Daten werden empfangen \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Erneut prüfen",
"scroll_to_the_top": "Scroll nach oben!",
"step_1": "Schritt 1: Installiere mit pnpm, npm oder yarn",
"step_2": "Schritt 2: Widget initialisieren",
@@ -1089,6 +1090,7 @@
"eliminate_branding_with_whitelabel": "Entferne Formbricks Branding und aktiviere zusätzliche White-Label-Anpassungsoptionen.",
"email_customization_preview_email_heading": "Hey {userName}",
"email_customization_preview_email_text": "Dies ist eine E-Mail-Vorschau, um dir zu zeigen, welches Logo in den E-Mails gerendert wird.",
"enable_formbricks_ai": "Formbricks KI aktivieren",
"error_deleting_organization_please_try_again": "Fehler beim Löschen der Organisation. Bitte versuche es erneut.",
"formbricks_ai": "Formbricks KI",
"formbricks_ai_description": "Erhalte personalisierte Einblicke aus deinen Umfrageantworten mit Formbricks KI",
@@ -1243,7 +1245,7 @@
"edit": {
"1_choose_the_default_language_for_this_survey": "1. Wähle die Standardsprache für diese Umfrage:",
"2_activate_translation_for_specific_languages": "2. Übersetzung für bestimmte Sprachen aktivieren:",
"add": "Hinzufügen +",
"add": "+ hinzufügen",
"add_a_delay_or_auto_close_the_survey": "Füge eine Verzögerung hinzu oder schließe die Umfrage automatisch.",
"add_a_four_digit_pin": "Füge eine vierstellige PIN hinzu",
"add_a_new_question_to_your_survey": "Neue Frage hinzufügen",

View File

@@ -814,6 +814,7 @@
"open_an_issue_on_github": "Open an issue on GitHub",
"open_the_browser_console_to_see_the_logs": "Open the browser console to see the logs.",
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Re-check",
"scroll_to_the_top": "Scroll to the top!",
"step_1": "Step 1: Install with pnpm, npm or yarn",
"step_2": "Step 2: Initialize widget",
@@ -1089,6 +1090,7 @@
"eliminate_branding_with_whitelabel": "Eliminate Formbricks branding and enable additional white-label customization options.",
"email_customization_preview_email_heading": "Hey {userName}",
"email_customization_preview_email_text": "This is an email preview to show you which logo will be rendered in the emails.",
"enable_formbricks_ai": "Enable Formbricks AI",
"error_deleting_organization_please_try_again": "Error deleting organization. Please try again.",
"formbricks_ai": "Formbricks AI",
"formbricks_ai_description": "Get personalised insights from your survey responses with Formbricks AI",

View File

@@ -242,7 +242,7 @@
"minimum": "Min",
"mobile_overlay_text": "Formbricks n'est pas disponible pour les appareils avec des résolutions plus petites.",
"move_down": "Déplacer vers le bas",
"move_up": "Monter",
"move_up": "Déplacer vers le haut",
"multiple_languages": "Plusieurs langues",
"name": "Nom",
"negative": "Négatif",
@@ -643,7 +643,7 @@
},
"formbricks_logo": "Logo Formbricks",
"integrations": {
"activepieces_integration_description": "Connectez Formbricks à des applications populaires pour automatiser des tâches sans code.",
"activepieces_integration_description": "Connectez instantanément Formbricks avec des applications populaires pour automatiser les tâches sans coder.",
"additional_settings": "Paramètres supplémentaires",
"airtable": {
"airtable_base": "Base Airtable",
@@ -814,6 +814,7 @@
"open_an_issue_on_github": "Ouvrir un problème sur GitHub",
"open_the_browser_console_to_see_the_logs": "Ouvrez la console du navigateur pour voir les journaux.",
"receiving_data": "Réception des données \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Re-vérifier",
"scroll_to_the_top": "Faites défiler vers le haut !",
"step_1": "Étape 1 : Installer avec pnpm, npm ou yarn",
"step_2": "Étape 2 : Initialiser le widget",
@@ -1089,6 +1090,7 @@
"eliminate_branding_with_whitelabel": "Éliminez la marque Formbricks et activez des options de personnalisation supplémentaires.",
"email_customization_preview_email_heading": "Salut {userName}",
"email_customization_preview_email_text": "Cette est une prévisualisation d'e-mail pour vous montrer quel logo sera rendu dans les e-mails.",
"enable_formbricks_ai": "Activer Formbricks IA",
"error_deleting_organization_please_try_again": "Erreur lors de la suppression de l'organisation. Veuillez réessayer.",
"formbricks_ai": "Formbricks IA",
"formbricks_ai_description": "Obtenez des insights personnalisés à partir de vos réponses au sondage avec Formbricks AI.",
@@ -1274,14 +1276,14 @@
"address_fields": "Champs d'adresse",
"address_line_1": "Ligne d'adresse 1",
"address_line_2": "Ligne d'adresse 2",
"adjust_survey_closed_message": "Ajuster le message \"Sondage fermé",
"adjust_survey_closed_message": "Ajuster le message \"Sondage fermé\"",
"adjust_survey_closed_message_description": "Modifiez le message que les visiteurs voient lorsque l'enquête est fermée.",
"adjust_the_theme_in_the": "Ajustez le thème dans le",
"all_other_answers_will_continue_to": "Tous les autres réponses continueront à",
"all_other_answers_will_continue_to": "Toutes les autres réponses continueront à",
"allow_file_type": "Autoriser le type de fichier",
"allow_multi_select": "Autoriser la sélection multiple",
"allow_multiple_files": "Autoriser plusieurs fichiers",
"allow_users_to_select_more_than_one_image": "Permettre aux utilisateurs de sélectionner plus d'une image",
"allow_users_to_select_more_than_one_image": "Permettre aux utilisateurs de sélectionner plusieurs images",
"always_show_survey": "Afficher toujours l'enquête",
"and_launch_surveys_in_your_website_or_app": "et lancez des enquêtes sur votre site web ou votre application.",
"animation": "Animation",
@@ -1295,13 +1297,13 @@
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Ferme automatiquement l'enquête au début de la journée (UTC).",
"automatically_mark_the_survey_as_complete_after": "Marquer automatiquement l'enquête comme terminée après",
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Libérer automatiquement l'enquête au début de la journée (UTC).",
"back_button_label": "Label du bouton 'Retour''",
"back_button_label": "Label du bouton \"Retour''",
"background_styling": "Style de fond",
"blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already": "Bloque les enquêtes si une soumission avec l'Identifiant à Usage Unique (suId) existe déjà.",
"blocks_survey_if_the_survey_url_has_no_single_use_id_suid": "Bloque les enquêtes si l'URL de l'enquête n'a pas d'Identifiant d'Utilisation Unique (suId).",
"brand_color": "Couleur de marque",
"brightness": "Luminosité",
"button_label": "Étiquette du bouton",
"button_label": "Label du bouton",
"button_to_continue_in_survey": "Bouton pour continuer dans l'enquête",
"button_to_link_to_external_url": "Bouton pour lier à une URL externe",
"button_url": "URL du bouton",
@@ -1493,7 +1495,7 @@
"max_file_size_limit_is": "La taille maximale du fichier est",
"multiply": "Multiplier *",
"needed_for_self_hosted_cal_com_instance": "Nécessaire pour une instance Cal.com auto-hébergée",
"next_button_label": "Étiquette du bouton \"Suivant",
"next_button_label": "Label du bouton \"Suivant\"",
"next_question": "Question suivante",
"no_hidden_fields_yet_add_first_one_below": "Aucun champ caché pour le moment. Ajoutez le premier ci-dessous.",
"no_images_found_for": "Aucune image trouvée pour ''{query}\"",

View File

@@ -643,7 +643,7 @@
},
"formbricks_logo": "Logo da Formbricks",
"integrations": {
"activepieces_integration_description": "Conecte o Formbricks com aplicativos populares para automatizar tarefas sem codificar.",
"activepieces_integration_description": "Conecte o Formbricks instantaneamente com aplicativos populares para automatizar tarefas sem codificação.",
"additional_settings": "Configurações Adicionais",
"airtable": {
"airtable_base": "base do Airtable",
@@ -814,6 +814,7 @@
"open_an_issue_on_github": "Abre uma issue no GitHub",
"open_the_browser_console_to_see_the_logs": "Abre o console do navegador pra ver os logs.",
"receiving_data": "Recebendo dados \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Verificar novamente",
"scroll_to_the_top": "Rola pra cima!",
"step_1": "Passo 1: Instale com pnpm, npm ou yarn",
"step_2": "Passo 2: Iniciar widget",
@@ -1089,6 +1090,7 @@
"eliminate_branding_with_whitelabel": "Elimine a marca Formbricks e ative opções adicionais de personalização de marca branca.",
"email_customization_preview_email_heading": "Oi {userName}",
"email_customization_preview_email_text": "Esta é uma pré-visualização de e-mail para mostrar qual logo será renderizado nos e-mails.",
"enable_formbricks_ai": "Ativar Formbricks IA",
"error_deleting_organization_please_try_again": "Erro ao deletar a organização. Por favor, tente novamente.",
"formbricks_ai": "Formbricks IA",
"formbricks_ai_description": "Obtenha insights personalizados das suas respostas de pesquisa com o Formbricks AI",

View File

@@ -8,8 +8,10 @@ import {
mockMeta,
mockResponse,
mockResponseData,
mockResponseNote, // mockResponseWithMockPerson,
mockSingleUseId, // mockSurvey,
mockResponseNote,
// mockResponseWithMockPerson,
mockSingleUseId,
// mockSurvey,
mockSurveyId,
mockSurveySummaryOutput,
mockTags,

View File

@@ -158,7 +158,7 @@ export function DateQuestion({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="fb-text-red-600">
<div id="error-message" className="fb-text-red-600" aria-live="assertive">
<span>{errorMessage}</span>
</div>
<div
@@ -166,7 +166,7 @@ export function DateQuestion({
id="date-picker-root">
<div className="fb-relative">
{!datePickerOpen && (
<div
<button
onClick={() => {
setDatePickerOpen(true);
}}
@@ -174,6 +174,8 @@ export function DateQuestion({
onKeyDown={(e) => {
if (e.key === " ") setDatePickerOpen(true);
}}
aria-label={selectedDate ? `You have selected ${formattedDate}` : "Select a date"}
aria-describedby={errorMessage ? "error-message" : undefined}
className="focus:fb-outline-brand fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-text-heading fb-rounded-custom fb-relative fb-flex fb-h-[12dvh] fb-w-full fb-cursor-pointer fb-appearance-none fb-items-center fb-justify-center fb-border fb-text-left fb-text-base fb-font-normal">
<div className="fb-flex fb-items-center fb-gap-2">
{selectedDate ? (
@@ -186,7 +188,7 @@ export function DateQuestion({
</div>
)}
</div>
</div>
</button>
)}
<DatePicker
@@ -222,14 +224,14 @@ export function DateQuestion({
"calendar-root !fb-bg-input-bg fb-border fb-border-border fb-rounded-custom fb-p-3 fb-h-[46dvh] sm:fb-h-[33dvh] fb-overflow-auto",
tileClassName: ({ date }: { date: Date }) => {
const baseClass =
"hover:fb-bg-input-bg-selected fb-rounded-custom fb-h-9 fb-p-0 fb-mt-1 fb-font-normal fb-text-heading aria-selected:fb-opacity-100 focus:fb-ring-2 focus:fb-bg-slate-200";
"hover:fb-bg-input-bg-selected fb-rounded-custom fb-h-9 fb-p-0 fb-mt-1 fb-font-normal aria-selected:fb-opacity-100 focus:fb-ring-2 focus:fb-bg-slate-200";
// today's date class
if (
date.getDate() === new Date().getDate() &&
date.getMonth() === new Date().getMonth() &&
date.getFullYear() === new Date().getFullYear()
) {
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-heading focus:fb-ring-2 focus:fb-bg-slate-200`;
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-calendar-tile focus:fb-ring-2 focus:fb-bg-slate-200`;
}
// active date class
if (
@@ -238,10 +240,10 @@ export function DateQuestion({
date.getMonth() === selectedDate.getMonth() &&
date.getFullYear() === selectedDate.getFullYear()
) {
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-heading`;
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-calendar-tile`;
}
return baseClass;
return `${baseClass} !fb-text-heading`;
},
formatShortWeekday: (_: any, date: Date) => {
return date.toLocaleDateString("en-US", { weekday: "short" }).slice(0, 2);

View File

@@ -77,16 +77,17 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
appendCssVariable("input-background-color", styling.inputColor?.light);
if (styling.questionColor?.light) {
let signatureColor = "";
let brandingColor = "";
if (isLight(styling.questionColor.light)) {
signatureColor = mixColor(styling.questionColor.light, "#000000", 0.2);
brandingColor = mixColor(styling.questionColor.light, "#000000", 0.3);
} else {
signatureColor = mixColor(styling.questionColor.light, "#ffffff", 0.2);
brandingColor = mixColor(styling.questionColor.light, "#ffffff", 0.3);
}
const isLightQuestionColor = isLight(styling.questionColor.light);
const signatureColor = mixColor(
styling.questionColor.light,
isLightQuestionColor ? "#000000" : "#ffffff",
0.2
);
const brandingColor = mixColor(
styling.questionColor.light,
isLightQuestionColor ? "#000000" : "#ffffff",
0.3
);
appendCssVariable("signature-text-color", signatureColor);
appendCssVariable("branding-text-color", brandingColor);
@@ -115,6 +116,10 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
appendCssVariable("accent-background-color", accentColor);
appendCssVariable("accent-background-color-selected", accentColorSelected);
if (isLight(brandColor)) {
appendCssVariable("calendar-tile-color", mixColor(brandColor, "#000000", 0.7));
}
}
// Close the :root block

View File

@@ -93,7 +93,7 @@ p.fb-editor-paragraph {
--fb-rating-selected: black;
--fb-close-btn-color: var(--slate-500);
--fb-close-btn-color-hover: var(--slate-700);
--fb-calendar-tile-color: var(--slate-50);
--fb-border-radius: 8px;
}

View File

@@ -37,6 +37,7 @@ module.exports = {
"submit-button-border": "var(--fb-submit-btn-border)",
"close-button": "var(--fb-close-btn-color)",
"close-button-focus": "var(--fb-close-btn-hover-color)",
"calendar-tile": "var(--fb-calendar-tile-color)",
},
borderRadius: {
custom: "var(--fb-border-radius)",

View File

@@ -7,7 +7,7 @@ export default defineConfig(
config,
getServiceConfig(config, {
exposeNetwork: "<loopback>",
timeout: 30000,
timeout: 33000,
os: ServiceOS.LINUX,
useCloudHostedBrowsers: true, // Set to false if you want to only use reporting and not cloud hosted browsers
}),