Integrate React components and update homepage layout: added Hero, Feature, Stats, Pricing, FAQ, Contact, and Footer sections; updated global styles and dependencies for React support.
This commit is contained in:
210
src/components/contact21.tsx
Normal file
210
src/components/contact21.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CornerDownRight, LoaderIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const contactFormSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "Email is required")
|
||||
.email("Please enter a valid email"),
|
||||
message: z.string().min(1, "Message is required"),
|
||||
});
|
||||
|
||||
type ContactFormData = z.infer<typeof contactFormSchema>;
|
||||
|
||||
interface Contact21Props {
|
||||
className?: string;
|
||||
onSubmit?: (data: ContactFormData) => Promise<void>;
|
||||
}
|
||||
|
||||
const Contact21 = ({ className, onSubmit }: Contact21Props) => {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
const form = useForm<ContactFormData>({
|
||||
resolver: zodResolver(contactFormSchema),
|
||||
mode: "onSubmit",
|
||||
reValidateMode: "onSubmit",
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleFormSubmit = async (data: ContactFormData) => {
|
||||
try {
|
||||
if (onSubmit) {
|
||||
await onSubmit(data);
|
||||
} else {
|
||||
console.log("Form submitted:", data);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
setIsSubmitted(true);
|
||||
setShowSuccess(true);
|
||||
form.reset();
|
||||
setTimeout(() => setShowSuccess(false), 4500);
|
||||
setTimeout(() => setIsSubmitted(false), 5000);
|
||||
} catch {
|
||||
form.setError("root", {
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={cn("py-32", className)}>
|
||||
<div className="container">
|
||||
<div className="mt-20 flex flex-col justify-between gap-15 md:gap-10 lg:flex-row">
|
||||
<div className="flex w-full max-w-lg flex-col justify-between gap-10">
|
||||
<p className="indent-[22%] text-3xl font-medium tracking-tight text-muted-foreground/50 lg:text-4xl">
|
||||
We are a team of creators, thinkers, and builders who believe in
|
||||
crafting experiences that truly connect.{" "}
|
||||
<span className="text-foreground">We're here to help</span>
|
||||
</p>
|
||||
<div className="mt-5 flex items-center gap-4 lg:mt-20">
|
||||
<img
|
||||
src="https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/avatar3.png"
|
||||
className="size-12"
|
||||
alt="avatar"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium tracking-tight">John Doe</h3>
|
||||
<p className="text-sm text-foreground/40">Creative Director</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-4 flex w-full flex-col gap-2 lg:pl-10">
|
||||
<h1 className="mb-7 text-6xl font-semibold tracking-tight lg:text-5xl">
|
||||
Get in Touch
|
||||
</h1>
|
||||
|
||||
{isSubmitted && (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 rounded-lg border border-green-500/20 bg-green-500/10 p-4 text-center transition-opacity duration-500",
|
||||
showSuccess ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
<p className="text-sm font-medium text-green-600 dark:text-green-400">
|
||||
Thank you! We'll be in touch soon.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.handleSubmit(handleFormSubmit)}>
|
||||
<FieldGroup className="gap-0">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name} className="sr-only">
|
||||
Name
|
||||
</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="Name*"
|
||||
className="h-15 rounded-none border-0 border-b border-b-foreground/25 !bg-transparent shadow-none placeholder:text-foreground/20 focus-visible:ring-0 lg:text-base"
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name} className="sr-only">
|
||||
Email
|
||||
</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
type="email"
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="Email*"
|
||||
className="h-15 rounded-none border-0 border-b border-b-foreground/25 !bg-transparent shadow-none placeholder:text-foreground/20 focus-visible:ring-0 lg:text-base"
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="message"
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name} className="sr-only">
|
||||
Message
|
||||
</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="Message (Tell us about your project)"
|
||||
className="h-15 rounded-none border-0 border-b border-b-foreground/25 !bg-transparent shadow-none placeholder:text-foreground/20 focus-visible:ring-0 lg:text-base"
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
{form.formState.errors.root && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.root.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-15 flex items-center justify-start gap-2 rounded-none !px-8 lg:h-12 lg:text-base"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{form.formState.isSubmitting ? (
|
||||
<>
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CornerDownRight className="size-5" />
|
||||
Get in touch
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { Contact21 };
|
||||
82
src/components/faq7.tsx
Normal file
82
src/components/faq7.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What is a FAQ and why is it important?",
|
||||
answer:
|
||||
"FAQ stands for Frequently Asked Questions. It is a list that provides answers to common questions people may have about a specific product, service, or topic.",
|
||||
},
|
||||
{
|
||||
question: "Why should I use a FAQ on my website or app?",
|
||||
answer:
|
||||
"Utilizing a FAQ section on your website or app is a practical way to offer instant assistance to your users or customers. Instead of waiting for customer support responses, they can find quick answers to commonly asked questions. ",
|
||||
},
|
||||
{
|
||||
question: "How do I effectively create a FAQ section?",
|
||||
answer:
|
||||
"Creating a FAQ section starts with gathering the most frequent questions you receive from your users or customers. Once you have a list, you need to write clear, detailed, and helpful answers to each question.",
|
||||
},
|
||||
{
|
||||
question: "What are the benefits of having a well-maintained FAQ section?",
|
||||
answer:
|
||||
"There are numerous advantages to maintaining a robust FAQ section. Firstly, it provides immediate answers to common queries, which improves the user experience.",
|
||||
},
|
||||
{
|
||||
question: "How do I effectively create a FAQ section?",
|
||||
answer:
|
||||
"Creating a FAQ section starts with gathering the most frequent questions you receive from your users or customers. Once you have a list, you need to write clear, detailed, and helpful answers to each question.",
|
||||
},
|
||||
];
|
||||
|
||||
interface Faq7Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Faq7 = ({ className }: Faq7Props) => {
|
||||
return (
|
||||
<section className={cn("py-32", className)}>
|
||||
<div className="container">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-6">
|
||||
<h2 className="text-4xl font-semibold">
|
||||
Need Help?
|
||||
<br />
|
||||
<span className="text-muted-foreground/70">
|
||||
We're here to assist.
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground md:text-xl">
|
||||
Still have questions? Feel free to contact our friendly
|
||||
<a href="#" className="mx-1 whitespace-nowrap underline">
|
||||
support team
|
||||
</a>
|
||||
specialists.
|
||||
</p>
|
||||
<Button size="lg" variant="outline" className="w-fit">
|
||||
View all FAQs
|
||||
</Button>
|
||||
</div>
|
||||
<Accordion type="multiple">
|
||||
{faqs.map((faq, index) => (
|
||||
<AccordionItem key={index} value={`item-${index}`}>
|
||||
<AccordionTrigger className="text-left">
|
||||
{faq.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>{faq.answer}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { Faq7 };
|
||||
96
src/components/feature284.tsx
Normal file
96
src/components/feature284.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { HelpCircleIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
import { GlowingEffect } from "@/components/ui/glowing-effect";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const featureData = [
|
||||
{
|
||||
desc: "Lorem ipsum dolor sit amet consec adipisicing elit. Quisquam, quos.",
|
||||
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img1.jpeg",
|
||||
title: "Quality",
|
||||
badgeTitle: "#1 Block",
|
||||
gridClass: "md:col-span-1",
|
||||
},
|
||||
{
|
||||
desc: "Consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore. Lorem ipsum dolor sit amet consec adipisicing elit.",
|
||||
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img7.jpeg",
|
||||
title: "Innovation",
|
||||
badgeTitle: "#2 Block",
|
||||
gridClass: "lg:col-span-2",
|
||||
},
|
||||
{
|
||||
desc: "Ut enim ad minim veniam quis nostrud exercitation ullamco laboris.",
|
||||
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img11.jpeg",
|
||||
title: "Performance",
|
||||
badgeTitle: "#3 Block",
|
||||
gridClass: "md:col-span-1 lg:row-span-2 ",
|
||||
},
|
||||
{
|
||||
desc: "Consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore. Lorem ipsum dolor sit amet consec adipisicing elit.",
|
||||
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img2.jpeg",
|
||||
title: "Innovation",
|
||||
badgeTitle: "#2 Block",
|
||||
gridClass: "lg:col-span-2",
|
||||
},
|
||||
{
|
||||
desc: "Duis aute irure dolor in reprehenderit in voluptate velit esse.",
|
||||
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img4.jpeg",
|
||||
title: "Reliability",
|
||||
badgeTitle: "#4 Block",
|
||||
gridClass: "md:col-span-1",
|
||||
},
|
||||
];
|
||||
|
||||
interface Feature284Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Feature284 = ({ className }: Feature284Props) => {
|
||||
return (
|
||||
<section className={cn("h-full overflow-hidden py-32", className)}>
|
||||
<div className="container flex h-full w-full items-center justify-center">
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 grid-rows-2 gap-4 md:grid-cols-2 lg:h-[800px] lg:grid-cols-4">
|
||||
{featureData.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"relative flex flex-col gap-2 rounded-3xl border p-4",
|
||||
feature.gridClass,
|
||||
)}
|
||||
>
|
||||
<GlowingEffect
|
||||
spread={40}
|
||||
glow={true}
|
||||
disabled={false}
|
||||
proximity={64}
|
||||
inactiveZone={0.01}
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-muted-foreground">{feature.badgeTitle}</p>
|
||||
<HelpCircleIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full flex-1 overflow-hidden rounded-3xl bg-muted",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={feature.img}
|
||||
alt={feature.title}
|
||||
className="pointer-events-none h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="mt-4 text-2xl font-semibold tracking-tight">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { Feature284 };
|
||||
142
src/components/footer27.tsx
Normal file
142
src/components/footer27.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Footer27Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Footer27 = ({ className }: Footer27Props) => {
|
||||
const socialLinks = [
|
||||
{ name: "Instagram", href: "#" },
|
||||
{ name: "X (Twitter)", href: "#" },
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={cn("py-32", className)}>
|
||||
<div className="container">
|
||||
<footer>
|
||||
<div>
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="flex flex-col justify-between md:flex-row md:items-center"
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<motion.div variants={itemVariants} className="space-y-6">
|
||||
<h2 className="text-4xl leading-tight font-bold text-foreground lg:text-5xl">
|
||||
Connect with Me
|
||||
</h2>
|
||||
<p className="max-w-md text-lg leading-relaxed text-muted-foreground">
|
||||
No commitments. Just a quick chat to see if we click.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<Button size="lg">Get in Touch</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-8 md:mt-0">
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="space-y-6">
|
||||
{socialLinks.map((link) => (
|
||||
<motion.div
|
||||
key={link.name}
|
||||
variants={itemVariants}
|
||||
whileHover={{ x: 4 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={link.href}
|
||||
className="group flex items-center gap-2 py-2 text-foreground transition-colors hover:text-foreground/80"
|
||||
>
|
||||
<span className="text-xl font-medium">
|
||||
{link.name}
|
||||
</span>
|
||||
<ArrowUpRight className="h-6 w-6 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="mt-16"
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<Separator className="mb-8" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
© Copyright 2025. All rights Reserved.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
Made by{" "}
|
||||
<motion.a
|
||||
href="https://x.com/shadcnblocks"
|
||||
className="underline underline-offset-4 transition-colors hover:text-foreground"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
shadcnblocks
|
||||
</motion.a>
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { Footer27 };
|
||||
96
src/components/hero235.tsx
Normal file
96
src/components/hero235.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ArrowDown, BookOpen, LayoutGrid, Sparkles } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Hero235Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SERVICES = [
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Visual Identity",
|
||||
description:
|
||||
"We shape visual systems that blend craft and clarity to spark recognition and connection.",
|
||||
},
|
||||
{
|
||||
icon: LayoutGrid,
|
||||
title: "Interactive Experiences",
|
||||
description:
|
||||
"Experiences that tie design and technology together to tell your story in motion.",
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: "Workshops",
|
||||
description:
|
||||
"Hands-on sessions that adapt to your goals and help you grow with practical guidance.",
|
||||
},
|
||||
];
|
||||
|
||||
const Hero235 = ({ className }: Hero235Props) => {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"relative flex h-dvh items-center justify-center overflow-hidden bg-background py-32",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div aria-hidden={true}>
|
||||
<div className="absolute -right-0 -bottom-[30rem] size-[35rem] rounded-full bg-rose-400 opacity-40 blur-[5rem] md:-right-[2rem] md:-bottom-[50rem] md:size-[55rem] dark:opacity-20" />
|
||||
<div className="absolute -right-[20rem] -bottom-[20rem] size-[35rem] rounded-full bg-sky-500 opacity-40 blur-[5rem] md:-right-[32rem] md:-bottom-[36rem] md:size-[55rem] dark:opacity-20" />
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 z-0 opacity-40"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(https://deifkwefumgah.cloudfront.net/shadcnblocks/block/noise.png)",
|
||||
backgroundRepeat: "repeat",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative container flex h-full flex-col justify-between">
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="mx-auto flex max-w-2xl flex-col items-center text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tight text-balance text-foreground md:text-5xl lg:text-6xl">
|
||||
Shadcnblocks. Digital craft with an eye for detail
|
||||
</h1>
|
||||
<p className="mt-8 max-w-xl text-pretty text-muted-foreground md:text-lg">
|
||||
We build clear, thoughtful interfaces that work as well as they
|
||||
look. If you want a partner who turns your vision into something
|
||||
real, you're in the right place.
|
||||
</p>
|
||||
<Button size="lg" className="mt-10">
|
||||
Explore our work
|
||||
<ArrowDown className="shrink-0" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-16">
|
||||
<div className="grid gap-12 lg:grid-cols-3 lg:gap-0">
|
||||
{SERVICES.map((service) => {
|
||||
const Icon = service.icon;
|
||||
return (
|
||||
<div
|
||||
key={service.title}
|
||||
className={cn(
|
||||
"flex flex-col border-l border-primary px-6 md:px-8",
|
||||
)}
|
||||
>
|
||||
<Icon className="mb-4 size-6" aria-hidden />
|
||||
<h2 className="font-semibold text-foreground">
|
||||
{service.title}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { Hero235 };
|
||||
154
src/components/pricing4.tsx
Normal file
154
src/components/pricing4.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PricingPlan {
|
||||
name: string;
|
||||
badge: string;
|
||||
monthlyPrice: string;
|
||||
yearlyPrice: string;
|
||||
features: string[];
|
||||
buttonText: string;
|
||||
isPopular?: boolean;
|
||||
}
|
||||
|
||||
interface Pricing4Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
plans?: PricingPlan[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Pricing4 = ({
|
||||
title = "Pricing",
|
||||
description = "Check out our affordable pricing plans.",
|
||||
plans = [
|
||||
{
|
||||
name: "Free",
|
||||
badge: "Free",
|
||||
monthlyPrice: "$0",
|
||||
yearlyPrice: "$0",
|
||||
features: [
|
||||
"Unlimited Integrations",
|
||||
"Windows, Linux, Mac support",
|
||||
"24/7 Support",
|
||||
"Free updates",
|
||||
],
|
||||
buttonText: "Get Started",
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
badge: "Pro",
|
||||
monthlyPrice: "$29",
|
||||
yearlyPrice: "$249",
|
||||
features: [
|
||||
"Everything in FREE",
|
||||
"Live call support every month",
|
||||
"Unlimited Storage",
|
||||
],
|
||||
buttonText: "Purchase",
|
||||
},
|
||||
{
|
||||
name: "Elite",
|
||||
badge: "Elite",
|
||||
monthlyPrice: "$59",
|
||||
yearlyPrice: "$549",
|
||||
features: [
|
||||
"Everything in PRO",
|
||||
"Advanced analytics",
|
||||
"Custom branding",
|
||||
"Unlimited users",
|
||||
],
|
||||
buttonText: "Purchase",
|
||||
isPopular: true,
|
||||
},
|
||||
],
|
||||
className,
|
||||
}: Pricing4Props) => {
|
||||
const [isAnnually, setIsAnnually] = useState(false);
|
||||
return (
|
||||
<section className={cn("py-32", className)}>
|
||||
<div className="container mx-auto">
|
||||
<div className="flex flex-col gap-6">
|
||||
<h2 className="text-4xl font-semibold text-pretty lg:text-6xl">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="flex flex-col justify-between gap-10 md:flex-row">
|
||||
<p className="max-w-3xl text-muted-foreground lg:text-xl">
|
||||
{description}
|
||||
</p>
|
||||
<Tabs
|
||||
value={isAnnually ? "annually" : "monthly"}
|
||||
onValueChange={(value: string) =>
|
||||
setIsAnnually(value === "annually")
|
||||
}
|
||||
className="w-fit shrink-0"
|
||||
aria-label="Billing period"
|
||||
>
|
||||
<TabsList className="grid h-11 w-max grid-cols-2 gap-0 rounded-md p-1 text-lg">
|
||||
<TabsTrigger
|
||||
value="monthly"
|
||||
className="h-full min-h-0 px-7 py-0 font-semibold text-muted-foreground data-active:text-foreground"
|
||||
>
|
||||
Monthly
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="annually"
|
||||
className="h-full min-h-0 px-7 py-0 font-semibold text-muted-foreground data-active:text-foreground"
|
||||
>
|
||||
Yearly
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-stretch gap-6 md:flex-row">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.name}
|
||||
className={`flex w-full flex-col rounded-lg border p-6 text-left ${
|
||||
plan.isPopular ? "bg-muted" : ""
|
||||
}`}
|
||||
>
|
||||
<Badge className="mb-8 block w-fit uppercase">
|
||||
{plan.badge}
|
||||
</Badge>
|
||||
<h3 className="font-mono text-4xl lg:text-5xl">
|
||||
{isAnnually ? plan.yearlyPrice : plan.monthlyPrice}
|
||||
</h3>
|
||||
<p
|
||||
className={`text-muted-foreground ${plan.monthlyPrice === "$0" ? "invisible" : ""}`}
|
||||
>
|
||||
{isAnnually ? "Per year" : "Per month"}
|
||||
</p>
|
||||
<Separator className="my-6" />
|
||||
<div className="flex h-full flex-col justify-between gap-20">
|
||||
<ul className="space-y-4 text-muted-foreground md:leading-snug">
|
||||
{plan.features.map((feature, featureIndex) => (
|
||||
<li
|
||||
key={featureIndex}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Check className="size-4 shrink-0" aria-hidden="true" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button className="w-full">{plan.buttonText}</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { Pricing4 };
|
||||
69
src/components/stats11.tsx
Normal file
69
src/components/stats11.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Stats11Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Stats11 = ({ className }: Stats11Props) => {
|
||||
return (
|
||||
<section className={cn("py-32", className)}>
|
||||
<div className="container">
|
||||
<div className="relative isolate overflow-hidden bg-linear-to-b from-primary/10 to-transparent md:border-x md:border-border">
|
||||
<div className="absolute right-0 -left-px -z-20 h-full w-full bg-[linear-gradient(90deg,var(--muted-foreground)_1px,transparent_1px)] [mask-image:linear-gradient(transparent_25%,black_25%,black_75%,transparent_75%)] [background-size:calc(100%/16)_100%] [mask-size:100%_16px] opacity-20 [-webkit-mask-image:linear-gradient(transparent_25%,black_25%,black_75%,transparent_75%)] [-webkit-mask-size:100%_16px]" />
|
||||
|
||||
<div>
|
||||
<h2 className="mb-16 max-w-3xl text-3xl leading-10 font-semibold sm:mb-24 md:mx-10">
|
||||
Revolutionizing healthcare with AI technology.
|
||||
<span className="font-medium text-primary/50">
|
||||
{" "}
|
||||
Our advanced diagnostic platform helps doctors make accurate
|
||||
diagnoses in seconds.
|
||||
</span>
|
||||
</h2>
|
||||
<div className="relative grid max-w-2xl gap-4 border-x border-border pb-32 sm:grid-cols-2 sm:gap-10 sm:pb-44 md:ml-10 md:border-0">
|
||||
<div className="absolute inset-0 -top-[1100px] -left-[calc(1000px-22vw)] -z-10 size-[1500px] rounded-full border border-primary bg-background sm:-top-[480%] sm:-left-[185%] sm:size-[2000px] md:-top-[906%] md:-left-[294%] md:size-[3500px] lg:-top-[1186%] lg:-left-[380%] lg:size-[4500px] xl:-top-[1200%] xl:-left-[350%] 2xl:-top-[1196%] 2xl:-left-[345%]"></div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex gap-5 text-3xl font-semibold">
|
||||
<span className="relative -left-px w-px bg-primary/50"></span>
|
||||
1,000,000+
|
||||
</span>
|
||||
<p className="pl-5 font-medium text-muted-foreground/80">
|
||||
Diagnoses Made
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex gap-5 text-3xl font-semibold">
|
||||
<span className="relative -left-px w-px bg-primary/50"></span>
|
||||
95%
|
||||
</span>
|
||||
<p className="pl-5 font-medium text-muted-foreground/80">
|
||||
Diagnostic Accuracy
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex gap-5 text-3xl font-semibold">
|
||||
<span className="relative -left-px w-px bg-primary/50"></span>
|
||||
3,000+
|
||||
</span>
|
||||
<p className="pl-5 font-medium text-muted-foreground/80">
|
||||
Healthcare Providers
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex gap-5 text-3xl font-semibold">
|
||||
<span className="relative -left-px w-px bg-primary/50"></span>
|
||||
2.5s
|
||||
</span>
|
||||
<p className="pl-5 font-medium text-muted-foreground/80">
|
||||
Latency
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { Stats11 };
|
||||
79
src/components/ui/accordion.tsx
Normal file
79
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
import { Accordion as AccordionPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
function Accordion({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
data-slot="accordion"
|
||||
className={cn("flex w-full flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("not-last:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
|
||||
<ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
49
src/components/ui/badge.tsx
Normal file
49
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
236
src/components/ui/field.tsx
Normal file
236
src/components/ui/field.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import { useMemo } from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLegend({
|
||||
className,
|
||||
variant = "legend",
|
||||
...props
|
||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||
return (
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const fieldVariants = cva(
|
||||
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
||||
horizontal:
|
||||
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
responsive:
|
||||
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Field({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
data-orientation={orientation}
|
||||
className={cn(fieldVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Label>) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
||||
"last:mt-0 nth-last-2:-mt-1",
|
||||
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
||||
data-slot="field-separator-content"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldError({
|
||||
className,
|
||||
children,
|
||||
errors,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
errors?: Array<{ message?: string } | undefined>
|
||||
}) {
|
||||
const content = useMemo(() => {
|
||||
if (children) {
|
||||
return children
|
||||
}
|
||||
|
||||
if (!errors?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const uniqueErrors = [
|
||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||
]
|
||||
|
||||
if (uniqueErrors?.length == 1) {
|
||||
return uniqueErrors[0]?.message
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{uniqueErrors.map(
|
||||
(error, index) =>
|
||||
error?.message && <li key={index}>{error.message}</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}, [children, errors])
|
||||
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn("text-sm font-normal text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldContent,
|
||||
FieldTitle,
|
||||
}
|
||||
190
src/components/ui/glowing-effect.tsx
Normal file
190
src/components/ui/glowing-effect.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useEffect, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { animate } from "motion/react";
|
||||
|
||||
interface GlowingEffectProps {
|
||||
blur?: number;
|
||||
inactiveZone?: number;
|
||||
proximity?: number;
|
||||
spread?: number;
|
||||
variant?: "default" | "white";
|
||||
glow?: boolean;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
movementDuration?: number;
|
||||
borderWidth?: number;
|
||||
}
|
||||
const GlowingEffect = memo(
|
||||
({
|
||||
blur = 0,
|
||||
inactiveZone = 0.7,
|
||||
proximity = 0,
|
||||
spread = 20,
|
||||
variant = "default",
|
||||
glow = false,
|
||||
className,
|
||||
movementDuration = 2,
|
||||
borderWidth = 1,
|
||||
disabled = true,
|
||||
}: GlowingEffectProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const lastPosition = useRef({ x: 0, y: 0 });
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(e?: MouseEvent | { x: number; y: number }) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
const element = containerRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const { left, top, width, height } = element.getBoundingClientRect();
|
||||
const mouseX = e?.x ?? lastPosition.current.x;
|
||||
const mouseY = e?.y ?? lastPosition.current.y;
|
||||
|
||||
if (e) {
|
||||
lastPosition.current = { x: mouseX, y: mouseY };
|
||||
}
|
||||
|
||||
const center = [left + width * 0.5, top + height * 0.5];
|
||||
const distanceFromCenter = Math.hypot(
|
||||
mouseX - center[0],
|
||||
mouseY - center[1]
|
||||
);
|
||||
const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone;
|
||||
|
||||
if (distanceFromCenter < inactiveRadius) {
|
||||
element.style.setProperty("--active", "0");
|
||||
return;
|
||||
}
|
||||
|
||||
const isActive =
|
||||
mouseX > left - proximity &&
|
||||
mouseX < left + width + proximity &&
|
||||
mouseY > top - proximity &&
|
||||
mouseY < top + height + proximity;
|
||||
|
||||
element.style.setProperty("--active", isActive ? "1" : "0");
|
||||
|
||||
if (!isActive) return;
|
||||
|
||||
const currentAngle =
|
||||
parseFloat(element.style.getPropertyValue("--start")) || 0;
|
||||
let targetAngle =
|
||||
(180 * Math.atan2(mouseY - center[1], mouseX - center[0])) /
|
||||
Math.PI +
|
||||
90;
|
||||
|
||||
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
|
||||
const newAngle = currentAngle + angleDiff;
|
||||
|
||||
animate(currentAngle, newAngle, {
|
||||
duration: movementDuration,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
onUpdate: (value) => {
|
||||
element.style.setProperty("--start", String(value));
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[inactiveZone, proximity, movementDuration]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) return;
|
||||
|
||||
const handleScroll = () => handleMove();
|
||||
const handlePointerMove = (e: PointerEvent) => handleMove(e);
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
document.body.addEventListener("pointermove", handlePointerMove, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
document.body.removeEventListener("pointermove", handlePointerMove);
|
||||
};
|
||||
}, [handleMove, disabled]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity",
|
||||
glow && "opacity-100",
|
||||
variant === "white" && "border-white",
|
||||
disabled && "!block"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={
|
||||
{
|
||||
"--blur": `${blur}px`,
|
||||
"--spread": spread,
|
||||
"--start": "0",
|
||||
"--active": "0",
|
||||
"--glowingeffect-border-width": `${borderWidth}px`,
|
||||
"--repeating-conic-gradient-times": "5",
|
||||
"--gradient":
|
||||
variant === "white"
|
||||
? `repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
var(--black),
|
||||
var(--black) calc(25% / var(--repeating-conic-gradient-times))
|
||||
)`
|
||||
: `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%),
|
||||
radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),
|
||||
radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%),
|
||||
radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),
|
||||
repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
#dd7bbb 0%,
|
||||
#d79f1e calc(25% / var(--repeating-conic-gradient-times)),
|
||||
#5a922c calc(50% / var(--repeating-conic-gradient-times)),
|
||||
#4c7894 calc(75% / var(--repeating-conic-gradient-times)),
|
||||
#dd7bbb calc(100% / var(--repeating-conic-gradient-times))
|
||||
)`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity",
|
||||
glow && "opacity-100",
|
||||
blur > 0 && "blur-[var(--blur)] ",
|
||||
className,
|
||||
disabled && "!hidden"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"glow",
|
||||
"rounded-[inherit]",
|
||||
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]',
|
||||
"after:[border:var(--glowingeffect-border-width)_solid_transparent]",
|
||||
"after:[background:var(--gradient)] after:[background-attachment:fixed]",
|
||||
"after:opacity-[var(--active)] after:transition-opacity after:duration-300",
|
||||
"after:[mask-clip:padding-box,border-box]",
|
||||
"after:[mask-composite:intersect]",
|
||||
"after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GlowingEffect.displayName = "GlowingEffect";
|
||||
|
||||
export { GlowingEffect };
|
||||
19
src/components/ui/input.tsx
Normal file
19
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
22
src/components/ui/label.tsx
Normal file
22
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import { Label as LabelPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
26
src/components/ui/separator.tsx
Normal file
26
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
90
src/components/ui/tabs.tsx
Normal file
90
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-horizontal:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 text-sm outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
Reference in New Issue
Block a user