fix(frontend): remove unused stuff (#2282)

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):
This commit is contained in:
Antoine Dewez 2024-03-04 11:35:34 -08:00 committed by GitHub
parent 34a521e3d8
commit 1b75e09420
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1 additions and 1707 deletions

View File

@ -1,8 +0,0 @@
.bg-slanted-upwards {
clip-path: polygon(
0 0,
100% 0,
100% max(70px, calc(100% - 100vw * tan(12deg))),
0 100%
);
}

View File

@ -1,38 +0,0 @@
import Link from "next/link";
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
import { cn } from "@/lib/utils";
import { PopoverMenuMobile } from "./components/PopoverMenuMobile";
import { useHomeHeader } from "./hooks/useHomeHeader";
import { linkStyle } from "./styles";
type HomeNavProps = {
color?: "white" | "black";
};
export const HomeHeader = ({ color = "white" }: HomeNavProps): JSX.Element => {
const { navLinks } = useHomeHeader({ color });
return (
<header className="w-screen flex justify-between items-center p-5 min-w-max md:max-w-6xl m-auto">
<Link
href="/"
className={cn(
"text-3xl flex gap-2 items-center",
linkStyle[color],
color === "black" ? "hover:text-black" : "hover:text-white"
)}
>
<QuivrLogo size={64} color={color} />
<div>Quivr</div>
</Link>
<div className="hidden md:flex">
<ul className="flex gap-4 items-center">{navLinks("desktop")}</ul>
</div>
<div className="md:hidden z-10">
<PopoverMenuMobile navLinks={navLinks("mobile")} color={color} />
</div>
</header>
);
};

View File

@ -1,13 +0,0 @@
import styles from "../HomeHeader.module.css";
export const HomeHeaderBackground = (): JSX.Element => {
return (
<div className="relative overflow-visible h-0 z-[-1]">
<div
className={`bg-gradient-to-b from-[#7A27FD] to-[#D07DF9] ${
styles["bg-slanted-upwards"] ?? ""
} w-screen h-[30vh] lg:h-[50vh] z-[-1]`}
></div>
</div>
);
};

View File

@ -1,73 +0,0 @@
import * as Popover from "@radix-ui/react-popover";
import { LuMenu, LuX } from "react-icons/lu";
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
import { cn } from "@/lib/utils";
type PopoverMenuMobileProps = {
navLinks: JSX.Element[];
color?: "white" | "black";
};
export const PopoverMenuMobile = ({
navLinks,
color = "white",
}: PopoverMenuMobileProps): JSX.Element => {
return (
<>
<Popover.Root>
<div>
<Popover.Anchor />
<Popover.Trigger
title="menu"
type="button"
className={cn(
"bg-[#D9D9D9] bg-opacity-30 rounded-full px-4 py-1",
color === "white" ? "text-white" : "text-black"
)}
>
<LuMenu size={32} />
</Popover.Trigger>
</div>
<Popover.Content
style={{
minWidth: "max-content",
backgroundColor: "white",
borderRadius: "0.75rem",
paddingTop: "0.5rem",
paddingInline: "1rem",
paddingBottom: "1.5rem",
marginRight: "1rem",
marginTop: "-1rem",
boxShadow: "0 0 0.5rem rgba(0, 0, 0, 0.1)",
}}
>
<div className="flex flex-col gap-4 min-w-max w-[calc(100vw-4rem)] sm:w-[300px]">
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<QuivrLogo size={64} color="primary" />
<div className="text-lg font-medium text-primary cursor-default ">
Quivr
</div>
</div>
<Popover.Close>
<button
title="close"
type="button"
className="hover:text-primary p-2"
>
<LuX size={24} />
</button>
</Popover.Close>
</div>
<nav>
<ul className="flex flex-col bg-[#F5F8FF] rounded-xl p-2">
{navLinks}
</ul>
</nav>
</div>
</Popover.Content>
</Popover.Root>
</>
);
};

View File

