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 {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<ContactFormData>({
resolver: zodResolver(contactFormSchema),
});
const onSubmit = async (data: ContactFormData) => { const {
setIsSubmitting(true); register,
handleSubmit,
try { formState: { errors },
const response = await fetch('/api/contact', { reset,
method: 'POST', } = useForm<ContactFormData>({
headers: { resolver: zodResolver(contactFormSchema),
'Content-Type': 'application/json', });
},
body: JSON.stringify(data),
});
const result = await response.json(); const onSubmit = async (data: ContactFormData) => {
setIsSubmitting(true);
if (response.ok) { try {
toast.success('Message sent successfully!', { const response = await fetch("/api/contact", {
description: 'We will get back to you as soon as possible.', method: "POST",
}); headers: {
reset(); "Content-Type": "application/json",
} else { },
toast.error('Failed to send message', { body: JSON.stringify(data),
description: result.message || 'Please try again later.', });
});
}
} catch (error) {
console.error('Error submitting form:', error);
toast.error('Something went wrong', {
description: 'Please try again later.',
});
} finally {
setIsSubmitting(false);
}
};
return ( const result = await response.json();
<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="flex items-start justify-start">
<FeatureIconContainer className="flex items-center justify-center overflow-hidden">
<IconMailFilled className="h-6 w-6 text-blue-500" />
</FeatureIconContainer>
</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">
Contact us
</h2>
<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.
Contact us and let us know how we can help you.
</p>
<div className="mt-10 hidden flex-col items-center gap-4 md:flex-row lg:flex"> if (response.ok) {
<Link href="mailto:rohan@surfsense.com" className="text-sm text-neutral-500 dark:text-neutral-400"> toast.success("Message sent successfully!", {
rohan@surfsense.com description: "We will get back to you as soon as possible.",
</Link> });
<div className="h-1 w-1 rounded-full bg-neutral-500 dark:bg-neutral-400" /> reset();
} else {
toast.error("Failed to send message", {
description: result.message || "Please try again later.",
});
}
} catch (error) {
console.error("Error submitting form:", error);
toast.error("Something went wrong", {
description: "Please try again later.",
});
} finally {
setIsSubmitting(false);
}
};
<Link href="https://cal.com/mod-surfsense" className="text-sm text-neutral-500 dark:text-neutral-400"> return (
https://cal.com/mod-surfsense <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">
</Link> <div className="relative flex flex-col items-center overflow-hidden lg:items-start">
</div> <div className="flex items-start justify-start">
<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"> <FeatureIconContainer className="flex items-center justify-center overflow-hidden">
<Pin className="h-30 w-85 top-0 left-0" /> <IconMailFilled className="h-6 w-6 text-blue-500" />
</FeatureIconContainer>
</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">
Contact us
</h2>
<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. Contact us and let us
know how we can help you.
</p>
<Image <div className="mt-10 hidden flex-col items-center gap-4 md:flex-row lg:flex">
src="/contact/world.svg" <Link
width={500} href="mailto:rohan@surfsense.com"
height={500} className="text-sm text-neutral-500 dark:text-neutral-400"
alt="world map" >
className="[transform:rotateX(45deg)_translateZ(0px)] dark:invert dark:filter" rohan@surfsense.com
/> </Link>
</div> <div className="h-1 w-1 rounded-full bg-neutral-500 dark:bg-neutral-400" />
</div>
<form <Link
onSubmit={handleSubmit(onSubmit)} href="https://cal.com/mod-surfsense"
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="text-sm text-neutral-500 dark:text-neutral-400"
> >
<Grid size={20} /> https://cal.com/mod-surfsense
<div className="relative z-20 mb-4 w-full"> </Link>
<label </div>
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" <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">
htmlFor="name" <Pin className="h-30 w-85 top-0 left-0" />
>
Full name <Image
</label> src="/contact/world.svg"
<input width={500}
id="name" height={500}
type="text" alt="world map"
placeholder="John Doe" className="[transform:rotateX(45deg)_translateZ(0px)] dark:invert dark:filter"
{...register("name")} />
className={cn( </div>
"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", </div>
errors.name ? "border-red-500" : "border-transparent" <form
)} 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"
{errors.name && ( >
<p className="mt-1 text-xs text-red-500">{errors.name.message}</p> <Grid size={20} />
)} <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="name"
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" >
htmlFor="email" Full name
> </label>
Email Address <input
</label> id="name"
<input type="text"
id="email" placeholder="John Doe"
type="email" {...register("name")}
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.name ? "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.name && <p className="mt-1 text-xs text-red-500">{errors.name.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="email"
<div className="relative z-20 mb-4 w-full"> >
<label Email Address
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" </label>
htmlFor="company" <input
> id="email"
Company type="email"
</label> placeholder="john.doe@example.com"
<input {...register("email")}
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.email ? "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.email && <p className="mt-1 text-xs text-red-500">{errors.email.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="company"
)} >
</div> Company
<div className="relative z-20 mb-4 w-full"> </label>
<label <input
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300" id="company"
htmlFor="message" type="text"
> placeholder="Example Inc."
Message {...register("company")}
</label> className={cn(
<textarea "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",
id="message" errors.company ? "border-red-500" : "border-transparent"
rows={5} )}
placeholder="Type your message here" />
{...register("message")} {errors.company && <p className="mt-1 text-xs text-red-500">{errors.company.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", <div className="relative z-20 mb-4 w-full">
errors.message ? "border-red-500" : "border-transparent" <label
)} className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
/> htmlFor="message"
{errors.message && ( >
<p className="mt-1 text-xs text-red-500">{errors.message.message}</p> Message <span className="text-neutral-400 text-xs font-normal">(optional)</span>
)} </label>
</div> <textarea
<button id="message"
type="submit" rows={5}
disabled={isSubmitting} placeholder="Type your message here"
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" {...register("message")}
> className={cn(
{isSubmitting ? 'Submitting...' : 'Submit'} "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"
</form> )}
</div> />
); {errors.message && <p className="mt-1 text-xs text-red-500">{errors.message.message}</p>}
</div>
<button
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 };