feat: contact to frontpage and biome

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-09-30 21:53:10 -07:00
parent ba62489715
commit e305bc4796
14 changed files with 620 additions and 618 deletions

View file

@ -1,5 +1,5 @@
"use client"; "use client";
import { IconBrandDiscord, IconBrandGithub, IconFileTypeDoc } from "@tabler/icons-react"; import { IconBrandDiscord, IconBrandGithub, IconFileTypeDoc, IconMail } from "@tabler/icons-react";
import Link from "next/link"; import Link from "next/link";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Logo } from "./Logo"; import { Logo } from "./Logo";
@ -54,6 +54,13 @@ export function ModernHeroWithGradients() {
Notion, YouTube, GitHub, Discord and more. Notion, YouTube, GitHub, Discord and more.
</p> </p>
<div className="flex flex-col items-center gap-6 py-6 sm:flex-row"> <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-blue-400 bg-gradient-to-b from-blue-100 to-blue-300 px-5 py-3 text-center text-sm font-medium text-blue-900 shadow-sm dark:border-blue-700 dark:bg-gradient-to-b dark:from-blue-900 dark:to-blue-700 dark:text-blue-100 dark:shadow-inner dark:shadow-blue-500/20 flex items-center justify-center"
>
<IconMail className="h-5 w-5 mr-2 text-blue-700 dark:text-blue-300" />
<span>Contact Us</span>
</Link>
<Link <Link
href="https://discord.gg/ejRNvftDp9" 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" 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"

View file

@ -1,5 +1,5 @@
"use client"; "use client";
import { IconMenu2, IconUser, IconX } from "@tabler/icons-react"; import { IconMail, IconMenu2, IconUser, IconX } from "@tabler/icons-react";
import { AnimatePresence, motion, useMotionValueEvent, useScroll } from "motion/react"; import { AnimatePresence, motion, useMotionValueEvent, useScroll } from "motion/react";
import Link from "next/link"; import Link from "next/link";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
@ -167,7 +167,17 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
duration: 0.2, 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 dark:bg-blue-900/40 dark:hover:bg-blue-800/50 dark:text-blue-100 dark:border-blue-700 bg-blue-100 hover:bg-blue-200 text-blue-900 border-blue-400"
>
<IconMail className="h-4 w-4" />
<span>Contact Us</span>
</Button>
</Link>
<Button <Button
onClick={handleGoogleLogin} onClick={handleGoogleLogin}
variant="outline" variant="outline"
@ -270,6 +280,15 @@ const MobileNav = ({ navItems, visible }: NavbarProps) => {
<motion.span className="block">{navItem.name}</motion.span> <motion.span className="block">{navItem.name}</motion.span>
</Link> </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 dark:bg-blue-900/40 dark:hover:bg-blue-800/50 dark:text-blue-100 dark:border-blue-700 bg-blue-100 hover:bg-blue-200 text-blue-900 border-blue-400"
>
<IconMail className="h-4 w-4" />
<span>Contact Us</span>
</Button>
</Link>
<Button <Button
onClick={handleGoogleLogin} onClick={handleGoogleLogin}
variant="outline" variant="outline"

View file

@ -85,7 +85,6 @@ const initialState: HighlightState = {
export function AnimatedEmptyState() { export function AnimatedEmptyState() {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref); const isInView = useInView(ref);
const { state: sidebarState } = useSidebar();
const [{ shouldShowHighlight, layoutStable }, dispatch] = useReducer( const [{ shouldShowHighlight, layoutStable }, dispatch] = useReducer(
highlightReducer, highlightReducer,
initialState initialState

View file

@ -1,371 +1,358 @@
"use client"; "use client";
import React, { useState } from "react";
import { IconMailFilled } from "@tabler/icons-react";
import { useId } from "react";
import { cn } from "@/lib/utils";
import Image from "next/image";
import { motion } from "motion/react";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { IconMailFilled } from "@tabler/icons-react";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import type React from "react";
import { useId, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
import { cn } from "@/lib/utils";
// Define validation schema matching the database schema // Define validation schema matching the database schema
const contactFormSchema = z.object({ const contactFormSchema = z.object({
name: z.string().min(1, 'Name is required').max(255, 'Name is too long'), name: z.string().min(1, "Name is required").max(255, "Name is too long"),
email: z.string().email('Invalid email address').max(255, 'Email is too long'), email: z.string().email("Invalid email address").max(255, "Email is too long"),
company: z.string().min(1, 'Company is required').max(255, 'Company name is too long'), company: z.string().min(1, "Company is required").max(255, "Company name is too long"),
message: z.string().optional().default(''), message: z.string().optional().default(""),
}); });
type ContactFormData = z.infer<typeof contactFormSchema>; type ContactFormData = z.infer<typeof contactFormSchema>;
export function ContactFormGridWithDetails() { export function ContactFormGridWithDetails() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset, reset,
} = useForm<ContactFormData>({ } = useForm<ContactFormData>({
resolver: zodResolver(contactFormSchema), resolver: zodResolver(contactFormSchema),
}); });
const onSubmit = async (data: ContactFormData) => { const onSubmit = async (data: ContactFormData) => {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const response = await fetch('/api/contact', { const response = await fetch("/api/contact", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
toast.success('Message sent successfully!', { toast.success("Message sent successfully!", {
description: 'We will get back to you as soon as possible.', description: "We will get back to you as soon as possible.",
}); });
reset(); reset();
} else { } else {
toast.error('Failed to send message', { toast.error("Failed to send message", {
description: result.message || 'Please try again later.', description: result.message || "Please try again later.",
}); });
} }
} catch (error) { } catch (error) {
console.error('Error submitting form:', error); console.error("Error submitting form:", error);
toast.error('Something went wrong', { toast.error("Something went wrong", {
description: 'Please try again later.', description: "Please try again later.",
}); });
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
return ( return (
<div className="mx-auto grid w-full max-w-7xl grid-cols-1 gap-10 px-4 py-10 md:px-6 md:py-20 lg:grid-cols-2"> <div className="mx-auto grid w-full max-w-7xl grid-cols-1 gap-10 px-4 py-10 md:px-6 md:py-20 lg:grid-cols-2">
<div className="relative flex flex-col items-center overflow-hidden lg:items-start"> <div className="relative flex flex-col items-center overflow-hidden lg:items-start">
<div className="flex items-start justify-start"> <div className="flex items-start justify-start">
<FeatureIconContainer className="flex items-center justify-center overflow-hidden"> <FeatureIconContainer className="flex items-center justify-center overflow-hidden">
<IconMailFilled className="h-6 w-6 text-blue-500" /> <IconMailFilled className="h-6 w-6 text-blue-500" />
</FeatureIconContainer> </FeatureIconContainer>
</div> </div>
<h2 className="mt-9 bg-gradient-to-b from-neutral-800 to-neutral-900 bg-clip-text text-left text-xl font-bold text-transparent md:text-3xl lg:text-5xl dark:from-neutral-200 dark:to-neutral-300"> <h2 className="mt-9 bg-gradient-to-b from-neutral-800 to-neutral-900 bg-clip-text text-left text-xl font-bold text-transparent md:text-3xl lg:text-5xl dark:from-neutral-200 dark:to-neutral-300">
Contact us Contact us
</h2> </h2>
<p className="mt-8 max-w-lg text-center text-base text-neutral-600 md:text-left dark:text-neutral-400"> <p className="mt-8 max-w-lg text-center text-base text-neutral-600 md:text-left dark:text-neutral-400">
We are always looking for ways to improve our products and services. We are always looking for ways to improve our products and services. Contact us and let us
Contact us and let us know how we can help you. know how we can help you.
</p> </p>
<div className="mt-10 hidden flex-col items-center gap-4 md:flex-row lg:flex"> <div className="mt-10 hidden flex-col items-center gap-4 md:flex-row lg:flex">
<Link href="mailto:rohan@surfsense.com" className="text-sm text-neutral-500 dark:text-neutral-400"> <Link
rohan@surfsense.com href="mailto:rohan@surfsense.com"
</Link> className="text-sm text-neutral-500 dark:text-neutral-400"
<div className="h-1 w-1 rounded-full bg-neutral-500 dark:bg-neutral-400" /> >
rohan@surfsense.com
</Link>
<div className="h-1 w-1 rounded-full bg-neutral-500 dark:bg-neutral-400" />
<Link href="https://cal.com/mod-surfsense" className="text-sm text-neutral-500 dark:text-neutral-400"> <Link
https://cal.com/mod-surfsense href="https://cal.com/mod-surfsense"
</Link> className="text-sm text-neutral-500 dark:text-neutral-400"
</div> >
<div className="div relative mt-20 flex w-[600px] flex-shrink-0 -translate-x-10 items-center justify-center [perspective:800px] [transform-style:preserve-3d] sm:-translate-x-0 lg:-translate-x-32"> https://cal.com/mod-surfsense
<Pin className="h-30 w-85 top-0 left-0" /> </Link>
</div>
<div className="div relative mt-20 flex w-[600px] flex-shrink-0 -translate-x-10 items-center justify-center [perspective:800px] [transform-style:preserve-3d] sm:-translate-x-0 lg:-translate-x-32">
<Pin className="h-30 w-85 top-0 left-0" />
<Image <Image
src="/contact/world.svg" src="/contact/world.svg"
width={500} width={500}
height={500} height={500}
alt="world map" alt="world map"
className="[transform:rotateX(45deg)_translateZ(0px)] dark:invert dark:filter" className="[transform:rotateX(45deg)_translateZ(0px)] dark:invert dark:filter"
/> />
</div> </div>
</div> </div>
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="relative mx-auto flex w-full max-w-2xl flex-col items-start gap-4 overflow-hidden rounded-3xl bg-gradient-to-b from-gray-100 to-gray-200 p-4 sm:p-10 dark:from-neutral-900 dark:to-neutral-950" className="relative mx-auto flex w-full max-w-2xl flex-col items-start gap-4 overflow-hidden rounded-3xl bg-gradient-to-b from-gray-100 to-gray-200 p-4 sm:p-10 dark:from-neutral-900 dark:to-neutral-950"
> >
<Grid size={20} /> <Grid size={20} />
<div className="relative z-20 mb-4 w-full"> <div className="relative z-20 mb-4 w-full">
<label <label
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
htmlFor="name" htmlFor="name"
> >
Full name Full name
</label> </label>
<input <input
id="name" id="name"
type="text" type="text"
placeholder="John Doe" placeholder="John Doe"
{...register("name")} {...register("name")}
className={cn( className={cn(
"shadow-input h-10 w-full rounded-md border bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white", "shadow-input h-10 w-full rounded-md border bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white",
errors.name ? "border-red-500" : "border-transparent" errors.name ? "border-red-500" : "border-transparent"
)} )}
/> />
{errors.name && ( {errors.name && <p className="mt-1 text-xs text-red-500">{errors.name.message}</p>}
<p className="mt-1 text-xs text-red-500">{errors.name.message}</p> </div>
)} <div className="relative z-20 mb-4 w-full">
</div> <label
<div className="relative z-20 mb-4 w-full"> className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
<label htmlFor="email"
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" >
htmlFor="email" Email Address
> </label>
Email Address <input
</label> id="email"
<input type="email"
id="email" placeholder="john.doe@example.com"
type="email" {...register("email")}
placeholder="john.doe@example.com" className={cn(
{...register("email")} "shadow-input h-10 w-full rounded-md border bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white",
className={cn( errors.email ? "border-red-500" : "border-transparent"
"shadow-input h-10 w-full rounded-md border bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white", )}
errors.email ? "border-red-500" : "border-transparent" />
)} {errors.email && <p className="mt-1 text-xs text-red-500">{errors.email.message}</p>}
/> </div>
{errors.email && ( <div className="relative z-20 mb-4 w-full">
<p className="mt-1 text-xs text-red-500">{errors.email.message}</p> <label
)} className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
</div> htmlFor="company"
<div className="relative z-20 mb-4 w-full"> >
<label Company
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" </label>
htmlFor="company" <input
> id="company"
Company type="text"
</label> placeholder="Example Inc."
<input {...register("company")}
id="company" className={cn(
type="text" "shadow-input h-10 w-full rounded-md border bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white",
placeholder="Example Inc." errors.company ? "border-red-500" : "border-transparent"
{...register("company")} )}
className={cn( />
"shadow-input h-10 w-full rounded-md border bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white", {errors.company && <p className="mt-1 text-xs text-red-500">{errors.company.message}</p>}
errors.company ? "border-red-500" : "border-transparent" </div>
)} <div className="relative z-20 mb-4 w-full">
/> <label
{errors.company && ( className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
<p className="mt-1 text-xs text-red-500">{errors.company.message}</p> htmlFor="message"
)} >
</div> Message <span className="text-neutral-400 text-xs font-normal">(optional)</span>
<div className="relative z-20 mb-4 w-full"> </label>
<label <textarea
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" id="message"
htmlFor="message" rows={5}
> placeholder="Type your message here"
Message {...register("message")}
</label> className={cn(
<textarea "shadow-input w-full rounded-md border bg-white pt-4 pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white",
id="message" errors.message ? "border-red-500" : "border-transparent"
rows={5} )}
placeholder="Type your message here" />
{...register("message")} {errors.message && <p className="mt-1 text-xs text-red-500">{errors.message.message}</p>}
className={cn( </div>
"shadow-input w-full rounded-md border bg-white pt-4 pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white", <button
errors.message ? "border-red-500" : "border-transparent" type="submit"
)} disabled={isSubmitting}
/> className="relative z-10 flex items-center justify-center rounded-md border border-transparent bg-neutral-800 px-4 py-2 text-sm font-medium text-white shadow-[0px_1px_0px_0px_#FFFFFF20_inset] transition duration-200 hover:bg-neutral-900 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
{errors.message && ( >
<p className="mt-1 text-xs text-red-500">{errors.message.message}</p> {isSubmitting ? "Submitting..." : "Submit"}
)} </button>
</div> </form>
<button </div>
type="submit" );
disabled={isSubmitting}
className="relative z-10 flex items-center justify-center rounded-md border border-transparent bg-neutral-800 px-4 py-2 text-sm font-medium text-white shadow-[0px_1px_0px_0px_#FFFFFF20_inset] transition duration-200 hover:bg-neutral-900 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
</div>
);
} }
const Pin = ({ className }: { className?: string }) => { const Pin = ({ className }: { className?: string }) => {
return ( return (
<motion.div <motion.div
style={{ transform: "translateZ(1px)" }} style={{ transform: "translateZ(1px)" }}
className={cn( className={cn(
"pointer-events-none absolute z-[60] flex h-40 w-96 items-center justify-center opacity-100 transition duration-500", "pointer-events-none absolute z-[60] flex h-40 w-96 items-center justify-center opacity-100 transition duration-500",
className, className
)} )}
> >
<div className="h-full w-full"> <div className="h-full w-full">
<div className="absolute inset-x-0 top-0 z-20 mx-auto inline-block w-fit rounded-lg bg-neutral-200 px-2 py-1 text-xs font-normal text-neutral-700 dark:bg-neutral-800 dark:text-white"> <div className="absolute inset-x-0 top-0 z-20 mx-auto inline-block w-fit rounded-lg bg-neutral-200 px-2 py-1 text-xs font-normal text-neutral-700 dark:bg-neutral-800 dark:text-white">
We are here We are here
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-blue-400/0 via-blue-400/90 to-blue-400/0 transition-opacity duration-500"></span> <span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-blue-400/0 via-blue-400/90 to-blue-400/0 transition-opacity duration-500"></span>
</div> </div>
<div <div
style={{ style={{
perspective: "800px", perspective: "800px",
transform: "rotateX(70deg) translateZ(0px)", transform: "rotateX(70deg) translateZ(0px)",
}} }}
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2" className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
> >
<> <>
<motion.div <motion.div
initial={{ opacity: 0, scale: 0 }} initial={{ opacity: 0, scale: 0 }}
animate={{ animate={{
opacity: [0, 1, 0.5, 0], opacity: [0, 1, 0.5, 0],
scale: 1, scale: 1,
}} }}
transition={{ duration: 6, repeat: Infinity, delay: 0 }} transition={{ duration: 6, repeat: Infinity, delay: 0 }}
className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]" className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]"
></motion.div> ></motion.div>
<motion.div <motion.div
initial={{ opacity: 0, scale: 0 }} initial={{ opacity: 0, scale: 0 }}
animate={{ animate={{
opacity: [0, 1, 0.5, 0], opacity: [0, 1, 0.5, 0],
scale: 1, scale: 1,
}} }}
transition={{ duration: 6, repeat: Infinity, delay: 2 }} transition={{ duration: 6, repeat: Infinity, delay: 2 }}
className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]" className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]"
></motion.div> ></motion.div>
<motion.div <motion.div
initial={{ opacity: 0, scale: 0 }} initial={{ opacity: 0, scale: 0 }}
animate={{ animate={{
opacity: [0, 1, 0.5, 0], opacity: [0, 1, 0.5, 0],
scale: 1, scale: 1,
}} }}
transition={{ duration: 6, repeat: Infinity, delay: 4 }} transition={{ duration: 6, repeat: Infinity, delay: 4 }}
className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]" className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]"
></motion.div> ></motion.div>
</> </>
</div> </div>
<> <>
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-blue-500 blur-[2px]" /> <motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-blue-500 blur-[2px]" />
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-blue-500" /> <motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-blue-500" />
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-blue-600 blur-[3px]" /> <motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-blue-600 blur-[3px]" />
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-blue-300" /> <motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-blue-300" />
</> </>
</div> </div>
</motion.div> </motion.div>
); );
}; };
export const FeatureIconContainer = ({ export const FeatureIconContainer = ({
children, children,
className, className,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
}) => { }) => {
return ( return (
<div <div
className={cn( className={cn(
"relative h-14 w-14 rounded-md bg-gradient-to-b from-gray-50 to-neutral-200 p-[4px] dark:from-neutral-800 dark:to-neutral-950", "relative h-14 w-14 rounded-md bg-gradient-to-b from-gray-50 to-neutral-200 p-[4px] dark:from-neutral-800 dark:to-neutral-950",
className, className
)} )}
> >
<div <div
className={cn( className={cn(
"relative z-20 h-full w-full rounded-[5px] bg-gray-50 dark:bg-neutral-800", "relative z-20 h-full w-full rounded-[5px] bg-gray-50 dark:bg-neutral-800",
className, className
)} )}
> >
{children} {children}
</div> </div>
<div className="absolute inset-x-0 bottom-0 z-30 mx-auto h-4 w-full rounded-full bg-neutral-600 opacity-50 blur-lg"></div> <div className="absolute inset-x-0 bottom-0 z-30 mx-auto h-4 w-full rounded-full bg-neutral-600 opacity-50 blur-lg"></div>
<div className="absolute inset-x-0 bottom-0 mx-auto h-px w-[60%] bg-gradient-to-r from-transparent via-blue-500 to-transparent"></div> <div className="absolute inset-x-0 bottom-0 mx-auto h-px w-[60%] bg-gradient-to-r from-transparent via-blue-500 to-transparent"></div>
<div className="absolute inset-x-0 bottom-0 mx-auto h-px w-[60%] bg-gradient-to-r from-transparent via-blue-600 to-transparent dark:h-[8px] dark:blur-sm"></div> <div className="absolute inset-x-0 bottom-0 mx-auto h-px w-[60%] bg-gradient-to-r from-transparent via-blue-600 to-transparent dark:h-[8px] dark:blur-sm"></div>
</div> </div>
); );
}; };
export const Grid = ({ export const Grid = ({ pattern, size }: { pattern?: number[][]; size?: number }) => {
pattern, const p = pattern ?? [
size, [9, 3],
}: { [8, 5],
pattern?: number[][]; [10, 2],
size?: number; [7, 4],
}) => { [9, 6],
const p = pattern ?? [ ];
[9, 3], return (
[8, 5], <div className="pointer-events-none absolute top-0 left-1/2 -mt-2 -ml-20 h-full w-full [mask-image:linear-gradient(white,transparent)]">
[10, 2], <div className="absolute inset-0 bg-gradient-to-r from-zinc-900/30 to-zinc-900/30 opacity-10 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-zinc-900/30 dark:to-zinc-900/30">
[7, 4], <GridPattern
[9, 6], width={size ?? 20}
]; height={size ?? 20}
return ( x="-12"
<div className="pointer-events-none absolute top-0 left-1/2 -mt-2 -ml-20 h-full w-full [mask-image:linear-gradient(white,transparent)]"> y="4"
<div className="absolute inset-0 bg-gradient-to-r from-zinc-900/30 to-zinc-900/30 opacity-10 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-zinc-900/30 dark:to-zinc-900/30"> squares={p}
<GridPattern className="absolute inset-0 h-full w-full fill-black/100 stroke-black/100 mix-blend-overlay dark:fill-white/100 dark:stroke-white/100"
width={size ?? 20} />
height={size ?? 20} </div>
x="-12" </div>
y="4" );
squares={p}
className="absolute inset-0 h-full w-full fill-black/100 stroke-black/100 mix-blend-overlay dark:fill-white/100 dark:stroke-white/100"
/>
</div>
</div>
);
}; };
export function GridPattern({ width, height, x, y, squares, ...props }: any) { export function GridPattern({ width, height, x, y, squares, ...props }: any) {
const patternId = useId(); const patternId = useId();
return ( return (
<svg aria-hidden="true" {...props}> <svg aria-hidden="true" {...props}>
<defs> <defs>
<pattern <pattern
id={patternId} id={patternId}
width={width} width={width}
height={height} height={height}
patternUnits="userSpaceOnUse" patternUnits="userSpaceOnUse"
x={x} x={x}
y={y} y={y}
> >
<path d={`M.5 ${height}V.5H${width}`} fill="none" /> <path d={`M.5 ${height}V.5H${width}`} fill="none" />
</pattern> </pattern>
</defs> </defs>
<rect <rect width="100%" height="100%" strokeWidth={0} fill={`url(#${patternId})`} />
width="100%" {squares && (
height="100%" <svg x={x} y={y} className="overflow-visible">
strokeWidth={0} {squares.map(([x, y]: any, idx: number) => (
fill={`url(#${patternId})`} <rect
/> strokeWidth="0"
{squares && ( key={`${x}-${y}-${idx}`}
<svg x={x} y={y} className="overflow-visible"> width={width + 1}
{squares.map(([x, y]: any, idx: number) => ( height={height + 1}
<rect x={x * width}
strokeWidth="0" y={y * height}
key={`${x}-${y}-${idx}`} />
width={width + 1} ))}
height={height + 1} </svg>
x={x * width} )}
y={y * height} </svg>
/> );
))}
</svg>
)}
</svg>
);
} }

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "motion/react";
import { AlertCircle, Bot, Plus, Trash2 } from "lucide-react"; import { AlertCircle, Bot, Plus, Trash2 } from "lucide-react";
import { motion } from "motion/react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "motion/react";
import { AlertCircle, Bot, Brain, CheckCircle, Zap } from "lucide-react"; import { AlertCircle, Bot, Brain, CheckCircle, Zap } from "lucide-react";
import { motion } from "motion/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "motion/react";
import { ArrowRight, Bot, Brain, CheckCircle, Sparkles, Zap } from "lucide-react"; import { ArrowRight, Bot, Brain, CheckCircle, Sparkles, Zap } from "lucide-react";
import { motion } from "motion/react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";

View file

@ -1,212 +1,202 @@
"use client"; "use client";
import NumberFlow from "@number-flow/react";
import confetti from "canvas-confetti";
import { motion } from "framer-motion";
import { Check, Star } from "lucide-react";
import Link from "next/link";
import { useRef, useState } from "react";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { useMediaQuery } from "@/hooks/use-media-query"; import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { Check, Star } from "lucide-react";
import Link from "next/link";
import { useState, useRef } from "react";
import NumberFlow from "@number-flow/react";
import confetti from "canvas-confetti";
interface PricingPlan { interface PricingPlan {
name: string; name: string;
price: string; price: string;
yearlyPrice: string; yearlyPrice: string;
period: string; period: string;
features: string[]; features: string[];
description: string; description: string;
buttonText: string; buttonText: string;
href: string; href: string;
isPopular: boolean; isPopular: boolean;
} }
interface PricingProps { interface PricingProps {
plans: PricingPlan[]; plans: PricingPlan[];
title?: string; title?: string;
description?: string; description?: string;
} }
export function Pricing({ export function Pricing({
plans, plans,
title = "Simple, Transparent Pricing", title = "Simple, Transparent Pricing",
description = "Choose the plan that works for you\nAll plans include access to our platform, lead generation tools, and dedicated support.", description = "Choose the plan that works for you\nAll plans include access to our platform, lead generation tools, and dedicated support.",
}: PricingProps) { }: PricingProps) {
const [isMonthly, setIsMonthly] = useState(true); const [isMonthly, setIsMonthly] = useState(true);
const isDesktop = useMediaQuery("(min-width: 768px)"); const isDesktop = useMediaQuery("(min-width: 768px)");
const switchRef = useRef<HTMLButtonElement>(null); const switchRef = useRef<HTMLButtonElement>(null);
const handleToggle = (checked: boolean) => { const handleToggle = (checked: boolean) => {
setIsMonthly(!checked); setIsMonthly(!checked);
if (checked && switchRef.current) { if (checked && switchRef.current) {
const rect = switchRef.current.getBoundingClientRect(); const rect = switchRef.current.getBoundingClientRect();
const x = rect.left + rect.width / 2; const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2; const y = rect.top + rect.height / 2;
confetti({ confetti({
particleCount: 50, particleCount: 50,
spread: 60, spread: 60,
origin: { origin: {
x: x / window.innerWidth, x: x / window.innerWidth,
y: y / window.innerHeight, y: y / window.innerHeight,
}, },
colors: [ colors: [
"hsl(var(--primary))", "hsl(var(--primary))",
"hsl(var(--accent))", "hsl(var(--accent))",
"hsl(var(--secondary))", "hsl(var(--secondary))",
"hsl(var(--muted))", "hsl(var(--muted))",
], ],
ticks: 200, ticks: 200,
gravity: 1.2, gravity: 1.2,
decay: 0.94, decay: 0.94,
startVelocity: 30, startVelocity: 30,
shapes: ["circle"], shapes: ["circle"],
}); });
} }
}; };
return ( return (
<div className="container py-20"> <div className="container py-20">
<div className="text-center space-y-4 mb-12"> <div className="text-center space-y-4 mb-12">
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl"> <h2 className="text-4xl font-bold tracking-tight sm:text-5xl">{title}</h2>
{title} <p className="text-muted-foreground text-lg whitespace-pre-line">{description}</p>
</h2> </div>
<p className="text-muted-foreground text-lg whitespace-pre-line">
{description}
</p>
</div>
<div className="flex justify-center mb-10"> <div className="flex justify-center mb-10">
<label className="relative inline-flex items-center cursor-pointer"> <label className="relative inline-flex items-center cursor-pointer">
<Label> <Label>
<Switch <Switch
ref={switchRef as any} ref={switchRef as any}
checked={!isMonthly} checked={!isMonthly}
onCheckedChange={handleToggle} onCheckedChange={handleToggle}
className="relative" className="relative"
/> />
</Label> </Label>
</label> </label>
<span className="ml-2 font-semibold"> <span className="ml-2 font-semibold">
Annual billing <span className="text-primary">(Save 20%)</span> Annual billing <span className="text-primary">(Save 20%)</span>
</span> </span>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4">
{plans.map((plan, index) => ( {plans.map((plan, index) => (
<motion.div <motion.div
key={index} key={index}
initial={{ y: 50, opacity: 1 }} initial={{ y: 50, opacity: 1 }}
whileInView={ whileInView={
isDesktop isDesktop
? { ? {
y: plan.isPopular ? -20 : 0, y: plan.isPopular ? -20 : 0,
opacity: 1, opacity: 1,
x: index === 2 ? -30 : index === 0 ? 30 : 0, x: index === 2 ? -30 : index === 0 ? 30 : 0,
scale: index === 0 || index === 2 ? 0.94 : 1.0, scale: index === 0 || index === 2 ? 0.94 : 1.0,
} }
: {} : {}
} }
viewport={{ once: true }} viewport={{ once: true }}
transition={{ transition={{
duration: 1.6, duration: 1.6,
type: "spring", type: "spring",
stiffness: 100, stiffness: 100,
damping: 30, damping: 30,
delay: 0.4, delay: 0.4,
opacity: { duration: 0.5 }, opacity: { duration: 0.5 },
}} }}
className={cn( className={cn(
`rounded-2xl border-[1px] p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`, `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", plan.isPopular ? "border-primary border-2" : "border-border",
"flex flex-col", "flex flex-col",
!plan.isPopular && "mt-5", !plan.isPopular && "mt-5",
index === 0 || index === 2 index === 0 || index === 2
? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-[10deg]" ? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-[10deg]"
: "z-10", : "z-10",
index === 0 && "origin-right", index === 0 && "origin-right",
index === 2 && "origin-left" index === 2 && "origin-left"
)} )}
> >
{plan.isPopular && ( {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"> <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" /> <Star className="text-primary-foreground h-4 w-4 fill-current" />
<span className="text-primary-foreground ml-1 font-sans font-semibold"> <span className="text-primary-foreground ml-1 font-sans font-semibold">
Popular Popular
</span> </span>
</div> </div>
)} )}
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<p className="text-base font-semibold text-muted-foreground"> <p className="text-base font-semibold text-muted-foreground">{plan.name}</p>
{plan.name} <div className="mt-6 flex items-center justify-center gap-x-2">
</p> <span className="text-5xl font-bold tracking-tight text-foreground">
<div className="mt-6 flex items-center justify-center gap-x-2"> <NumberFlow
<span className="text-5xl font-bold tracking-tight text-foreground"> value={isMonthly ? Number(plan.price) : Number(plan.yearlyPrice)}
<NumberFlow format={{
value={ style: "currency",
isMonthly ? Number(plan.price) : Number(plan.yearlyPrice) currency: "USD",
} minimumFractionDigits: 0,
format={{ maximumFractionDigits: 0,
style: "currency", }}
currency: "USD", transformTiming={{
minimumFractionDigits: 0, duration: 500,
maximumFractionDigits: 0, easing: "ease-out",
}} }}
transformTiming={{ willChange
duration: 500, className="font-variant-numeric: tabular-nums"
easing: "ease-out", />
}} </span>
willChange {plan.period !== "Next 3 months" && (
className="font-variant-numeric: tabular-nums" <span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground">
/> / {plan.period}
</span> </span>
{plan.period !== "Next 3 months" && ( )}
<span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground"> </div>
/ {plan.period}
</span>
)}
</div>
<p className="text-xs leading-5 text-muted-foreground"> <p className="text-xs leading-5 text-muted-foreground">
{isMonthly ? "billed monthly" : "billed annually"} {isMonthly ? "billed monthly" : "billed annually"}
</p> </p>
<ul className="mt-5 gap-2 flex flex-col"> <ul className="mt-5 gap-2 flex flex-col">
{plan.features.map((feature, idx) => ( {plan.features.map((feature, idx) => (
<li key={idx} className="flex items-start gap-2"> <li key={idx} className="flex items-start gap-2">
<Check className="h-4 w-4 text-primary mt-1 flex-shrink-0" /> <Check className="h-4 w-4 text-primary mt-1 flex-shrink-0" />
<span className="text-left">{feature}</span> <span className="text-left">{feature}</span>
</li> </li>
))} ))}
</ul> </ul>
<hr className="w-full my-4" /> <hr className="w-full my-4" />
<Link <Link
href={plan.href} href={plan.href}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: "outline", variant: "outline",
}), }),
"group relative w-full gap-2 overflow-hidden text-lg font-semibold tracking-tighter", "group relative w-full gap-2 overflow-hidden text-lg font-semibold tracking-tighter",
"transform-gpu ring-offset-current transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-1 hover:bg-primary hover:text-primary-foreground", "transform-gpu ring-offset-current transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-1 hover:bg-primary hover:text-primary-foreground",
plan.isPopular plan.isPopular
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "bg-background text-foreground" : "bg-background text-foreground"
)} )}
> >
{plan.buttonText} {plan.buttonText}
</Link> </Link>
<p className="mt-6 text-xs leading-5 text-muted-foreground"> <p className="mt-6 text-xs leading-5 text-muted-foreground">{plan.description}</p>
{plan.description} </div>
</p> </motion.div>
</div> ))}
</motion.div> </div>
))} </div>
</div> );
</div>
);
} }

View file

@ -3,72 +3,72 @@
import { Pricing } from "@/components/pricing"; import { Pricing } from "@/components/pricing";
const demoPlans = [ const demoPlans = [
{ {
name: "STARTER", name: "STARTER",
price: "50", price: "50",
yearlyPrice: "40", yearlyPrice: "40",
period: "per month", period: "per month",
features: [ features: [
"Up to 10 projects", "Up to 10 projects",
"Basic analytics", "Basic analytics",
"48-hour support response time", "48-hour support response time",
"Limited API access", "Limited API access",
"Community support", "Community support",
], ],
description: "Perfect for individuals and small projects", description: "Perfect for individuals and small projects",
buttonText: "Start Free Trial", buttonText: "Start Free Trial",
href: "/sign-up", href: "/sign-up",
isPopular: false, isPopular: false,
}, },
{ {
name: "PROFESSIONAL", name: "PROFESSIONAL",
price: "99", price: "99",
yearlyPrice: "79", yearlyPrice: "79",
period: "per month", period: "per month",
features: [ features: [
"Unlimited projects", "Unlimited projects",
"Advanced analytics", "Advanced analytics",
"24-hour support response time", "24-hour support response time",
"Full API access", "Full API access",
"Priority support", "Priority support",
"Team collaboration", "Team collaboration",
"Custom integrations", "Custom integrations",
], ],
description: "Ideal for growing teams and businesses", description: "Ideal for growing teams and businesses",
buttonText: "Get Started", buttonText: "Get Started",
href: "/sign-up", href: "/sign-up",
isPopular: true, isPopular: true,
}, },
{ {
name: "ENTERPRISE", name: "ENTERPRISE",
price: "299", price: "299",
yearlyPrice: "239", yearlyPrice: "239",
period: "per month", period: "per month",
features: [ features: [
"Everything in Professional", "Everything in Professional",
"Custom solutions", "Custom solutions",
"Dedicated account manager", "Dedicated account manager",
"1-hour support response time", "1-hour support response time",
"SSO Authentication", "SSO Authentication",
"Advanced security", "Advanced security",
"Custom contracts", "Custom contracts",
"SLA agreement", "SLA agreement",
], ],
description: "For large organizations with specific needs", description: "For large organizations with specific needs",
buttonText: "Contact Sales", buttonText: "Contact Sales",
href: "/contact", href: "/contact",
isPopular: false, isPopular: false,
}, },
]; ];
function PricingBasic() { function PricingBasic() {
return ( return (
<Pricing <Pricing
plans={demoPlans} plans={demoPlans}
title="Simple, Transparent Pricing" title="Simple, Transparent Pricing"
description="Choose the plan that works for you" description="Choose the plan that works for you"
/> />
); );
} }
export default PricingBasic; export default PricingBasic;

View file

@ -1,8 +1,8 @@
"use client"; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { motion, type Variants } from "motion/react";
import { MoveLeftIcon, Plus, Search, Trash2 } from "lucide-react"; import { MoveLeftIcon, Plus, Search, Trash2 } from "lucide-react";
import { motion, type Variants } from "motion/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import { motion } from "motion/react";
import { import {
AlertCircle, AlertCircle,
Bot, Bot,
@ -13,6 +12,7 @@ import {
Settings2, Settings2,
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import { motion } from "motion/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import { AnimatePresence, motion } from "motion/react";
import { import {
AlertCircle, AlertCircle,
Bot, Bot,
@ -15,6 +14,7 @@ import {
Settings2, Settings2,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "motion/react";
import { MoonIcon, SunIcon } from "lucide-react"; import { MoonIcon, SunIcon } from "lucide-react";
import { motion } from "motion/react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";

View file

@ -1,29 +1,29 @@
"use client" "use client";
import * as React from "react" import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className, className
)} )}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0", "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>
)) ));
Switch.displayName = SwitchPrimitives.Root.displayName Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch } export { Switch };