mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
fix: UI tweaks (#4721)
This commit is contained in:
committed by
GitHub
parent
25b8920d20
commit
cb8497229d
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { AlertTriangleIcon, CheckIcon } from "lucide-react";
|
||||
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
|
||||
@@ -11,6 +13,7 @@ interface WidgetStatusIndicatorProps {
|
||||
|
||||
export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProps) => {
|
||||
const { t } = useTranslate();
|
||||
const router = useRouter();
|
||||
const stati = {
|
||||
notImplemented: {
|
||||
icon: AlertTriangleIcon,
|
||||
@@ -51,6 +54,12 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
||||
</div>
|
||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
||||
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
|
||||
{status === "notImplemented" && (
|
||||
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
||||
<RotateCcwIcon />
|
||||
{t("environments.project.app-connection.recheck")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -60,8 +60,7 @@ export const AIToggle = ({ organization, isOwnerOrManager }: AIToggleProps) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="formbricks-ai-toggle" className="cursor-pointer">
|
||||
{isAIEnabled ? t("common.disable") : t("common.enable")}{" "}
|
||||
{t("environments.settings.general.formbricks_ai")}
|
||||
{t("environments.settings.general.enable_formbricks_ai")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="formbricks-ai-toggle"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
import { InsightView } from "@/modules/ee/insights/components/insights-view";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -122,7 +123,11 @@ export const OpenTextSummary = ({
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{response.value}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{typeof response.value === "string"
|
||||
? renderHyperlinkedContent(response.value)
|
||||
: response.value}
|
||||
</TableCell>
|
||||
<TableCell width={120}>
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</TableCell>
|
||||
|
||||
@@ -85,11 +85,11 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
case TSurveyQuestionTypeEnum.Consent:
|
||||
return <CheckIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.PictureSelection:
|
||||
return <ImageIcon width={18} className="text-white" />;
|
||||
return <ImageIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.Matrix:
|
||||
return <GridIcon width={18} className="text-white" />;
|
||||
return <GridIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
return <ListOrderedIcon width={18} className="text-white" />;
|
||||
return <ListOrderedIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.ATTRIBUTES:
|
||||
return <User width={18} height={18} className="text-white" />;
|
||||
@@ -115,7 +115,7 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
return <LanguagesIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.TAGS:
|
||||
return <HashIcon width={18} className="text-white" />;
|
||||
return <HashIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,7 +133,7 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
return (
|
||||
<div className="flex h-5 w-[12rem] items-center sm:w-4/5">
|
||||
<span className={clsx("rounded-md p-1", getColor())}>{getIconType()}</span>
|
||||
<p className="ml-3 truncate text-base text-slate-600">
|
||||
<p className="ml-3 truncate text-sm text-slate-600">
|
||||
{typeof label === "string" ? label : getLocalizedValue(label, "default")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
import { FileUploadResponse } from "@/modules/ui/components/file-upload-response";
|
||||
import { PictureSelectionResponse } from "@/modules/ui/components/picture-selection-response";
|
||||
@@ -173,7 +174,11 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
"ph-no-capture my-1 truncate font-normal text-slate-700",
|
||||
isExpanded ? "whitespace-pre-line" : "whitespace-nowrap"
|
||||
)}>
|
||||
{Array.isArray(responseData) ? handleArray(responseData) : responseData}
|
||||
{typeof responseData === "string"
|
||||
? renderHyperlinkedContent(responseData)
|
||||
: Array.isArray(responseData)
|
||||
? handleArray(responseData)
|
||||
: responseData}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
26
apps/web/modules/analysis/utils.tsx
Normal file
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>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
mockMeta,
|
||||
mockResponse,
|
||||
mockResponseData,
|
||||
mockResponseNote, // mockResponseWithMockPerson,
|
||||
mockSingleUseId, // mockSurvey,
|
||||
mockResponseNote,
|
||||
// mockResponseWithMockPerson,
|
||||
mockSingleUseId,
|
||||
// mockSurvey,
|
||||
mockSurveyId,
|
||||
mockSurveySummaryOutput,
|
||||
mockTags,
|
||||
|
||||
Reference in New Issue
Block a user