fix: UI tweaks (#4721)

This commit is contained in:
Dhruwang Jariwala
2025-02-10 09:40:01 +05:30
committed by GitHub
parent 25b8920d20
commit cb8497229d
38 changed files with 289 additions and 171 deletions

View File

@@ -1,3 +1,5 @@
pnpm lint-staged
pnpm tolgee-pull || true
git add packages/lib/messages/*.json
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

@@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

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