@ -1,70 +0,0 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { AiFillStar } from "react-icons/ai";
import { LuChevronRight } from "react-icons/lu";
import { useHomepageTracking } from "@/app/(home)/hooks/useHomepageTracking";
import { cn } from "@/lib/utils";
import { linkStyle } from "../styles";
import { NavbarItem } from "../types";
type UseHomeHeaderProps = {
color: "white" | "black";
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useHomeHeader = ({ color }: UseHomeHeaderProps) => {
const { t } = useTranslation("home");
const { onLinkClick } = useHomepageTracking();
const navItems: NavbarItem[] = [
{
href: "https://github.com/quivrhq/quivr",
label: t("star_us"),
leftIcon: <AiFillStar size={16} className="hidden md:inline" />,
rightIcon: null,
},
{ href: "/pricing", label: t("pricing"), rightIcon: null },
{
href: "https://docs.quivr.app",
label: t("docs"),
rightIcon: null,
newTab: true,
},
{ href: "/blog", label: t("blog"), rightIcon: null, newTab: true },
{ href: "/login", label: t("sign_in") },
];
const navLinks = (device: "mobile" | "desktop") =>
navItems.map(
({ href, label, leftIcon, rightIcon, newTab = false, className }) => (
<li key={label}>
<Link
href={href}
onClick={(event) => {
onLinkClick({
href,
label,
event,
});
}}
{...(newTab && { target: "_blank", rel: "noopener noreferrer" })}
className={cn(
"flex justify-between items-center hover:text-primary p-2 gap-1",
device === "desktop" ? linkStyle[color] : null,
className
)}
>
{leftIcon}
{label}
{rightIcon !== null && (rightIcon ?? <LuChevronRight size={16} />)}
</Link>
</li>
)
);
return {
navLinks,
};
};

View File

@ -1,4 +0,0 @@
export const linkStyle = {
white: "text-white hover:text-slate-200",
black: "text-black",
};

View File

@ -1,8 +0,0 @@
export type NavbarItem = {
href: string;
label: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode | null;
newTab?: boolean;
className?: string;
};

View File

@ -1,36 +0,0 @@
/* Fixes padding and margins due to slanted sibling sections */
.slant-before-is-up {
z-index: 2;
}
.slant-before-is-down {
z-index: 2;
margin-top: calc(-100vw * tan(6deg));
}
.slant-after-is-down {
padding-bottom: calc(50vw * tan(6deg));
}
.section-slanted-downwards {
transform: skew(0, 6deg) translateY(calc(50vw / -12));
padding-bottom: calc(100vw * tan(6deg));
display: flex;
align-items: center;
justify-content: center;
}
.section-slanted-downwards > * {
transform: skew(0, -6deg) translateY(calc(50vw / 12));
margin-top: calc(-50vw * tan(6deg));
}
.section-slanted-upwards {
transform: skew(0, -6deg) translateY(calc(50vw * tan(6deg)));
display: flex;
align-items: center;
justify-content: center;
}
.section-slanted-upwards > * {
transform: skew(0, 6deg) translateY(calc(-50vw * tan(6deg)));
margin-top: calc(50vw * tan(6deg));
}

View File

@ -1,50 +0,0 @@
import { cn } from "@/lib/utils";
import styles from "./HomeSection.module.css";
type HomeSectionProps = {
bg: string;
slantCurrent?: "up" | "down" | "none";
slantBefore?: "up" | "down" | "none";
slantAfter?: "up" | "down" | "none";
gradient?: string;
hiddenOnMobile?: boolean;
children: React.ReactNode;
className?: string;
};
export const HomeSection = ({
bg,
slantCurrent = "none",
slantBefore = "none",
slantAfter = "none",
gradient,
hiddenOnMobile = false,
className,
children,
}: HomeSectionProps): JSX.Element => {
const slantBeforeFix = styles[`slant-before-is-${slantBefore}`] ?? "";
const slantAfterFix = styles[`slant-after-is-${slantAfter}`] ?? "";
const flex = hiddenOnMobile
? "hidden md:flex md:justify-center"
: "flex justify-center";
const slant = styles[`section-slanted-${slantCurrent}wards`] ?? "";
return (
<div
className={cn(
`${bg} w-screen ${flex} ${slantBeforeFix} ${slantAfterFix} ${slant} overflow-hidden`,
className
)}
>
<section className="flex flex-col items-center w-full max-w-6xl z-[2] py-8">
{children}
</section>
{gradient !== undefined ? (
<div
className={`absolute w-screen bottom-[calc(100vw*tan(6deg)-1px)] left-0 h-[30%] ${gradient}`}
/>
) : null}
</div>
);
};

View File

@ -1,49 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { LuChevronRight } from "react-icons/lu";
import { useHomepageTracking } from "@/app/(home)/hooks/useHomepageTracking";
import { DEMO_VIDEO_DATA_KEY } from "@/lib/api/cms/config";
import { useCmsApi } from "@/lib/api/cms/useCmsApi";
import Button from "@/lib/components/ui/Button";
import Spinner from "@/lib/components/ui/Spinner";
import { VideoPlayer } from "./components/VideoPlayer";
export const DemoSection = (): JSX.Element => {
const { t } = useTranslation("home", { keyPrefix: "demo" });
const { getDemoVideoUrl } = useCmsApi();
const { onLinkClick } = useHomepageTracking();
const { data: demoVideoUrl } = useQuery({
queryKey: [DEMO_VIDEO_DATA_KEY],
queryFn: getDemoVideoUrl,
});
return (
<div className="sm:min-h-[calc(100vh-250px)] flex flex-col items-center justify-center gap-10">
<h2 className="text-center text-3xl font-semibold mb-5">{t("title")}</h2>
<div className="max-w-4xl">
{demoVideoUrl !== undefined ? (
<VideoPlayer videoSrc={demoVideoUrl} />
) : (
<Spinner />
)}
</div>
<Link
href="/login"
onClick={(event) => {
onLinkClick({
href: "/login",
label: "SIGN_IN",
event,
});
}}
>
<Button className="mt-2 rounded-full">
{t("start_now")} <LuChevronRight size={24} />
</Button>
</Link>
</div>
);
};

View File

@ -1,46 +0,0 @@
import { useEffect, useRef } from "react";
interface VideoPlayerProps {
videoSrc: string;
}
export const VideoPlayer = ({ videoSrc }: VideoPlayerProps): JSX.Element => {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const videoElement = videoRef.current;
const handleScroll = () => {
if (!videoElement) {
return;
}
const videoRect = videoElement.getBoundingClientRect();
const isVideoVisible =
videoRect.top >= 0 &&
videoRect.bottom <= window.innerHeight &&
videoElement.checkVisibility();
if (isVideoVisible && videoElement.paused) {
void videoElement.play();
} else if (!isVideoVisible && !videoElement.paused) {
videoElement.pause();
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<video
className="rounded-md shadow-lg dark:shadow-white/25 border dark:border-white/25 w-full"
ref={videoRef}
src={videoSrc}
muted
loop
/>
);
};

View File

@ -1,90 +0,0 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { FaGithub, FaLinkedin } from "react-icons/fa";
import { LuChevronRight } from "react-icons/lu";
import { RiTwitterXLine } from "react-icons/ri";
import Button from "@/lib/components/ui/Button";
import { GITHUB_URL, LINKEDIN_URL, TWITTER_URL } from "@/lib/config/CONSTANTS";
import { useHomepageTracking } from "../../hooks/useHomepageTracking";
export const FooterSection = (): JSX.Element => {
const { t } = useTranslation("home", { keyPrefix: "footer" });
const { onLinkClick } = useHomepageTracking();
return (
<div className="flex flex-col items-center gap-10 text-white text-center text-lg">
<h2 className="text-3xl">{t("title")}</h2>
<p>
{t("description_1")} <br /> {t("description_2")}{" "}
</p>
<div className="flex items-center justify-center gap-5 flex-wrap">
<Link
href="/login"
onClick={(event) => {
onLinkClick({
href: "/login",
label: "SIGN_IN",
event,
});
}}
>
<Button className=" rounded-full">
{t("start_using")}
<LuChevronRight size={24} />
</Button>
</Link>
<Link
href="/contact"
onClick={(event) => {
onLinkClick({
href: "/contact",
label: "CONTACT",
event,
});
}}
>
<Button variant="tertiary">
{t("contact_sales")} <LuChevronRight size={24} />
</Button>
</Link>
</div>
<ul className="flex gap-10 mt-5 mb-10 text-black">
<li>
<a
href={LINKEDIN_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="Quivr LinkedIn"
className="hover:text-black"
>
<FaLinkedin size={52} />
</a>
</li>
<li>
<a
href={TWITTER_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="Quivr Twitter"
className="hover:text-black"
>
<RiTwitterXLine size={52} />
</a>
</li>
<li>
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="Quivr GitHub"
className="hover:text-black"
>
<FaGithub size={52} />
</a>
</li>
</ul>
</div>
);
};

View File

@ -1,77 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { LuChevronRight } from "react-icons/lu";
import Button from "@/lib/components/ui/Button";
import { useHomepageTracking } from "../../hooks/useHomepageTracking";
export const IntroSection = (): JSX.Element => {
const { t } = useTranslation("home", { keyPrefix: "intro" });
const laptopImage = "/Homepage/laptop-demo.png";
const smartphoneImage = "/Homepage/smartphone-demo.png";
const { onLinkClick } = useHomepageTracking();
return (
<>
<div className="flex flex-col lg:flex-row items-center justify-center md:justify-start gap-10 lg:gap-0 xl:gap-10 lg:h-[calc(100vh-250px)] mb-[calc(50vw*tan(6deg))] md:mb-0">
<div className="w-[80vw] lg:w-[50%] lg:shrink-0 flex flex-col justify-center gap-10 sm:gap-20 lg:gap-32 xl:gap-36">
<div>
<h1 className="text-5xl leading-[4rem] sm:text-6xl sm:leading-[5rem] lg:text-[4.2rem] lg:leading-[6rem] font-bold text-black block max-w-2xl">
{t("title")} <span className="text-primary">Quivr</span>
</h1>
<br />
<p className="text-xl">{t("subtitle")}</p>
</div>
<div className="flex flex-col items-start sm:flex-row sm:items-center gap-5">
<Link
href="/login"
onClick={(event) =>
onLinkClick({
href: "/login",
label: "SIGN_IN",
event,
})
}
>
<Button className="text-white bg-black rounded-full">
{t("try_demo")} <LuChevronRight size={24} />
</Button>
</Link>
<Link
href="/contact"
onClick={(event) => {
onLinkClick({
href: "/contact",
label: "CONTACT_SALES",
event,
});
}}
>
<Button variant="tertiary" className="font-semibold">
{t("contact_sales")} <LuChevronRight size={24} />
</Button>
</Link>
</div>
</div>
<div className="w-[80vw] lg:w-[calc(50vw)] lg:shrink-0 lg:max-h-[calc(80vh-100px)] rounded flex items-center justify-center lg:justify-start">
<Image
src={laptopImage}
alt="Quivr on laptop"
width={1200}
height={1200}
className="hidden sm:block max-w-[calc(80vh-100px)] max-h-[calc(80vh-100px)] xl:scale-125"
/>
<Image
src={smartphoneImage}
alt="Quivr on smartphone"
width={640}
height={640}
className="sm:hidden"
/>
</div>
</div>
</>
);
};

View File

@ -1,3 +0,0 @@
export * from "./DemoSection/DemoSection";
export * from "./FooterSection";
export * from "./IntroSection";

View File

@ -1,77 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { LuChevronRight, LuShieldCheck } from "react-icons/lu";
import { SECURITY_QUESTIONS_DATA_KEY } from "@/lib/api/cms/config";
import { useCmsApi } from "@/lib/api/cms/useCmsApi";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/lib/components/ui/Accordion";
import Button from "@/lib/components/ui/Button";
import Spinner from "@/lib/components/ui/Spinner";
import { useHomepageTracking } from "../../hooks/useHomepageTracking";
export const SecuritySection = (): JSX.Element => {
const { t } = useTranslation("home", {
keyPrefix: "security",
});
const { onLinkClick } = useHomepageTracking();
const { getSecurityQuestions } = useCmsApi();
const { data: securityQuestions = [] } = useQuery({
queryKey: [SECURITY_QUESTIONS_DATA_KEY],
queryFn: getSecurityQuestions,
});
if (securityQuestions.length === 0) {
return <Spinner />;
}
return (
<>
<div className="flex flex-1 w-full mb-10 p-6">
<div className="hidden md:flex flex-1 items-center justify-center">
<LuShieldCheck className="text-[150px]" />
</div>
<div className="flex-1">
<Accordion type="multiple">
{securityQuestions.map((question) => {
return (
<AccordionItem
value={question.question}
key={question.question}
>
<AccordionTrigger>{question.question}</AccordionTrigger>
<AccordionContent>{question.answer}</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
</div>
<div className="flex md:justify-end w-full">
<Link
href="/login"
onClick={(event) => {
onLinkClick({
href: "/login",
label: "SIGN_IN",
event,
});
}}
>
<Button className="rounded-full">
{t("cta")}
<LuChevronRight size={24} />
</Button>
</Link>
</div>
</>
);
};

View File

@ -1 +0,0 @@
export * from "./SecuritySection";

View File

@ -1,40 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { TESTIMONIALS_DATA_KEY } from "@/lib/api/cms/config";
import { useCmsApi } from "@/lib/api/cms/useCmsApi";
import Spinner from "@/lib/components/ui/Spinner";
import { TestimonialCard } from "./components/TestimonialCard";
export const TestimonialsSection = (): JSX.Element => {
const { t } = useTranslation("home", {
keyPrefix: "testimonials",
});
const { getTestimonials } = useCmsApi();
const { data: testimonials, isLoading } = useQuery({
queryKey: [TESTIMONIALS_DATA_KEY],
queryFn: getTestimonials,
});
if (isLoading || !testimonials) {
return <Spinner />;
}
return (
<>
<p className="text-4xl font-semibold my-10">
{t("title")} <span className="text-primary">Quivr</span>{" "}
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 mb-5 items-stretch p-4">
{testimonials.map((testimonial) => (
<div key={testimonial.content}>
<TestimonialCard {...testimonial} />
</div>
))}
</div>
</>
);
};

View File

@ -1,45 +0,0 @@
import Link from "next/link";
import { Avatar } from "@/lib/components/ui/Avatar";
import { Testimonial } from "@/lib/types/Testimonial";
import { socialMediaToIcon } from "../utils/socialMediaToIcon";
export const TestimonialCard = ({
socialMedia,
url,
name,
jobTitle,
content,
profilePicture,
}: Testimonial): JSX.Element => {
return (
<div className="px-8 py-4 rounded-3xl shadow-2xl dark:shadow-white/25 w-full bg-white dark:bg-black h-full flex flex-col gap-3">
<Link
href={url}
className="hover:text-black"
target="_blank"
rel="noopener noreferrer"
>
<div className="w-full flex justify-end">
{socialMediaToIcon[socialMedia]}
</div>
</Link>
<p className="flex-1 italic">&quot;{content}&quot;</p>
<div>
<div className="flex mt-3 flex-1 items-center">
<Avatar
url={profilePicture ?? "https://www.gravatar.com/avatar?d=mp"}
imgClassName={"rounded-full"}
className="w-10 h-10"
/>
<div className="flex-1 ml-3">
<p className="font-semibold">{name}</p>
<p className="text-sm">{jobTitle}</p>
</div>
</div>
</div>
</div>
);
};

View File

@ -1 +0,0 @@
export * from "./TestimonialsSection";

View File

@ -1,12 +0,0 @@
import { AiFillLinkedin } from "react-icons/ai";
import { RiTwitterXFill } from "react-icons/ri";
import { Testimonial } from "@/lib/types/Testimonial";
export const socialMediaToIcon: Record<
Testimonial["socialMedia"],
JSX.Element
> = {
linkedin: <AiFillLinkedin />,
x: <RiTwitterXFill />,
};

View File

@ -1,42 +0,0 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { LuChevronRight } from "react-icons/lu";
import Button from "@/lib/components/ui/Button";
import { UseCasesListing } from "./components/UseCasesListing/UseCasesListing";
import { useHomepageTracking } from "../../hooks/useHomepageTracking";
export const UseCases = (): JSX.Element => {
const { t } = useTranslation("home");
const { onLinkClick } = useHomepageTracking();
return (
<div className="text-white w-full">
<div className="mb-3">
<h2 className="text-center text-3xl font-semibold mb-2">
{t("useCases.title")}
</h2>
<p className="text-center text-lg">{t("useCases.subtitle")}</p>
</div>
<UseCasesListing />
<div className="mt-10 flex md:justify-center">
<Link
href="/login"
onClick={(event) => {
onLinkClick({
href: "/login",
label: "SIGN_IN",
event,
});
}}
>
<Button className="bg-black rounded-full">
{t("intro.try_demo")} <LuChevronRight size={24} />
</Button>
</Link>
</div>
</div>
);
};

View File

@ -1,78 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { LuPanelLeft } from "react-icons/lu";
import { useHomepageTracking } from "@/app/(home)/hooks/useHomepageTracking";
import { USE_CASES_DATA_KEY } from "@/lib/api/cms/config";
import { useCmsApi } from "@/lib/api/cms/useCmsApi";
import Spinner from "@/lib/components/ui/Spinner";
import { useDevice } from "@/lib/hooks/useDevice";
import { cn } from "@/lib/utils";
import { UseCaseComponent } from "./components/UseCaseComponent";
export const UseCasesListing = (): JSX.Element => {
const { getUseCases } = useCmsApi();
const { onButtonClick } = useHomepageTracking();
const { data: cases = [], isLoading } = useQuery({
queryKey: [USE_CASES_DATA_KEY],
queryFn: getUseCases,
});
const { isMobile } = useDevice();
const [selectedCaseId, setSelectedCaseId] = useState<string>();
const selectedCase = cases.find((c) => c.id === selectedCaseId);
useEffect(() => {
if (cases.length > 0) {
setSelectedCaseId(cases[0].id);
}
}, [cases]);
if (isLoading || selectedCaseId === undefined) {
return (
<div className="flex justify-center">
<Spinner />
</div>
);
}
const handleUseCaseClick = (id: string) => {
onButtonClick({
label: `USE_CASES_${id}`,
});
if (isMobile) {
return;
}
setSelectedCaseId(id);
};
return (
<div className="grid grid-cols-6 md:gap-10 flex-column items-start ">
<div className={"col-span-6 md:col-span-2 flex flex-col gap-3"}>
{cases.map((c) => (
<div
key={c.id}
onClick={() => handleUseCaseClick(c.id)}
className={cn(
"p-6 rounded-lg cursor-pointer",
selectedCaseId === c.id &&
"md:bg-[#7D73A7] md:border-[1px] md:border-[#6752F5]"
)}
>
<h3 className="font-semibold mb-3">{c.name}</h3>
<p>{c.description}</p>
</div>
))}
</div>
{selectedCase !== undefined && (
<div className="hidden md:block col-span-4 bg-white rounded-xl md:p-6 px-10 m-6">
<LuPanelLeft className="text-black text-xl" />
<UseCaseComponent discussions={selectedCase.discussions} />
</div>
)}
</div>
);
};

View File

@ -1,38 +0,0 @@
import { Fragment } from "react";
import { PiPaperclipFill } from "react-icons/pi";
import { UseCase } from "@/lib/types/UseCase";
type UseCaseComponentProps = {
discussions: UseCase["discussions"];
};
export const UseCaseComponent = ({
discussions,
}: UseCaseComponentProps): JSX.Element => {
return (
<div className="flex flex-col gap-2 text-black">
{discussions.map((d) => (
<Fragment key={d.question}>
<div className="flex justify-end">
<div className="bg-[#9B9B9B] max-w-[75%] bg-opacity-10 p-4 rounded-xl">
<p>{d.question}</p>
</div>
</div>
<div className="flex">
<div className="bg-[#E0DDFC] max-w-[75%] rounded-xl p-4">
<span className="text-[#8F8F8F] text-xs">@Quivr</span>
<p>{d.answer}</p>
</div>
</div>
</Fragment>
))}
<div className="flex flex-col w-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 mt-10">
<div className="flex items-center">
<PiPaperclipFill className="text-3xl" />
<span className="text-[#BFBFBF]">@Einstein</span>
</div>
</div>
</div>
);
};

View File

@ -1 +0,0 @@
export * from "./UseCasesListing";

View File

@ -1 +0,0 @@
export * from "./UseCasesListing";

View File

@ -1,5 +0,0 @@
export * from "./HomeHeader/HomeHeader";
export * from "./HomeSection/HomeSection";
export * from "./Sections";
export * from "./SecuritySection";
export * from "./TestimonialsSection";

View File

@ -1,33 +0,0 @@
import { useRouter } from "next/navigation";
import { MouseEvent } from "react";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useHomepageTracking = () => {
const { track } = useEventTracking();
const router = useRouter();
const onLinkClick = ({
href,
label,
event,
}: {
href: string;
label: string;
event: MouseEvent<HTMLAnchorElement>;
}) => {
event.preventDefault();
void track(`HOMEPAGE-${label}`);
router.push(href);
};
const onButtonClick = ({ label }: { label: string }) => {
void track(`HOMEPAGE-${label}`);
};
return {
onLinkClick,
onButtonClick,
};
};

View File

@ -1,65 +0,0 @@
"use client";
import Link from "next/link";
import { Suspense } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
import { Divider } from "@/lib/components/ui/Divider";
import { useAuthModes } from "@/lib/hooks/useAuthModes";
import { EmailLogin } from "../(auth)/login/components/EmailLogin";
import { GoogleLoginButton } from "../(auth)/login/components/GoogleLogin";
import { useLogin } from "../(auth)/login/hooks/useLogin";
import { EmailAuthContextType } from "../(auth)/login/types";
const Main = (): JSX.Element => {
useLogin();
const { googleSso, password, magicLink } = useAuthModes();
const methods = useForm<EmailAuthContextType>({
defaultValues: {
email: "",
password: "",
},
});
const { t } = useTranslation(["translation", "login"]);
return (
<div className="w-screen h-screen bg-ivory" data-testid="sign-in-card">
<main className="h-full flex flex-col items-center justify-center">
<section className="w-full md:w-1/2 lg:w-1/3 flex flex-col gap-2">
<Link href="/" className="flex justify-center">
<QuivrLogo size={80} color="black" />
</Link>
<p className="text-center text-4xl font-medium">
{t("talk_to", { ns: "login" })}{" "}
<span className="text-primary">Quivr</span>
</p>
<div className="mt-5 flex flex-col">
<FormProvider {...methods}>
<EmailLogin />
</FormProvider>
{googleSso && (password || magicLink) && (
<Divider text={t("or")} className="my-3 uppercase" />
)}
{googleSso && <GoogleLoginButton />}
</div>
<p className="text-[10px] text-center">
{t("restriction_message", { ns: "login" })}
</p>
</section>
</main>
</div>
);
};
const Login = (): JSX.Element => {
return (
<Suspense fallback="Loading...">
<Main />
</Suspense>
);
};
export default Login;

View File

@ -1,86 +0,0 @@
import { SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { LuChevronRight } from "react-icons/lu";
import Button from "@/lib/components/ui/Button";
import Spinner from "@/lib/components/ui/Spinner";
import { emailPattern } from "@/lib/config/patterns";
import { usePostContactSales } from "../hooks/usePostContactSales";
export const ContactForm = (): JSX.Element => {
const { t } = useTranslation("contact", { keyPrefix: "form" });
const { register, handleSubmit, formState } = useForm({
defaultValues: { email: "", message: "" },
});
const postEmail = usePostContactSales();
const onSubmit: SubmitHandler<{ email: string; message: string }> = (
data,
event
) => {
event?.preventDefault();
postEmail.mutate({
customer_email: data.email,
content: data.message,
});
};
if (postEmail.isSuccess) {
return (
<div className="flex flex-col items-center justify-center gap-5">
<h2 className="text-2xl font-bold">{t("thank_you")}</h2>
<p className="text-center text-zinc-400">{t("thank_you_text")}</p>
</div>
);
}
if (postEmail.isPending) {
return <Spinner />;
}
return (
<form
className="flex flex-col gap-5 justify-stretch w-full"
onSubmit={(event) => void handleSubmit(onSubmit)(event)}
>
<fieldset className="grid grid-cols-1 sm:grid-cols-3 gap-2 w-full gap-y-5">
<label className="font-bold" htmlFor="email">
{t("email")}
<sup>*</sup>:
</label>
<input
type="email"
{...register("email", {
pattern: emailPattern,
required: true,
})}
placeholder="jane@example.com"
className="col-span-2 bg-[#FCFAF6] rounded-md p-2"
/>
<label className="font-bold" htmlFor="message">
{t("question")}
<sup>*</sup>:
</label>
<textarea
{...register("message", {
required: true,
})}
rows={3}
placeholder={t("placeholder_question")}
className="col-span-2 bg-[#FCFAF6] rounded-md p-2"
></textarea>
</fieldset>
<Button
className="self-end rounded-full bg-primary flex items-center justify-center gap-2 border-none hover:bg-primary/90"
disabled={!formState.isValid}
>
{t("submit")}
<LuChevronRight size={24} />
</Button>
</form>
);
};

View File

@ -1 +0,0 @@
export * from "./ContactForm";

View File

@ -1,33 +0,0 @@
import { useMutation, UseMutationResult } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useAxios } from "@/lib/hooks";
import { useToast } from "@/lib/hooks/useToast";
interface ContactSalesDto {
customer_email: string;
content: string;
}
export const usePostContactSales = (): UseMutationResult<
void,
unknown,
ContactSalesDto
> => {
const { axiosInstance } = useAxios();
const toast = useToast();
const { t } = useTranslation("contact", { keyPrefix: "form" });
return useMutation({
mutationKey: ["contactSales"],
mutationFn: async (data) => {
await axiosInstance.post("/contact", data);
},
onError: () => {
toast.publish({
text: t("sending_mail_error"),
variant: "danger",
});
},
});
};

View File

@ -1,41 +0,0 @@
"use client";
import { useTranslation } from "react-i18next";
import Card from "@/lib/components/ui/Card";
import { ContactForm } from "./components";
import {
FooterSection,
HomeHeader,
HomeSection,
TestimonialsSection,
} from "../(home)/components";
const ContactSalesPage = (): JSX.Element => {
const { t } = useTranslation("contact");
return (
<div className="bg-[#FCFAF6]">
<HomeHeader color="black" />
<main className="relative flex flex-col items-center px-10">
<h1 className="text-4xl font-semibold my-10 text-center">
{t("speak_to")}{" "}
<span className="text-primary">{t("sales_team")}</span>
</h1>
<Card className="flex flex-col items-center mt-5 mb-10 p-10 w-full max-w-xl">
<ContactForm />
</Card>
<HomeSection bg="bg-[#FCFAF6]">
<TestimonialsSection />
</HomeSection>
<HomeSection bg="bg-gradient-to-b from-[#D07DF9] to-[#7A27FD]">
<FooterSection />
</HomeSection>
</main>
</div>
);
};
export default ContactSalesPage;

View File

@ -1,50 +0,0 @@
"use client";
import { StripePricingTable } from "@/lib/components/Stripe/PricingModal/components/PricingTable/PricingTable";
import {
FooterSection,
HomeHeader,
HomeSection,
TestimonialsSection,
} from "../(home)/components";
import { UseCases } from "../(home)/components/UseCases/UseCases";
const ContactSalesPage = (): JSX.Element => {
return (
<div className="bg-[#FCFAF6]">
<HomeHeader color="black" />
<main className="relative flex flex-col items-center px-10">
<section className="flex flex-col h-fit mt-5 mb-10 p-10 w-full">
<div className="rounded-xl overflow-hidden">
<div className="p-8 text-center">
<h1 className="text-6xl font-bold text-primary mb-4">Pricing</h1>
<p className="text-2xl font-semibold text-gray-700 mb-6">
Explore our extensive free tier, or upgrade for more features.
</p>
</div>
{/* Stripe Pricing Table */}
<StripePricingTable />
</div>
</section>
<HomeSection bg="bg-[#362469]">
<UseCases />
<div />
</HomeSection>
<HomeSection bg="bg-[#FCFAF6]">
<TestimonialsSection />
</HomeSection>
<HomeSection bg="bg-gradient-to-b from-[#D07DF9] to-[#7A27FD]">
<FooterSection />
</HomeSection>
</main>
</div>
);
};
export default ContactSalesPage;

View File

@ -1,22 +0,0 @@
import { forwardRef } from "react";
import { TabsTrigger } from "@/lib/components/ui/Tabs";
import { cn } from "@/lib/utils";
export const StyledTabsTrigger = forwardRef<
React.ElementRef<typeof TabsTrigger>,
React.ComponentPropsWithoutRef<typeof TabsTrigger>
>(({ className, ...props }, ref) => (
<TabsTrigger
ref={ref}
className={cn(
"capitalize font-normal",
"data-[state=active]:shadow-none",
"data-[state=active]:text-primary",
"data-[state=active]:font-semibold",
className
)}
{...props}
/>
));
StyledTabsTrigger.displayName = TabsTrigger.displayName;

View File

@ -11,7 +11,7 @@ import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider
import { ButtonType } from "@/lib/types/QuivrButton";
import { Tab } from "@/lib/types/Tab";
import { ManageBrains } from "./components/BrainsTabs/components/ManageBrains/ManageBrains";
import { ManageBrains } from "./BrainsTabs/components/ManageBrains/ManageBrains";
import styles from "./page.module.scss";
const Studio = (): JSX.Element => {

View File

@ -1,241 +0,0 @@
/* eslint-disable max-lines */
import { renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { Subscription } from "../brain";
import {
CreateBrainInput,
SubscriptionUpdatableProperties,
UpdateBrainInput,
} from "../types";
import { useBrainApi } from "../useBrainApi";
const axiosGetMock = vi.fn(() => ({
data: {
documents: [],
},
}));
const axiosPostMock = vi.fn(() => ({
data: {
id: "123",
name: "Test Brain",
},
}));
const axiosDeleteMock = vi.fn(() => ({}));
const axiosPutMock = vi.fn(() => ({}));
vi.mock("@/lib/hooks", () => ({
useAxios: vi.fn(() => ({
axiosInstance: {
get: axiosGetMock,
post: axiosPostMock,
delete: axiosDeleteMock,
put: axiosPutMock,
},
})),
}));
describe("useBrainApi", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("should call createBrain with the correct parameters", async () => {
const {
result: {
current: { createBrain },
},
} = renderHook(() => useBrainApi());
const brain: CreateBrainInput = {
name: "Test Brain",
description: "This is a description",
status: "public",
model: "gpt-3.5-turbo",
temperature: 0.0,
max_tokens: 256,
};
await createBrain(brain);
expect(axiosPostMock).toHaveBeenCalledTimes(1);
expect(axiosPostMock).toHaveBeenCalledWith("/brains/", brain);
});
it("should call deleteBrain with the correct parameters", async () => {
const {
result: {
current: { deleteBrain },
},
} = renderHook(() => useBrainApi());
const id = "123";
await deleteBrain(id);
expect(axiosDeleteMock).toHaveBeenCalledTimes(1);
expect(axiosDeleteMock).toHaveBeenCalledWith(`/brains/${id}/subscription`);
});
it("should call getDefaultBrain with the correct parameters", async () => {
const {
result: {
current: { getDefaultBrain },
},
} = renderHook(() => useBrainApi());
await getDefaultBrain();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith("/brains/default/");
});
it("should call getBrains with the correct parameters", async () => {
axiosGetMock.mockImplementationOnce(() => ({
data: {
//@ts-ignore we don't really need returned value here
brains: [],
},
}));
const {
result: {
current: { getBrains },
},
} = renderHook(() => useBrainApi());
await getBrains();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith("/brains/");
});
it("should call getBrain with the correct parameters", async () => {
const {
result: {
current: { getBrain },
},
} = renderHook(() => useBrainApi());
const id = "123";
await getBrain(id);
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/`);
});
it("should call addBrainSubscription with the correct parameters", async () => {
const {
result: {
current: { addBrainSubscriptions },
},
} = renderHook(() => useBrainApi());
const id = "123";
const subscriptions: Subscription[] = [
{
email: "user@quivr.app",
role: "Viewer",
},
];
await addBrainSubscriptions(id, subscriptions);
expect(axiosPostMock).toHaveBeenCalledTimes(1);
expect(axiosPostMock).toHaveBeenCalledWith(`/brains/${id}/subscription`, [
{
email: "user@quivr.app",
rights: "Viewer",
},
]);
});
it("should call getBrainUsers with the correct parameters", async () => {
axiosGetMock.mockImplementationOnce(() => ({
//@ts-ignore we don't really need returned value here
data: [],
}));
const {
result: {
current: { getBrainUsers },
},
} = renderHook(() => useBrainApi());
const id = "123";
await getBrainUsers(id);
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/users`);
});
it("should call updateBrainAccess with the correct parameters", async () => {
const {
result: {
current: { updateBrainAccess },
},
} = renderHook(() => useBrainApi());
const brainId = "123";
const email = "456";
const subscription: SubscriptionUpdatableProperties = {
role: "Viewer",
};
await updateBrainAccess(brainId, email, subscription);
expect(axiosPutMock).toHaveBeenCalledTimes(1);
expect(axiosPutMock).toHaveBeenCalledWith(
`/brains/${brainId}/subscription`,
{ rights: "Viewer", email }
);
});
it("should call setAsDefaultBrain with correct brainId", async () => {
const {
result: {
current: { setAsDefaultBrain },
},
} = renderHook(() => useBrainApi());
const brainId = "123";
await setAsDefaultBrain(brainId);
expect(axiosPostMock).toHaveBeenCalledTimes(1);
expect(axiosPostMock).toHaveBeenCalledWith(`/brains/${brainId}/default`);
});
it("should call updateBrain with correct brainId and brain", async () => {
const {
result: {
current: { updateBrain },
},
} = renderHook(() => useBrainApi());
const brainId = "123";
const brain: UpdateBrainInput = {
name: "Test Brain",
description: "This is a description",
status: "public",
model: "gpt-3.5-turbo",
temperature: 0.0,
max_tokens: 256,
};
await updateBrain(brainId, brain);
expect(axiosPutMock).toHaveBeenCalledTimes(1);
expect(axiosPutMock).toHaveBeenCalledWith(`/brains/${brainId}/`, brain);
});
it("should call getPublicBrains with correct parameters", async () => {
const {
result: {
current: { getPublicBrains },
},
} = renderHook(() => useBrainApi());
await getPublicBrains();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/public`);
});
it("should call updateBrainSecrets with correct parameters", async () => {
const {
result: {
current: { updateBrainSecrets },
},
} = renderHook(() => useBrainApi());
const brainId = "123";
const secrets = {
key: "value",
};
await updateBrainSecrets(brainId, secrets);
expect(axiosPutMock).toHaveBeenCalledTimes(1);
expect(axiosPutMock).toHaveBeenCalledWith(
`/brains/${brainId}/secrets-values`,
secrets
);
});
});

View File

@ -1,56 +0,0 @@
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import * as React from "react";
import { LuChevronDown } from "react-icons/lu";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={className} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 transition-all [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<LuChevronDown className="h-8 w-8 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm font-light transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className
)}
{...props}
>
<div className="pb-4 pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

View File

@ -1,26 +0,0 @@
import Image from "next/image";
import { cn } from "@/lib/utils";
type AvatarProps = {
url: string;
imgClassName?: string;
className?: string;
};
export const Avatar = ({
url,
imgClassName,
className,
}: AvatarProps): JSX.Element => {
return (
<div className={cn("relative w-8 h-8 shrink-0", className)}>
<Image
alt="avatar"
fill={true}
sizes="32px"
src={url}
className={cn("rounded-xl", imgClassName)}
/>
</div>
);
};

View File

@ -1,26 +0,0 @@
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import * as React from "react";
import { LuCheck } from "react-icons/lu";
import { cn } from "@/lib/utils";
export const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<LuCheck className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;

View File

@ -1,22 +0,0 @@
import { HTMLProps } from "react";
import { cn } from "@/lib/utils";
export const Chip = ({
label,
children,
className,
...restProps
}: HTMLProps<HTMLSpanElement>): JSX.Element => {
return (
<span
className={cn(
"px-2 bg-gray-400 text-black rounded-xl text-sm flex items-center justify-center",
className
)}
{...restProps}
>
{label ?? children}
</span>
);
};

View File

@ -1,43 +0,0 @@
/* eslint-disable */
"use client";
import { HTMLAttributes } from "react";
import { cn } from "@/lib/utils";
import Tooltip from "./Tooltip/Tooltip";
interface EllipsisProps extends HTMLAttributes<HTMLDivElement> {
children: string;
maxCharacters: number;
tooltip?: boolean;
}
const Ellipsis = ({
children: originalContent,
className,
maxCharacters,
tooltip = false,
}: EllipsisProps): JSX.Element => {
const renderedContent =
originalContent.length > maxCharacters
? `${originalContent.slice(0, maxCharacters)}...`
: originalContent;
if (tooltip && originalContent !== renderedContent) {
return (
<Tooltip tooltip={originalContent}>
<span aria-label={originalContent} className={cn("", className)}>
{renderedContent}
</span>
</Tooltip>
);
}
return (
<span aria-label={originalContent} className={cn("", className)}>
{renderedContent}
</span>
);
};
export default Ellipsis;

View File

@ -1,55 +0,0 @@
"use client";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsContent, TabsList, TabsTrigger };