mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat: updated homepage and pricing
This commit is contained in:
parent
0fc241e5fa
commit
6415d4fd57
27 changed files with 857 additions and 998 deletions
12
surfsense_web/app/(home)/contact/page.tsx
Normal file
12
surfsense_web/app/(home)/contact/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import { ContactFormGridWithDetails } from "@/components/contact/contact-form";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<ContactFormGridWithDetails />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
14
surfsense_web/app/(home)/layout.tsx
Normal file
14
surfsense_web/app/(home)/layout.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { Footer } from "@/components/homepage/footer";
|
||||
import { Navbar } from "@/components/homepage/navbar";
|
||||
|
||||
export default function HomePageLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white">
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { Footer } from "@/components/Footer";
|
||||
import { CTAHomepage } from "@/components/homepage/cta";
|
||||
import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid";
|
||||
import { ModernHeroWithGradients } from "@/components/homepage/ModernHeroWithGradients";
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { FeaturesCards } from "@/components/homepage/features-card";
|
||||
import { Footer } from "@/components/homepage/footer";
|
||||
import { HeroSection } from "@/components/homepage/hero-section";
|
||||
import ExternalIntegrations from "@/components/homepage/integrations";
|
||||
import { Navbar } from "@/components/homepage/navbar";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white">
|
||||
<Navbar />
|
||||
<ModernHeroWithGradients />
|
||||
<HeroSection />
|
||||
<FeaturesCards />
|
||||
<FeaturesBentoGrid />
|
||||
<ExternalIntegrations />
|
||||
<CTAHomepage />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
12
surfsense_web/app/(home)/pricing/page.tsx
Normal file
12
surfsense_web/app/(home)/pricing/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import PricingBasic from "@/components/pricing/pricing-section";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<PricingBasic />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react'
|
||||
import { ContactFormGridWithDetails } from '@/components/contact/contact-form'
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<ContactFormGridWithDetails />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react'
|
||||
import PricingBasic from '@/components/pricing/pricing-section'
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<PricingBasic />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
"use client";
|
||||
import { IconMail, IconMenu2, IconUser, IconUserPlus, IconX } from "@tabler/icons-react";
|
||||
import { AnimatePresence, motion, useMotionValueEvent, useScroll } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Logo } from "./Logo";
|
||||
import { ThemeTogglerComponent } from "./theme/theme-toggle";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface NavbarProps {
|
||||
navItems: {
|
||||
name: string;
|
||||
link: string;
|
||||
}[];
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export const Navbar = () => {
|
||||
const navItems = [
|
||||
{
|
||||
name: "Docs",
|
||||
link: "/docs",
|
||||
},
|
||||
// {
|
||||
// name: "Product",
|
||||
// link: "/#product",
|
||||
// },
|
||||
// {
|
||||
// name: "Pricing",
|
||||
// link: "/#pricing",
|
||||
// },
|
||||
];
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { scrollY } = useScroll({
|
||||
target: ref,
|
||||
offset: ["start start", "end start"],
|
||||
});
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
|
||||
useMotionValueEvent(scrollY, "change", (latest) => {
|
||||
if (latest > 100) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div ref={ref} className="w-full fixed top-2 inset-x-0 z-50">
|
||||
<DesktopNav visible={visible} navItems={navItems} />
|
||||
<MobileNav visible={visible} navItems={navItems} />
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopNav = ({ navItems, visible }: NavbarProps) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
// Redirect to the login page
|
||||
window.location.href = "/login";
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
animate={{
|
||||
backdropFilter: "blur(16px)",
|
||||
background: visible
|
||||
? "rgba(var(--background-rgb), 0.8)"
|
||||
: "rgba(var(--background-rgb), 0.6)",
|
||||
width: visible ? "38%" : "80%",
|
||||
height: visible ? "48px" : "64px",
|
||||
y: visible ? 8 : 0,
|
||||
}}
|
||||
initial={{
|
||||
width: "80%",
|
||||
height: "64px",
|
||||
background: "rgba(var(--background-rgb), 0.6)",
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
className={cn(
|
||||
"hidden lg:flex flex-row self-center items-center justify-between py-2 mx-auto px-6 rounded-full relative z-[60] backdrop-saturate-[1.8]",
|
||||
visible ? "border dark:border-white/10 border-gray-300/30" : "border-0"
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--background-rgb": "var(--tw-dark) ? '0, 0, 0' : '255, 255, 255'",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Logo className="h-8 w-8 rounded-md" />
|
||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<motion.div
|
||||
className="lg:flex flex-row items-center justify-end space-x-1 text-sm"
|
||||
animate={{
|
||||
scale: visible ? 0.9 : 1,
|
||||
}}
|
||||
>
|
||||
{navItems.map((navItem, idx) => (
|
||||
<motion.div
|
||||
key={`nav-item-${navItem.name}`}
|
||||
onHoverStart={() => setHoveredIndex(idx)}
|
||||
className="relative"
|
||||
>
|
||||
<Link
|
||||
className="dark:text-white/90 text-gray-800 relative px-3 py-1.5 transition-colors"
|
||||
href={navItem.link}
|
||||
>
|
||||
<span className="relative z-10">{navItem.name}</span>
|
||||
{hoveredIndex === idx && (
|
||||
<motion.div
|
||||
layoutId="menu-hover"
|
||||
className="absolute inset-0 rounded-full dark:bg-gradient-to-r dark:from-white/10 dark:to-white/20 bg-gradient-to-r from-gray-200 to-gray-300"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1.1,
|
||||
background:
|
||||
"var(--tw-dark) ? radial-gradient(circle at center, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 50%, transparent 100%) : radial-gradient(circle at center, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.03) 50%, transparent 100%)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
bounce: 0.4,
|
||||
duration: 0.4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
<ThemeTogglerComponent />
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{!visible && (
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 25,
|
||||
},
|
||||
}}
|
||||
exit={{
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
},
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Link href="/contact">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden cursor-pointer md:flex items-center gap-2 rounded-full bg-white/10 dark:bg-white/10 backdrop-blur-lg hover:bg-white/20 dark:hover:bg-white/20 text-gray-900 dark:text-white border-white/30 transition-all"
|
||||
>
|
||||
<IconUserPlus className="h-4 w-4" />
|
||||
<span>Sign Up</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
onClick={handleGoogleLogin}
|
||||
variant="outline"
|
||||
className="hidden cursor-pointer md:flex items-center gap-2 rounded-full dark:bg-white/20 dark:hover:bg-white/30 dark:text-white bg-gray-100 hover:bg-gray-200 text-gray-800 border-0"
|
||||
>
|
||||
<IconUser className="h-4 w-4" />
|
||||
<span>Sign in</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNav = ({ navItems, visible }: NavbarProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
// Redirect to the login page
|
||||
window.location.href = "./login";
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{
|
||||
backdropFilter: "blur(16px)",
|
||||
background: visible
|
||||
? "rgba(var(--background-rgb), 0.8)"
|
||||
: "rgba(var(--background-rgb), 0.6)",
|
||||
width: visible ? "80%" : "90%",
|
||||
y: visible ? 0 : 8,
|
||||
borderRadius: open ? "24px" : "full",
|
||||
padding: "8px 16px",
|
||||
}}
|
||||
initial={{
|
||||
width: "80%",
|
||||
background: "rgba(var(--background-rgb), 0.6)",
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
className={cn(
|
||||
"flex relative flex-col lg:hidden w-full justify-between items-center max-w-[calc(100vw-2rem)] mx-auto z-50 backdrop-saturate-[1.8] rounded-full",
|
||||
visible ? "border border-solid dark:border-white/40 border-gray-300/30" : "border-0"
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--background-rgb": "var(--tw-dark) ? '0, 0, 0' : '255, 255, 255'",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row justify-between items-center w-full">
|
||||
<Logo className="h-8 w-8 rounded-md" />
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeTogglerComponent />
|
||||
{open ? (
|
||||
<IconX className="dark:text-white/90 text-gray-800" onClick={() => setOpen(!open)} />
|
||||
) : (
|
||||
<IconMenu2
|
||||
className="dark:text-white/90 text-gray-800"
|
||||
onClick={() => setOpen(!open)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
className="flex rounded-3xl absolute top-16 dark:bg-black/80 bg-white/90 backdrop-blur-xl backdrop-saturate-[1.8] inset-x-0 z-50 flex-col items-start justify-start gap-4 w-full px-6 py-8"
|
||||
>
|
||||
{navItems.map((navItem: { link: string; name: string }) => (
|
||||
<Link
|
||||
key={`link-${navItem.name}`}
|
||||
href={navItem.link}
|
||||
onClick={() => setOpen(false)}
|
||||
className="relative dark:text-white/90 text-gray-800 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<motion.span className="block">{navItem.name}</motion.span>
|
||||
</Link>
|
||||
))}
|
||||
<Link href="/contact" className="w-full" onClick={() => setOpen(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex cursor-pointer items-center gap-2 mt-4 w-full justify-center rounded-full bg-white/10 dark:bg-white/10 backdrop-blur-lg hover:bg-white/20 dark:hover:bg-white/20 text-gray-900 dark:text-white border-white/30 transition-all"
|
||||
>
|
||||
<IconUserPlus className="h-4 w-4" />
|
||||
<span>Sign Up</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
onClick={handleGoogleLogin}
|
||||
variant="outline"
|
||||
className="flex cursor-pointer items-center gap-2 mt-4 w-full justify-center rounded-full dark:bg-white/20 dark:hover:bg-white/30 dark:text-white bg-gray-100 hover:bg-gray-200 text-gray-800 border-0"
|
||||
>
|
||||
<IconUser className="h-4 w-4" />
|
||||
<span>Sign in</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,549 +0,0 @@
|
|||
"use client";
|
||||
import { IconBrandDiscord, IconBrandGithub, IconFileTypeDoc, IconMail, IconUserPlus } from "@tabler/icons-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Logo } from "../Logo";
|
||||
import { Features } from "./features-card";
|
||||
|
||||
export function ModernHeroWithGradients() {
|
||||
return (
|
||||
<div className="relative h-full min-h-[50rem] w-full">
|
||||
<div className="relative z-20 mx-auto w-full px-4 py-6 md:px-8 lg:px-4">
|
||||
<div className="relative my-12 overflow-hidden rounded-3xl bg-white py-16 shadow-sm dark:bg-gray-900/80 dark:shadow-lg dark:shadow-purple-900/10 md:py-48 mx-auto w-full max-w-[95%] xl:max-w-[98%]">
|
||||
<TopLines />
|
||||
<BottomLines />
|
||||
<SideLines />
|
||||
<TopGradient />
|
||||
<BottomGradient />
|
||||
<DarkModeGradient />
|
||||
|
||||
<div className="relative z-20 flex flex-col items-center justify-center overflow-hidden rounded-3xl p-4 md:p-12 lg:p-16">
|
||||
<div className="flex justify-center w-full mb-4">
|
||||
<Link
|
||||
href="https://github.com/MODSetter/SurfSense"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src="https://trendshift.io/api/badge/repositories/13606"
|
||||
alt="MODSetter%2FSurfSense | Trendshift"
|
||||
style={{ width: "250px", height: "55px" }}
|
||||
width={250}
|
||||
height={55}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="flex items-center mt-8 gap-1 rounded-full border border-gray-200 bg-gradient-to-b from-gray-50 to-gray-100 px-4 py-1 text-center text-sm text-gray-800 shadow-sm dark:border-[#404040] dark:bg-gradient-to-b dark:from-[#5B5B5D] dark:to-[#262627] dark:text-white dark:shadow-inner dark:shadow-purple-500/10"
|
||||
>
|
||||
<IconFileTypeDoc className="h-4 w-4 text-gray-800 dark:text-white" />
|
||||
<span>Documentation</span>
|
||||
</Link>
|
||||
{/* Import the Logo component or define it in this file */}
|
||||
<div className="flex items-center justify-center gap-4 mt-2">
|
||||
<div className="h-16 w-16">
|
||||
<Logo className="rounded-md" />
|
||||
</div>
|
||||
<h1 className="bg-gradient-to-b from-gray-800 to-gray-600 bg-clip-text py-4 text-center text-3xl text-transparent dark:from-white dark:to-purple-300 md:text-5xl lg:text-8xl">
|
||||
SurfSense
|
||||
</h1>
|
||||
</div>
|
||||
<p className="mx-auto max-w-3xl pb-10 text-center text-base text-gray-600 dark:text-neutral-300 md:text-lg lg:text-xl">
|
||||
Your all-in-one AI research workspace.
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-6 py-6 sm:flex-row">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="w-48 gap-1 rounded-full border border-white/30 bg-white/10 backdrop-blur-lg px-5 py-3 text-center text-sm font-medium text-gray-900 dark:text-white shadow-lg hover:bg-white/20 dark:hover:bg-white/20 transition-all flex items-center justify-center"
|
||||
>
|
||||
<IconUserPlus className="h-5 w-5 mr-2" />
|
||||
<span>Sign Up</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://discord.gg/ejRNvftDp9"
|
||||
className="w-48 gap-1 rounded-full border border-gray-200 bg-gradient-to-b from-gray-50 to-gray-100 px-5 py-3 text-center text-sm font-medium text-gray-800 shadow-sm dark:border-[#404040] dark:bg-gradient-to-b dark:from-[#5B5B5D] dark:to-[#262627] dark:text-white dark:shadow-inner dark:shadow-purple-500/10 flex items-center justify-center"
|
||||
>
|
||||
<IconBrandDiscord className="h-5 w-5 mr-2" />
|
||||
<span>Discord</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/MODSetter/SurfSense"
|
||||
className="w-48 gap-1 rounded-full border border-transparent bg-gray-800 px-5 py-3 text-center text-sm font-medium text-white shadow-sm hover:bg-gray-700 dark:bg-gradient-to-r dark:from-purple-700 dark:to-indigo-800 dark:text-white dark:hover:from-purple-600 dark:hover:to-indigo-700 flex items-center justify-center"
|
||||
>
|
||||
<IconBrandGithub className="h-5 w-5 mr-2" />
|
||||
<span>GitHub</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Features />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TopLines = () => {
|
||||
return (
|
||||
<svg
|
||||
width="166"
|
||||
height="298"
|
||||
viewBox="0 0 166 298"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="aspect-square pointer-events-none absolute inset-x-0 top-0 h-[100px] w-full md:h-[200px]"
|
||||
>
|
||||
<title>Top Lines</title>
|
||||
<line
|
||||
y1="-0.5"
|
||||
x2="406"
|
||||
y2="-0.5"
|
||||
transform="matrix(0 1 1 0 1 -108)"
|
||||
stroke="url(#paint0_linear_254_143)"
|
||||
/>
|
||||
<line
|
||||
y1="-0.5"
|
||||
x2="406"
|
||||
y2="-0.5"
|
||||
transform="matrix(0 1 1 0 34 -108)"
|
||||
stroke="url(#paint1_linear_254_143)"
|
||||
/>
|
||||
<line
|
||||
y1="-0.5"
|
||||
x2="406"
|
||||
y2="-0.5"
|
||||
transform="matrix(0 1 1 0 67 -108)"
|
||||
stroke="url(#paint2_linear_254_143)"
|
||||
/>
|
||||
<line
|
||||
y1="-0.5"
|
||||
x2="406"
|
||||
y2="-0.5"
|
||||
transform="matrix(0 1 1 0 100 -108)"
|
||||
stroke="url(#paint3_linear_254_143)"
|
||||
/>
|
||||
<line
|
||||
y1="-0.5"
|
||||
x2="406"
|
||||
y2="-0.5"
|
||||
transform="matrix(0 1 1 0 133 -108)"
|
||||
stroke="url(#paint4_linear_254_143)"
|
||||
/>
|
||||
<line
|
||||
y1="-0.5"
|
||||
x2="406"
|
||||
y2="-0.5"
|
||||
transform="matrix(0 1 1 0 166 -108)"
|
||||
stroke="url(#paint5_linear_254_143)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_254_143"
|
||||
x1="-7.42412e-06"
|
||||
y1="0.500009"
|
||||
x2="405"
|
||||
y2="0.500009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_254_143"
|
||||
x1="-7.42412e-06"
|
||||
y1="0.500009"
|
||||
x2="405"
|
||||
y2="0.500009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_254_143"
|
||||
x1="-7.42412e-06"
|
||||
y1="0.500009"
|
||||
x2="405"
|
||||
y2="0.500009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_254_143"
|
||||
x1="-7.42412e-06"
|
||||
y1="0.500009"
|
||||
x2="405"
|
||||
y2="0.500009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_254_143"
|
||||
x1="-7.42412e-06"
|
||||
y1="0.500009"
|
||||
x2="405"
|
||||
y2="0.500009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_254_143"
|
||||
x1="-7.42412e-06"
|
||||
y1="0.500009"
|
||||
x2="405"
|
||||
y2="0.500009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const BottomLines = () => {
|
||||
return (
|
||||
<svg
|
||||
width="445"
|
||||
height="418"
|
||||
viewBox="0 0 445 418"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="aspect-square pointer-events-none absolute inset-x-0 -bottom-20 z-20 h-[150px] w-full md:h-[300px]"
|
||||
>
|
||||
<title>Bottom Lines</title>
|
||||
<line x1="139.5" y1="418" x2="139.5" y2="12" stroke="url(#paint0_linear_0_1)" />
|
||||
<line x1="172.5" y1="418" x2="172.5" y2="12" stroke="url(#paint1_linear_0_1)" />
|
||||
<line x1="205.5" y1="418" x2="205.5" y2="12" stroke="url(#paint2_linear_0_1)" />
|
||||
<line x1="238.5" y1="418" x2="238.5" y2="12" stroke="url(#paint3_linear_0_1)" />
|
||||
<line x1="271.5" y1="418" x2="271.5" y2="12" stroke="url(#paint4_linear_0_1)" />
|
||||
<line x1="304.5" y1="418" x2="304.5" y2="12" stroke="url(#paint5_linear_0_1)" />
|
||||
<path
|
||||
d="M1 149L109.028 235.894C112.804 238.931 115 243.515 115 248.361V417"
|
||||
stroke="url(#paint6_linear_0_1)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M444 149L335.972 235.894C332.196 238.931 330 243.515 330 248.361V417"
|
||||
stroke="url(#paint7_linear_0_1)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_0_1"
|
||||
x1="140.5"
|
||||
y1="418"
|
||||
x2="140.5"
|
||||
y2="13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_0_1"
|
||||
x1="173.5"
|
||||
y1="418"
|
||||
x2="173.5"
|
||||
y2="13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_0_1"
|
||||
x1="206.5"
|
||||
y1="418"
|
||||
x2="206.5"
|
||||
y2="13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_0_1"
|
||||
x1="239.5"
|
||||
y1="418"
|
||||
x2="239.5"
|
||||
y2="13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_0_1"
|
||||
x1="272.5"
|
||||
y1="418"
|
||||
x2="272.5"
|
||||
y2="13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_0_1"
|
||||
x1="305.5"
|
||||
y1="418"
|
||||
x2="305.5"
|
||||
y2="13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="gray" className="dark:stop-color-white" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_0_1"
|
||||
x1="115"
|
||||
y1="390.591"
|
||||
x2="-59.1703"
|
||||
y2="205.673"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.481613" stopColor="#E8E8E8" className="dark:stop-color-[#F8F8F8]" />
|
||||
<stop
|
||||
offset="1"
|
||||
stopColor="#E8E8E8"
|
||||
stopOpacity="0"
|
||||
className="dark:stop-color-[#F8F8F8]"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint7_linear_0_1"
|
||||
x1="330"
|
||||
y1="390.591"
|
||||
x2="504.17"
|
||||
y2="205.673"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.481613" stopColor="#E8E8E8" className="dark:stop-color-[#F8F8F8]" />
|
||||
<stop
|
||||
offset="1"
|
||||
stopColor="#E8E8E8"
|
||||
stopOpacity="0"
|
||||
className="dark:stop-color-[#F8F8F8]"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const SideLines = () => {
|
||||
return (
|
||||
<svg
|
||||
width="1382"
|
||||
height="370"
|
||||
viewBox="0 0 1382 370"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="pointer-events-none absolute inset-0 z-30 h-full w-full"
|
||||
>
|
||||
<title>Side Lines</title>
|
||||
<path
|
||||
d="M268 115L181.106 6.97176C178.069 3.19599 173.485 1 168.639 1H0"
|
||||
stroke="url(#paint0_linear_337_46)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M1114 115L1200.89 6.97176C1203.93 3.19599 1208.52 1 1213.36 1H1382"
|
||||
stroke="url(#paint1_linear_337_46)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M268 255L181.106 363.028C178.069 366.804 173.485 369 168.639 369H0"
|
||||
stroke="url(#paint2_linear_337_46)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M1114 255L1200.89 363.028C1203.93 366.804 1208.52 369 1213.36 369H1382"
|
||||
stroke="url(#paint3_linear_337_46)"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_337_46"
|
||||
x1="26.4087"
|
||||
y1="1.00001"
|
||||
x2="211.327"
|
||||
y2="175.17"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.481613" stopColor="#E8E8E8" className="dark:stop-color-[#F8F8F8]" />
|
||||
<stop
|
||||
offset="1"
|
||||
stopColor="#E8E8E8"
|
||||
stopOpacity="0"
|
||||
className="dark:stop-color-[#F8F8F8]"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_337_46"
|
||||
x1="1355.59"
|
||||
y1="1.00001"
|
||||
x2="1170.67"
|
||||
y2="175.17"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.481613" stopColor="#E8E8E8" className="dark:stop-color-[#F8F8F8]" />
|
||||
<stop
|
||||
offset="1"
|
||||
stopColor="#E8E8E8"
|
||||
stopOpacity="0"
|
||||
className="dark:stop-color-[#F8F8F8]"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_337_46"
|
||||
x1="26.4087"
|
||||
y1="369"
|
||||
x2="211.327"
|
||||
y2="194.83"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.481613" stopColor="#E8E8E8" className="dark:stop-color-[#F8F8F8]" />
|
||||
<stop
|
||||
offset="1"
|
||||
stopColor="#E8E8E8"
|
||||
stopOpacity="0"
|
||||
className="dark:stop-color-[#F8F8F8]"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_337_46"
|
||||
x1="1355.59"
|
||||
y1="369"
|
||||
x2="1170.67"
|
||||
y2="194.83"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.481613" stopColor="#E8E8E8" className="dark:stop-color-[#F8F8F8]" />
|
||||
<stop
|
||||
offset="1"
|
||||
stopColor="#E8E8E8"
|
||||
stopOpacity="0"
|
||||
className="dark:stop-color-[#F8F8F8]"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const BottomGradient = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="851"
|
||||
height="595"
|
||||
viewBox="0 0 851 595"
|
||||
fill="none"
|
||||
className={cn(
|
||||
"pointer-events-none absolute -right-80 bottom-0 h-full w-full opacity-30 dark:opacity-100 dark:hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<title>Bottom Gradient</title>
|
||||
<path
|
||||
d="M118.499 0H532.468L635.375 38.6161L665 194.625L562.093 346H0L24.9473 121.254L118.499 0Z"
|
||||
fill="url(#paint0_radial_254_132)"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial_254_132"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(412.5 346) rotate(-91.153) scale(397.581 423.744)"
|
||||
>
|
||||
<stop stopColor="#AAD3E9" />
|
||||
<stop offset="0.25" stopColor="#7FB8D4" />
|
||||
<stop offset="0.573634" stopColor="#5A9BB8" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const TopGradient = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1007"
|
||||
height="997"
|
||||
viewBox="0 0 1007 997"
|
||||
fill="none"
|
||||
className={cn(
|
||||
"pointer-events-none absolute -left-96 top-0 h-full w-full opacity-30 dark:opacity-100 dark:hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<title>Top Gradient</title>
|
||||
<path
|
||||
d="M807 110.119L699.5 -117.546L8.5 -154L-141 246.994L-7 952L127 782.111L279 652.114L513 453.337L807 110.119Z"
|
||||
fill="url(#paint0_radial_254_135)"
|
||||
/>
|
||||
<path
|
||||
d="M807 110.119L699.5 -117.546L8.5 -154L-141 246.994L-7 952L127 782.111L279 652.114L513 453.337L807 110.119Z"
|
||||
fill="url(#paint1_radial_254_135)"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial_254_135"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(77.0001 15.8894) rotate(90.3625) scale(869.41 413.353)"
|
||||
>
|
||||
<stop stopColor="#AAD3E9" />
|
||||
<stop offset="0.25" stopColor="#7FB8D4" />
|
||||
<stop offset="0.573634" stopColor="#5A9BB8" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="paint1_radial_254_135"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(127.5 -31) rotate(1.98106) scale(679.906 715.987)"
|
||||
>
|
||||
<stop stopColor="#AAD3E9" />
|
||||
<stop offset="0.283363" stopColor="#7FB8D4" />
|
||||
<stop offset="0.573634" stopColor="#5A9BB8" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const DarkModeGradient = () => {
|
||||
return (
|
||||
<div className="hidden dark:block">
|
||||
<div className="absolute -left-48 -top-48 h-[800px] w-[800px] rounded-full bg-purple-900/20 blur-[180px]"></div>
|
||||
<div className="absolute -right-48 -bottom-48 h-[800px] w-[800px] rounded-full bg-indigo-900/20 blur-[180px]"></div>
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-[400px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-purple-800/10 blur-[120px]"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
|
|||
|
||||
export function CTAHomepage() {
|
||||
return (
|
||||
<section className="w-full grid grid-cols-1 md:grid-cols-3 my-20 md:my-40 justify-start relative z-20 max-w-7xl mx-auto bg-gradient-to-br from-gray-100 to-white dark:from-neutral-900 dark:to-neutral-950">
|
||||
<section className="w-full grid grid-cols-1 md:grid-cols-3 my-20 md:my-20 justify-start relative z-20 max-w-7xl mx-auto bg-gradient-to-br from-gray-100 to-white dark:from-neutral-900 dark:to-neutral-950">
|
||||
<GridLineHorizontal className="top-0" offset="200px" />
|
||||
<GridLineHorizontal className="bottom-0 top-auto" offset="200px" />
|
||||
<GridLineVertical className="left-0" offset="80px" />
|
||||
|
|
|
|||
|
|
@ -2,42 +2,33 @@ import { Sliders, Users, Workflow } from "lucide-react";
|
|||
import type { ReactNode } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
|
||||
export function Features() {
|
||||
export function FeaturesCards() {
|
||||
return (
|
||||
<section className="py-2 md:py-8 dark:bg-transparent">
|
||||
<div className="@container mx-auto max-w-5xl">
|
||||
{/* <div className="text-center">
|
||||
<h2 className="text-balance text-4xl font-semibold lg:text-5xl">Built to cover your needs</h2>
|
||||
<p className="mt-4">Libero sapiente aliquam quibusdam aspernatur, praesentium iusto repellendus.</p>
|
||||
</div> */}
|
||||
<div className="@container mx-auto max-w-7xl">
|
||||
<div className="text-center">
|
||||
<h2 className="text-balance text-4xl font-semibold lg:text-5xl">
|
||||
Everything Your Team Needs to Succeed
|
||||
</h2>
|
||||
<p className="mt-4">
|
||||
Powerful features designed to enhance collaboration, boost productivity, and streamline
|
||||
your workflow.
|
||||
</p>
|
||||
</div>
|
||||
<div className="@min-4xl:max-w-full @min-4xl:grid-cols-3 mx-auto mt-8 grid max-w-sm gap-6 *:text-center md:mt-16">
|
||||
<Card className="group shadow-black-950/5">
|
||||
<CardHeader className="pb-3">
|
||||
<CardDecorator>
|
||||
<Sliders className="size-6" aria-hidden />
|
||||
</CardDecorator>
|
||||
|
||||
<h3 className="mt-6 font-medium">Customizable</h3>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<p className="text-sm">Customize your research agent to your specific needs.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="group shadow-black-950/5">
|
||||
<CardHeader className="pb-3">
|
||||
<CardDecorator>
|
||||
<Workflow className="size-6" aria-hidden />
|
||||
</CardDecorator>
|
||||
|
||||
<h3 className="mt-6 font-medium">Streamline your workflow</h3>
|
||||
<h3 className="mt-6 font-medium">Streamlined Workflow</h3>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<p className="text-sm">
|
||||
Pull all your knowledge into one place, so you can find what matters and get things
|
||||
done faster.
|
||||
Centralize all your knowledge and resources in one intelligent workspace. Find what
|
||||
you need instantly and accelerate decision-making.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -52,7 +43,26 @@ export function Features() {
|
|||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<p className="text-sm">Make your company and personal content collaborative.</p>
|
||||
<p className="text-sm">
|
||||
Work together effortlessly with real-time collaboration tools that keep your entire
|
||||
team aligned.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="group shadow-black-950/5">
|
||||
<CardHeader className="pb-3">
|
||||
<CardDecorator>
|
||||
<Sliders className="size-6" aria-hidden />
|
||||
</CardDecorator>
|
||||
|
||||
<h3 className="mt-6 font-medium">Fully Customizable</h3>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<p className="text-sm">
|
||||
Choose from 100+ leading LLMs and seamlessly call any model on demand.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function Footer() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="border-t border-neutral-100 dark:border-white/[0.1] px-8 py-20 bg-white dark:bg-neutral-950 w-full relative overflow-hidden">
|
||||
<div className="border-t border-neutral-100 dark:border-white/[0.1] px-8 py-20 w-full relative overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto text-sm text-neutral-500 justify-between items-start md:px-8">
|
||||
<div className="flex flex-col items-center justify-center w-full relative">
|
||||
<div className="mr-0 md:mr-4 md:flex mb-4">
|
||||
318
surfsense_web/components/homepage/hero-section.tsx
Normal file
318
surfsense_web/components/homepage/hero-section.tsx
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
"use client";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Balancer from "react-wrap-balancer";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function HeroSection() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-20 md:px-8 md:py-40"
|
||||
>
|
||||
<BackgroundGrids />
|
||||
<CollisionMechanism
|
||||
beamOptions={{
|
||||
initialX: -400,
|
||||
translateX: 600,
|
||||
duration: 7,
|
||||
repeatDelay: 3,
|
||||
}}
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<CollisionMechanism
|
||||
beamOptions={{
|
||||
initialX: -200,
|
||||
translateX: 800,
|
||||
duration: 4,
|
||||
repeatDelay: 3,
|
||||
}}
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<CollisionMechanism
|
||||
beamOptions={{
|
||||
initialX: 200,
|
||||
translateX: 1200,
|
||||
duration: 5,
|
||||
repeatDelay: 3,
|
||||
}}
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
<CollisionMechanism
|
||||
containerRef={containerRef}
|
||||
parentRef={parentRef}
|
||||
beamOptions={{
|
||||
initialX: 400,
|
||||
translateX: 1400,
|
||||
duration: 6,
|
||||
repeatDelay: 3,
|
||||
}}
|
||||
/>
|
||||
|
||||
<h2 className="relative z-50 mx-auto mb-4 mt-4 max-w-4xl text-balance text-center text-3xl font-semibold tracking-tight text-gray-700 md:text-7xl dark:text-neutral-300">
|
||||
<Balancer>
|
||||
The AI Workspace{" "}
|
||||
<div className="relative mx-auto inline-block w-max [filter:drop-shadow(0px_1px_3px_rgba(27,_37,_80,_0.14))]">
|
||||
<div className="text-black [text-shadow:0_0_rgba(0,0,0,0.1)] dark:text-white">
|
||||
<span className="">Built for Teams</span>
|
||||
</div>
|
||||
</div>
|
||||
</Balancer>
|
||||
</h2>
|
||||
{/* // TODO:aCTUAL DESCRITION */}
|
||||
<p className="relative z-50 mx-auto mt-4 max-w-lg px-4 text-center text-base/6 text-gray-600 dark:text-gray-200">
|
||||
Your multi-collaborative AI workspace, connected to external sources.
|
||||
</p>
|
||||
<div className="mb-10 mt-8 flex w-full flex-col items-center justify-center gap-4 px-8 sm:flex-row md:mb-20">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="group relative z-20 flex h-10 w-full cursor-pointer items-center justify-center space-x-2 rounded-lg bg-black p-px px-4 py-2 text-center text-sm font-semibold leading-6 text-white no-underline transition duration-200 sm:w-52 dark:bg-white dark:text-black"
|
||||
>
|
||||
Start Free Trial
|
||||
</Link>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="shadow-input group relative z-20 flex h-10 w-full cursor-pointer items-center justify-center space-x-2 rounded-lg bg-white p-px px-4 py-2 text-sm font-semibold leading-6 text-black no-underline transition duration-200 hover:-translate-y-0.5 sm:w-52 dark:bg-neutral-800 dark:text-white"
|
||||
>
|
||||
Explore
|
||||
</Link>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative mx-auto max-w-7xl rounded-[32px] border border-neutral-200/50 bg-neutral-100 p-2 backdrop-blur-lg md:p-4 dark:border-neutral-700 dark:bg-neutral-800/50"
|
||||
>
|
||||
<div className="rounded-[24px] border border-neutral-200 bg-white p-2 dark:border-neutral-700 dark:bg-black">
|
||||
{/* Light mode image */}
|
||||
<Image
|
||||
src="/homepage/temp_hero_light.png"
|
||||
alt="header"
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="rounded-[20px] block dark:hidden"
|
||||
/>
|
||||
{/* Dark mode image */}
|
||||
<Image
|
||||
src="/homepage/temp_hero_dark.png"
|
||||
alt="header"
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="rounded-[20px] hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BackgroundGrids = () => {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-0 grid h-full w-full -rotate-45 transform select-none grid-cols-2 gap-10 md:grid-cols-4">
|
||||
<div className="relative h-full w-full">
|
||||
<GridLineVertical className="left-0" />
|
||||
<GridLineVertical className="left-auto right-0" />
|
||||
</div>
|
||||
<div className="relative h-full w-full">
|
||||
<GridLineVertical className="left-0" />
|
||||
<GridLineVertical className="left-auto right-0" />
|
||||
</div>
|
||||
<div className="relative h-full w-full bg-gradient-to-b from-transparent via-neutral-100 to-transparent dark:via-neutral-800">
|
||||
<GridLineVertical className="left-0" />
|
||||
<GridLineVertical className="left-auto right-0" />
|
||||
</div>
|
||||
<div className="relative h-full w-full">
|
||||
<GridLineVertical className="left-0" />
|
||||
<GridLineVertical className="left-auto right-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CollisionMechanism = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
parentRef: React.RefObject<HTMLDivElement | null>;
|
||||
beamOptions?: {
|
||||
initialX?: number;
|
||||
translateX?: number;
|
||||
initialY?: number;
|
||||
translateY?: number;
|
||||
rotate?: number;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
repeatDelay?: number;
|
||||
};
|
||||
}
|
||||
>(({ parentRef, containerRef, beamOptions = {} }, ref) => {
|
||||
const beamRef = useRef<HTMLDivElement>(null);
|
||||
const [collision, setCollision] = useState<{
|
||||
detected: boolean;
|
||||
coordinates: { x: number; y: number } | null;
|
||||
}>({ detected: false, coordinates: null });
|
||||
const [beamKey, setBeamKey] = useState(0);
|
||||
const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkCollision = () => {
|
||||
if (beamRef.current && containerRef.current && parentRef.current && !cycleCollisionDetected) {
|
||||
const beamRect = beamRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const parentRect = parentRef.current.getBoundingClientRect();
|
||||
|
||||
if (beamRect.bottom >= containerRect.top) {
|
||||
const relativeX = beamRect.left - parentRect.left + beamRect.width / 2;
|
||||
const relativeY = beamRect.bottom - parentRect.top;
|
||||
|
||||
setCollision({
|
||||
detected: true,
|
||||
coordinates: { x: relativeX, y: relativeY },
|
||||
});
|
||||
setCycleCollisionDetected(true);
|
||||
if (beamRef.current) {
|
||||
beamRef.current.style.opacity = "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const animationInterval = setInterval(checkCollision, 50);
|
||||
|
||||
return () => clearInterval(animationInterval);
|
||||
}, [cycleCollisionDetected, containerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (collision.detected && collision.coordinates) {
|
||||
setTimeout(() => {
|
||||
setCollision({ detected: false, coordinates: null });
|
||||
setCycleCollisionDetected(false);
|
||||
// Set beam opacity to 0
|
||||
if (beamRef.current) {
|
||||
beamRef.current.style.opacity = "1";
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Reset the beam animation after a delay
|
||||
setTimeout(() => {
|
||||
setBeamKey((prevKey) => prevKey + 1);
|
||||
}, 2000);
|
||||
}
|
||||
}, [collision]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
key={beamKey}
|
||||
ref={beamRef}
|
||||
animate="animate"
|
||||
initial={{
|
||||
translateY: beamOptions.initialY || "-200px",
|
||||
translateX: beamOptions.initialX || "0px",
|
||||
rotate: beamOptions.rotate || -45,
|
||||
}}
|
||||
variants={{
|
||||
animate: {
|
||||
translateY: beamOptions.translateY || "800px",
|
||||
translateX: beamOptions.translateX || "700px",
|
||||
rotate: beamOptions.rotate || -45,
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
duration: beamOptions.duration || 8,
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
ease: "linear",
|
||||
delay: beamOptions.delay || 0,
|
||||
repeatDelay: beamOptions.repeatDelay || 0,
|
||||
}}
|
||||
className={cn(
|
||||
"absolute left-96 top-20 m-auto h-14 w-px rounded-full bg-gradient-to-t from-orange-500 via-yellow-500 to-transparent",
|
||||
beamOptions.className
|
||||
)}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{collision.detected && collision.coordinates && (
|
||||
<Explosion
|
||||
key={`${collision.coordinates.x}-${collision.coordinates.y}`}
|
||||
className=""
|
||||
style={{
|
||||
left: `${collision.coordinates.x + 20}px`,
|
||||
top: `${collision.coordinates.y}px`,
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CollisionMechanism.displayName = "CollisionMechanism";
|
||||
|
||||
const Explosion = ({ ...props }: React.HTMLProps<HTMLDivElement>) => {
|
||||
const spans = Array.from({ length: 20 }, (_, index) => ({
|
||||
id: index,
|
||||
initialX: 0,
|
||||
initialY: 0,
|
||||
directionX: Math.floor(Math.random() * 80 - 40),
|
||||
directionY: Math.floor(Math.random() * -50 - 10),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div {...props} className={cn("absolute z-50 h-2 w-2", props.className)}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1, ease: "easeOut" }}
|
||||
className="absolute -inset-x-10 top-0 m-auto h-[4px] w-10 rounded-full bg-gradient-to-r from-transparent via-orange-500 to-transparent blur-sm"
|
||||
></motion.div>
|
||||
{spans.map((span) => (
|
||||
<motion.span
|
||||
key={span.id}
|
||||
initial={{ x: span.initialX, y: span.initialY, opacity: 1 }}
|
||||
animate={{ x: span.directionX, y: span.directionY, opacity: 0 }}
|
||||
transition={{ duration: Math.random() * 1.5 + 0.5, ease: "easeOut" }}
|
||||
className="absolute h-1 w-1 rounded-full bg-gradient-to-b from-orange-500 to-yellow-500"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridLineVertical = ({ className, offset }: { className?: string; offset?: string }) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--background": "#ffffff",
|
||||
"--color": "rgba(0, 0, 0, 0.2)",
|
||||
"--height": "5px",
|
||||
"--width": "1px",
|
||||
"--fade-stop": "90%",
|
||||
"--offset": offset || "150px", //-100px if you want to keep the line inside
|
||||
"--color-dark": "rgba(255, 255, 255, 0.3)",
|
||||
maskComposite: "exclude",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-[var(--width)]",
|
||||
"bg-[linear-gradient(to_bottom,var(--color),var(--color)_50%,transparent_0,transparent)]",
|
||||
"[background-size:var(--width)_var(--height)]",
|
||||
"[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
|
||||
"[mask-composite:exclude]",
|
||||
"z-30",
|
||||
"dark:bg-[linear-gradient(to_bottom,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
|
||||
className
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
185
surfsense_web/components/homepage/integrations.tsx
Normal file
185
surfsense_web/components/homepage/integrations.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Integration {
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const INTEGRATIONS: Integration[] = [
|
||||
// Search
|
||||
{ name: "Tavily", icon: "https://www.tavily.com/images/logo.svg" },
|
||||
{
|
||||
name: "LinkUp",
|
||||
icon: "https://framerusercontent.com/images/7zeIm6t3f1HaSltkw8upEvsD80.png?scale-down-to=512",
|
||||
},
|
||||
|
||||
// Communication
|
||||
{ name: "Slack", icon: "https://cdn.simpleicons.org/slack/4A154B" },
|
||||
{ name: "Discord", icon: "https://cdn.simpleicons.org/discord/5865F2" },
|
||||
{ name: "Gmail", icon: "https://cdn.simpleicons.org/gmail/EA4335" },
|
||||
|
||||
// Project Management
|
||||
{ name: "Linear", icon: "https://cdn.simpleicons.org/linear/5E6AD2" },
|
||||
{ name: "Jira", icon: "https://cdn.simpleicons.org/jira/0052CC" },
|
||||
{ name: "ClickUp", icon: "https://cdn.simpleicons.org/clickup/7B68EE" },
|
||||
{ name: "Airtable", icon: "https://cdn.simpleicons.org/airtable/18BFFF" },
|
||||
|
||||
// Documentation & Knowledge
|
||||
{ name: "Confluence", icon: "https://cdn.simpleicons.org/confluence/172B4D" },
|
||||
{ name: "Notion", icon: "https://cdn.simpleicons.org/notion/000000/ffffff" },
|
||||
|
||||
// Cloud Storage
|
||||
{ name: "Google Drive", icon: "https://cdn.simpleicons.org/googledrive/4285F4" },
|
||||
{ name: "Dropbox", icon: "https://cdn.simpleicons.org/dropbox/0061FF" },
|
||||
{
|
||||
name: "Amazon S3",
|
||||
icon: "https://upload.wikimedia.org/wikipedia/commons/b/bc/Amazon-S3-Logo.svg",
|
||||
},
|
||||
|
||||
// Development
|
||||
{ name: "GitHub", icon: "https://cdn.simpleicons.org/github/181717/ffffff" },
|
||||
|
||||
// Productivity
|
||||
{ name: "Google Calendar", icon: "https://cdn.simpleicons.org/googlecalendar/4285F4" },
|
||||
{ name: "Luma", icon: "https://images.lumacdn.com/social-images/default-social-202407.png" },
|
||||
|
||||
// Media
|
||||
{ name: "YouTube", icon: "https://cdn.simpleicons.org/youtube/FF0000" },
|
||||
];
|
||||
|
||||
function SemiCircleOrbit({ radius, centerX, centerY, count, iconSize, startIndex }: any) {
|
||||
return (
|
||||
<>
|
||||
{/* Semi-circle glow background */}
|
||||
<div className="absolute inset-0 flex justify-center items-start overflow-visible">
|
||||
<div
|
||||
className="
|
||||
w-[800px] h-[800px] rounded-full
|
||||
bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.15),transparent_70%)]
|
||||
dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.15),transparent_70%)]
|
||||
blur-3xl
|
||||
pointer-events-none
|
||||
"
|
||||
style={{
|
||||
zIndex: 0,
|
||||
transform: "translateY(-20%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Orbit icons */}
|
||||
{Array.from({ length: count }).map((_, index) => {
|
||||
const actualIndex = startIndex + index;
|
||||
// Skip if we've run out of integrations
|
||||
if (actualIndex >= INTEGRATIONS.length) return null;
|
||||
|
||||
const angle = (index / (count - 1)) * 180;
|
||||
const x = radius * Math.cos((angle * Math.PI) / 180);
|
||||
const y = radius * Math.sin((angle * Math.PI) / 180);
|
||||
const integration = INTEGRATIONS[actualIndex];
|
||||
|
||||
// Tooltip positioning — above or below based on angle
|
||||
const tooltipAbove = angle > 90;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute flex flex-col items-center group"
|
||||
style={{
|
||||
left: `${centerX + x - iconSize / 2}px`,
|
||||
top: `${centerY - y - iconSize / 2}px`,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={integration.icon}
|
||||
alt={integration.name}
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
className="object-contain cursor-pointer transition-transform hover:scale-110"
|
||||
style={{ minWidth: iconSize, minHeight: iconSize }} // fix accidental shrink
|
||||
/>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div
|
||||
className={`absolute ${
|
||||
tooltipAbove ? "bottom-[calc(100%+8px)]" : "top-[calc(100%+8px)]"
|
||||
} hidden group-hover:block w-auto min-w-max rounded-lg bg-black px-3 py-1.5 text-xs text-white shadow-lg text-center whitespace-nowrap`}
|
||||
>
|
||||
{integration.name}
|
||||
<div
|
||||
className={`absolute left-1/2 -translate-x-1/2 w-3 h-3 rotate-45 bg-black ${
|
||||
tooltipAbove ? "top-full" : "bottom-full"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ExternalIntegrations() {
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const updateSize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
|
||||
updateSize();
|
||||
window.addEventListener("resize", updateSize);
|
||||
return () => window.removeEventListener("resize", updateSize);
|
||||
}, []);
|
||||
|
||||
const baseWidth = Math.min(size.width * 0.8, 700);
|
||||
const centerX = baseWidth / 2;
|
||||
const centerY = baseWidth * 0.5;
|
||||
|
||||
const iconSize =
|
||||
size.width < 480
|
||||
? Math.max(24, baseWidth * 0.05)
|
||||
: size.width < 768
|
||||
? Math.max(28, baseWidth * 0.06)
|
||||
: Math.max(32, baseWidth * 0.07);
|
||||
|
||||
return (
|
||||
<section className="py-12 relative min-h-screen w-full overflow-visible">
|
||||
<div className="relative flex flex-col items-center text-center z-10">
|
||||
<h1 className="my-6 text-4xl font-bold lg:text-7xl">Integrations</h1>
|
||||
<p className="mb-12 max-w-2xl text-gray-600 dark:text-gray-400 lg:text-xl">
|
||||
Connect your favourite apps to your workflow.
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="relative overflow-visible"
|
||||
style={{ width: baseWidth, height: baseWidth * 0.7, paddingBottom: "100px" }}
|
||||
>
|
||||
<SemiCircleOrbit
|
||||
radius={baseWidth * 0.22}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
count={5}
|
||||
iconSize={iconSize}
|
||||
startIndex={0}
|
||||
/>
|
||||
<SemiCircleOrbit
|
||||
radius={baseWidth * 0.36}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
count={6}
|
||||
iconSize={iconSize}
|
||||
startIndex={5}
|
||||
/>
|
||||
<SemiCircleOrbit
|
||||
radius={baseWidth * 0.5}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
count={8}
|
||||
iconSize={iconSize}
|
||||
startIndex={11}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
177
surfsense_web/components/homepage/navbar.tsx
Normal file
177
surfsense_web/components/homepage/navbar.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"use client";
|
||||
import { IconBrandDiscord, IconBrandGithub, IconMenu2, IconX } from "@tabler/icons-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Navbar = () => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
const navItems = [
|
||||
{ name: "Home", link: "/" },
|
||||
{ name: "Pricing", link: "/pricing" },
|
||||
{ name: "Sign In", link: "/login" },
|
||||
{ name: "Docs", link: "/docs" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed top-1 left-0 right-0 z-[60] w-full">
|
||||
<DesktopNav navItems={navItems} isScrolled={isScrolled} />
|
||||
<MobileNav navItems={navItems} isScrolled={isScrolled} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopNav = ({ navItems, isScrolled }: any) => {
|
||||
const [hovered, setHovered] = useState<number | null>(null);
|
||||
return (
|
||||
<motion.div
|
||||
onMouseLeave={() => {
|
||||
setHovered(null);
|
||||
}}
|
||||
className={cn(
|
||||
"mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full px-4 py-2 lg:flex transition-all duration-300",
|
||||
isScrolled
|
||||
? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
|
||||
: "bg-transparent border border-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Logo className="h-8 w-8 rounded-md" />
|
||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||
</div>
|
||||
<div className="hidden flex-1 flex-row items-center justify-center space-x-2 text-sm font-medium text-zinc-600 transition duration-200 hover:text-zinc-800 lg:flex lg:space-x-2">
|
||||
{navItems.map((navItem: any, idx: number) => (
|
||||
<Link
|
||||
onMouseEnter={() => setHovered(idx)}
|
||||
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
|
||||
key={`link=${idx}`}
|
||||
href={navItem.link}
|
||||
>
|
||||
{hovered === idx && (
|
||||
<motion.div
|
||||
layoutId="hovered"
|
||||
className="absolute inset-0 h-full w-full rounded-full bg-gray-100 dark:bg-neutral-800"
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-20">{navItem.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="https://discord.gg/ejRNvftDp9"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden rounded-full p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors md:flex items-center justify-center"
|
||||
>
|
||||
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/MODSetter/SurfSense"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden rounded-full px-3 py-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors md:flex items-center gap-1.5"
|
||||
>
|
||||
<IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-300">8.3k</span>
|
||||
</Link>
|
||||
<ThemeTogglerComponent />
|
||||
<Link
|
||||
href="/contact"
|
||||
className="hidden rounded-full bg-black px-8 py-2 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] md:block dark:bg-white dark:text-black"
|
||||
>
|
||||
Book a call
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNav = ({ navItems, isScrolled }: any) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
animate={{ borderRadius: open ? "4px" : "2rem" }}
|
||||
key={String(open)}
|
||||
className={cn(
|
||||
"mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between px-4 py-2 lg:hidden transition-all duration-300",
|
||||
isScrolled
|
||||
? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
|
||||
: "bg-transparent border border-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Logo className="h-8 w-8 rounded-md" />
|
||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||
</div>
|
||||
{open ? (
|
||||
<IconX className="text-black dark:text-white" onClick={() => setOpen(!open)} />
|
||||
) : (
|
||||
<IconMenu2 className="text-black dark:text-white" onClick={() => setOpen(!open)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-x-0 top-16 z-20 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white/80 backdrop-blur-md border border-white/20 shadow-lg px-4 py-8 dark:bg-neutral-950/80 dark:border-neutral-800/50"
|
||||
>
|
||||
{navItems.map((navItem: any, idx: number) => (
|
||||
<Link
|
||||
key={`link=${idx}`}
|
||||
href={navItem.link}
|
||||
className="relative text-neutral-600 dark:text-neutral-300"
|
||||
>
|
||||
<motion.span className="block">{navItem.name} </motion.span>
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex w-full items-center gap-2 pt-2">
|
||||
<Link
|
||||
href="https://discord.gg/your-server"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/MODSetter/SurfSense"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 rounded-lg px-3 py-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
<IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-300">
|
||||
8.3k
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<button className="w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black">
|
||||
Book a call
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -91,42 +91,53 @@ export function Pricing({
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4">
|
||||
{plans.map((plan, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ y: 50, opacity: 1 }}
|
||||
whileInView={
|
||||
isDesktop
|
||||
<div className={cn(
|
||||
"grid grid-cols-1 gap-4",
|
||||
plans.length === 2 ? "md:grid-cols-2 max-w-5xl mx-auto" : "md:grid-cols-3"
|
||||
)}>
|
||||
{plans.map((plan, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ y: 50, opacity: 1 }}
|
||||
whileInView={
|
||||
isDesktop
|
||||
? plans.length === 2
|
||||
? {
|
||||
y: plan.isPopular ? -20 : 0,
|
||||
opacity: 1,
|
||||
scale: plan.isPopular ? 1.0 : 0.96,
|
||||
}
|
||||
: {
|
||||
y: plan.isPopular ? -20 : 0,
|
||||
opacity: 1,
|
||||
x: index === 2 ? -30 : index === 0 ? 30 : 0,
|
||||
scale: index === 0 || index === 2 ? 0.94 : 1.0,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
duration: 1.6,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
delay: 0.4,
|
||||
opacity: { duration: 0.5 },
|
||||
}}
|
||||
className={cn(
|
||||
`rounded-2xl border-[1px] p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`,
|
||||
plan.isPopular ? "border-primary border-2" : "border-border",
|
||||
"flex flex-col",
|
||||
!plan.isPopular && "mt-5",
|
||||
index === 0 || index === 2
|
||||
? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-[10deg]"
|
||||
: {}
|
||||
}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
duration: 1.6,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
delay: 0.4,
|
||||
opacity: { duration: 0.5 },
|
||||
}}
|
||||
className={cn(
|
||||
`rounded-2xl border-[1px] p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`,
|
||||
plan.isPopular ? "border-primary border-2" : "border-border",
|
||||
"flex flex-col",
|
||||
!plan.isPopular && "mt-5",
|
||||
plans.length === 3 && (index === 0 || index === 2)
|
||||
? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-[10deg]"
|
||||
: plans.length === 2 && !plan.isPopular
|
||||
? "z-0"
|
||||
: "z-10",
|
||||
index === 0 && "origin-right",
|
||||
index === 2 && "origin-left"
|
||||
)}
|
||||
>
|
||||
plans.length === 3 && index === 0 && "origin-right",
|
||||
plans.length === 3 && index === 2 && "origin-left"
|
||||
)}
|
||||
>
|
||||
{plan.isPopular && (
|
||||
<div className="absolute top-0 right-0 bg-primary py-0.5 px-2 rounded-bl-xl rounded-tr-xl flex items-center">
|
||||
<Star className="text-primary-foreground h-4 w-4 fill-current" />
|
||||
|
|
@ -136,9 +147,12 @@ export function Pricing({
|
|||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<p className="text-base font-semibold text-muted-foreground">{plan.name}</p>
|
||||
<div className="mt-6 flex items-center justify-center gap-x-2">
|
||||
<span className="text-5xl font-bold tracking-tight text-foreground">
|
||||
<p className="text-base font-semibold text-muted-foreground">{plan.name}</p>
|
||||
<div className="mt-6 flex items-center justify-center gap-x-2">
|
||||
<span className="text-5xl font-bold tracking-tight text-foreground">
|
||||
{isNaN(Number(plan.price)) ? (
|
||||
<span>{isMonthly ? plan.price : plan.yearlyPrice}</span>
|
||||
) : (
|
||||
<NumberFlow
|
||||
value={isMonthly ? Number(plan.price) : Number(plan.yearlyPrice)}
|
||||
format={{
|
||||
|
|
@ -154,17 +168,18 @@ export function Pricing({
|
|||
willChange
|
||||
className="font-variant-numeric: tabular-nums"
|
||||
/>
|
||||
</span>
|
||||
{plan.period !== "Next 3 months" && (
|
||||
<span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground">
|
||||
/ {plan.period}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
{plan.period && plan.period !== "Next 3 months" && (
|
||||
<span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground">
|
||||
/ {plan.period}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{isMonthly ? "billed monthly" : "billed annually"}
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{isNaN(Number(plan.price)) ? "" : isMonthly ? "billed monthly" : "billed annually"}
|
||||
</p>
|
||||
|
||||
<ul className="mt-5 gap-2 flex flex-col">
|
||||
{plan.features.map((feature, idx) => (
|
||||
|
|
|
|||
|
|
@ -4,55 +4,38 @@ import { Pricing } from "@/components/pricing";
|
|||
|
||||
const demoPlans = [
|
||||
{
|
||||
name: "STARTER",
|
||||
price: "50",
|
||||
yearlyPrice: "40",
|
||||
period: "per month",
|
||||
name: "COMMUNITY",
|
||||
price: "0",
|
||||
yearlyPrice: "0",
|
||||
period: "forever",
|
||||
features: [
|
||||
"Up to 10 projects",
|
||||
"Basic analytics",
|
||||
"48-hour support response time",
|
||||
"Limited API access",
|
||||
"Community support",
|
||||
"Supports 100+ LLMs",
|
||||
"Supports local Ollama or vLLM setups",
|
||||
"6000+ Embedding Models",
|
||||
"50+ File extensions supported.",
|
||||
"Podcasts support with local TTS providers.",
|
||||
"Connects with 15+ external sources.",
|
||||
"Cross-Browser Extension for dynamic webpages including authenticated content",
|
||||
"Upcoming: Mergeable MindMaps",
|
||||
"Upcoming: Note Management",
|
||||
],
|
||||
description: "Perfect for individuals and small projects",
|
||||
buttonText: "Start Free Trial",
|
||||
href: "/sign-up",
|
||||
isPopular: false,
|
||||
},
|
||||
{
|
||||
name: "PROFESSIONAL",
|
||||
price: "99",
|
||||
yearlyPrice: "79",
|
||||
period: "per month",
|
||||
features: [
|
||||
"Unlimited projects",
|
||||
"Advanced analytics",
|
||||
"24-hour support response time",
|
||||
"Full API access",
|
||||
"Priority support",
|
||||
"Team collaboration",
|
||||
"Custom integrations",
|
||||
],
|
||||
description: "Ideal for growing teams and businesses",
|
||||
description: "Open source version with powerful features",
|
||||
buttonText: "Get Started",
|
||||
href: "/sign-up",
|
||||
href: "/docs",
|
||||
isPopular: true,
|
||||
},
|
||||
{
|
||||
name: "ENTERPRISE",
|
||||
price: "299",
|
||||
yearlyPrice: "239",
|
||||
period: "per month",
|
||||
price: "Contact Us",
|
||||
yearlyPrice: "Contact Us",
|
||||
period: "",
|
||||
features: [
|
||||
"Everything in Professional",
|
||||
"Custom solutions",
|
||||
"Dedicated account manager",
|
||||
"1-hour support response time",
|
||||
"SSO Authentication",
|
||||
"Advanced security",
|
||||
"Custom contracts",
|
||||
"SLA agreement",
|
||||
"Everything in Community",
|
||||
"Priority Support",
|
||||
"Access Controls",
|
||||
"Multicollaborative and multiplayer features",
|
||||
"Video generation",
|
||||
"Advanced security features",
|
||||
],
|
||||
description: "For large organizations with specific needs",
|
||||
buttonText: "Contact Sales",
|
||||
|
|
@ -63,11 +46,7 @@ const demoPlans = [
|
|||
|
||||
function PricingBasic() {
|
||||
return (
|
||||
<Pricing
|
||||
plans={demoPlans}
|
||||
title="Simple, Transparent Pricing"
|
||||
description="Choose the plan that works for you"
|
||||
/>
|
||||
<Pricing plans={demoPlans} title="SurfSense Pricing" description="Choose that works for you" />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@
|
|||
"react-markdown": "^10.1.0",
|
||||
"react-rough-notation": "^1.0.5",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-wrap-balancer": "^1.1.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
|
|
|
|||
12
surfsense_web/pnpm-lock.yaml
generated
12
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -173,6 +173,9 @@ importers:
|
|||
react-syntax-highlighter:
|
||||
specifier: ^15.6.1
|
||||
version: 15.6.1(react@19.1.0)
|
||||
react-wrap-balancer:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(react@19.1.0)
|
||||
rehype-raw:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
|
|
@ -4797,6 +4800,11 @@ packages:
|
|||
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
react-wrap-balancer@1.1.1:
|
||||
resolution: {integrity: sha512-AB+l7FPRWl6uZ28VcJ8skkwLn2+UC62bjiw8tQUrZPlEWDVnR9MG0lghyn7EyxuJSsFEpht4G+yh2WikEqQ/5Q==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0 || ^17.0.0 || ^18'
|
||||
|
||||
react@19.1.0:
|
||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -11108,6 +11116,10 @@ snapshots:
|
|||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-wrap-balancer@1.1.1(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
react@19.1.0: {}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
|
|
|
|||
BIN
surfsense_web/public/homepage/temp_hero_dark.png
Normal file
BIN
surfsense_web/public/homepage/temp_hero_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
surfsense_web/public/homepage/temp_hero_light.png
Normal file
BIN
surfsense_web/public/homepage/temp_hero_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Loading…
Add table
Add a link
Reference in a new issue