mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-11-09 20:47:28 +03:00
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:
parent
34a521e3d8
commit
1b75e09420
@ -1,8 +0,0 @@
|
||||
.bg-slanted-upwards {
|
||||
clip-path: polygon(
|
||||
0 0,
|
||||
100% 0,
|
||||
100% max(70px, calc(100% - 100vw * tan(12deg))),
|
||||
0 100%
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
export const linkStyle = {
|
||||
white: "text-white hover:text-slate-200",
|
||||
black: "text-black",
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
export type NavbarItem = {
|
||||
href: string;
|
||||
label: string;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode | null;
|
||||
newTab?: boolean;
|
||||
className?: string;
|
||||
};
|
@ -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));
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
export * from "./DemoSection/DemoSection";
|
||||
export * from "./FooterSection";
|
||||
export * from "./IntroSection";
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from "./SecuritySection";
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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">"{content}"</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>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from "./TestimonialsSection";
|
@ -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 />,
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from "./UseCasesListing";
|
@ -1 +0,0 @@
|
||||
export * from "./UseCasesListing";
|
@ -1,5 +0,0 @@
|
||||
export * from "./HomeHeader/HomeHeader";
|
||||
export * from "./HomeSection/HomeSection";
|
||||
export * from "./Sections";
|
||||
export * from "./SecuritySection";
|
||||
export * from "./TestimonialsSection";
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from "./ContactForm";
|
@ -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",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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 => {
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
@ -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 };
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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 };
|
Loading…
Reference in New Issue
Block a user