mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-25 16:00:16 -06:00
Compare commits
8 Commits
tolgee-pul
...
fix/enviro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f803022d7c | ||
|
|
22e8a137ef | ||
|
|
a9fe05d64a | ||
|
|
f476693f0d | ||
|
|
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
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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}\"",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
mockMeta,
|
||||
mockResponse,
|
||||
mockResponseData,
|
||||
mockResponseNote, // mockResponseWithMockPerson,
|
||||
mockSingleUseId, // mockSurvey,
|
||||
mockResponseNote,
|
||||
// mockResponseWithMockPerson,
|
||||
mockSingleUseId,
|
||||
// mockSurvey,
|
||||
mockSurveyId,
|
||||
mockSurveySummaryOutput,
|
||||
mockTags,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user