Design a basic website for the therapy

This commit is contained in:
abubakarim78 2025-11-06 12:02:10 +00:00
parent b7dfd11d4f
commit c1233f2643
24 changed files with 1716 additions and 102 deletions

View File

@ -1,26 +1,93 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: #ffffff; --foreground-rgb: 0, 0, 0;
--foreground: #171717; --background-start-rgb: 214, 219, 220;
} --background-end-rgb: 255, 255, 255;
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background: #0a0a0a; --foreground-rgb: 255, 255, 255;
--foreground: #ededed; --background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
} }
} }
body { @layer base {
background: var(--background); :root {
color: var(--foreground); --background: 0 0% 100%;
font-family: Arial, Helvetica, sans-serif; --foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
border-color: hsl(var(--border));
}
body {
background-color: #ffffff;
color: hsl(var(--foreground));
}
html.dark body {
background-color: #1a1e26;
}
html {
scroll-behavior: smooth;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
} }

View File

@ -1,33 +1,28 @@
import type { Metadata } from "next"; import './globals.css';
import { Geist, Geist_Mono } from "next/font/google"; import type { Metadata } from 'next';
import "./globals.css"; import { Inter } from 'next/font/google';
import { Providers } from './providers';
import { Toaster } from '@/components/ui/toaster';
const geistSans = Geist({ const inter = Inter({ subsets: ['latin'] });
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: 'Attune Heart Therapy | Nathalie Mac Guffie, LCSW | Miami, FL',
description: "Generated by create next app", description: 'Compassionate, evidence-based therapy in Miami, FL. Licensed Clinical Social Worker offering anxiety, depression, trauma therapy and more.',
}; };
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: {
children: React.ReactNode; children: React.ReactNode;
}>) { }) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<body <body className={inter.className}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`} <Providers>
>
{children} {children}
<Toaster />
</Providers>
</body> </body>
</html> </html>
); );

View File

@ -1,65 +1,22 @@
import Image from "next/image"; import Section from "../components/Section";
import { Footer } from "../components/Footer";
import { HeroSection } from "@/components/Hero";
import { About } from "@/components/About";
import { Services } from "@/components/Services";
import { ContactSection } from "@/components/ContactSection";
export default function Home() { export default function Home() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <main>
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <HeroSection />
<Image
className="dark:invert" <About />
src="/next.svg"
alt="Next.js logo" <Services />
width={100}
height={20} <ContactSection />
priority
/> <Footer />
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main> </main>
</div>
); );
} }

15
app/providers.tsx Normal file
View File

@ -0,0 +1,15 @@
"use client";
import { ThemeProvider } from "../components/ThemeProvider";
import { Navbar } from "../components/Navbar";
import { type ReactNode } from "react";
export function Providers({ children }: { children: ReactNode }) {
return (
<ThemeProvider>
<Navbar />
{children}
</ThemeProvider>
);
}

22
components.json Normal file
View File

@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

201
components/About.tsx Normal file
View File

@ -0,0 +1,201 @@
'use client';
import { motion } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef, useEffect, useState } from "react";
import { Award, Heart, Users } from "lucide-react";
export function About() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const checkTheme = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
const credentials = [
{
icon: Award,
title: "Licensed Mental Health Counselor (LMHC)",
description: "Florida licensed with 30 years of experience",
},
{
icon: Heart,
title: "Trauma-Focused Specialist",
description: "Certified in TF-CBT for trauma recovery",
},
{
icon: Users,
title: "Infant Mental Health & Play Therapy",
description: "Registered Play Therapist (RPT-S) and IMH Endorsement",
},
];
return (
<section
id="about"
ref={ref}
className="relative py-20 px-4 overflow-hidden"
>
<div
className="absolute inset-0 z-0"
style={{
backgroundColor: isDark ? '#1a1e26' : '#ffffff'
}}
/>
{!isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-rose-50/50 via-pink-50/50 to-orange-50/50" />
)}
{isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-gray-900/80 via-gray-800/80 to-gray-900/80" />
)}
<div className="absolute inset-0 overflow-hidden z-[2]">
<motion.div
className="absolute top-10 left-10 w-64 h-64 bg-cyan-100 dark:bg-cyan-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-40 dark:opacity-60"
animate={{
x: [0, 80, 0],
y: [0, 40, 0],
scale: [1, 1.1, 1],
}}
transition={{
duration: 18,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
<motion.div
className="absolute bottom-20 right-10 w-80 h-80 bg-emerald-100 dark:bg-emerald-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-40 dark:opacity-60"
animate={{
x: [0, -80, 0],
y: [0, 60, 0],
scale: [1, 1.15, 1],
}}
transition={{
duration: 22,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</div>
<div className="container max-w-6xl mx-auto relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<motion.h2
className="text-4xl md:text-5xl font-bold mb-6 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }}
>
Meet Nathalie Mac-Guffie
</motion.h2>
<motion.p
className="text-xl text-muted-foreground max-w-3xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.4 }}
>
A dedicated mental health professional specializing in helping children
under 10 and their families navigate trauma, emotional challenges, and
developmental needs.
</motion.p>
</motion.div>
<div className="grid md:grid-cols-2 gap-12 items-center mb-16">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }}
>
<div className="bg-gradient-to-br from-rose-100/30 via-pink-100/30 to-orange-100/30 dark:from-rose-900/20 dark:via-pink-900/20 dark:to-orange-900/20 rounded-3xl p-8 border border-border/50 backdrop-blur-sm">
<motion.h3
className="text-2xl font-semibold mb-4 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.3 }}
>
My Approach
</motion.h3>
<p className="text-muted-foreground mb-4 leading-relaxed">
I provide person-centered guidance, following your child's lead while
drawing out their strengths and incorporating effective coping skills.
My interventions are relationship-based, creating a warm, non-judgmental
space for growth and healing.
</p>
<p className="text-muted-foreground leading-relaxed">
Together, we'll set realistic, measurable, and achievable goals with
clear objectives tailored to your family's unique needs.
</p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.4 }}
className="space-y-6"
>
{credentials.map((cred, index) => {
const Icon = cred.icon;
return (
<motion.div
key={cred.title}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.6 + index * 0.1 }}
className="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 hover:border-rose-500/50 hover:scale-105 transition-all cursor-pointer"
>
<div className="flex items-start gap-4">
<motion.div
className="bg-gradient-to-br from-rose-500/20 via-pink-500/20 to-orange-500/20 dark:from-rose-500/30 dark:via-pink-500/30 dark:to-orange-500/30 p-3 rounded-xl"
whileHover={{ scale: 1.1, rotate: 5 }}
transition={{ duration: 0.2 }}
>
<Icon className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</motion.div>
<div>
<motion.h4
className="font-semibold mb-2 text-foreground"
initial={{ opacity: 0, x: -10 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5, delay: 0.7 + index * 0.1 }}
>
{cred.title}
</motion.h4>
<motion.p
className="text-sm text-muted-foreground"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.5, delay: 0.8 + index * 0.1 }}
>
{cred.description}
</motion.p>
</div>
</div>
</motion.div>
);
})}
</motion.div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,181 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { Send } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent } from "@/components/ui/card";
import { toast } from "sonner";
export function ContactSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const [isDark, setIsDark] = useState(false);
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
message: "",
});
// Sync with global theme class like Navbar/Hero/About
useEffect(() => {
const sync = () => setIsDark(document.documentElement.classList.contains("dark"));
sync();
const observer = new MutationObserver(sync);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
toast("Message Received", {
description: "Thank you for reaching out. We'll get back to you soon!",
});
setFormData({ name: "", email: "", phone: "", message: "" });
};
return (
<section id="contact" className="relative overflow-hidden py-20" ref={ref}>
{/* Theme sync like Hero/About/Navbar */}
<div className="absolute inset-0 z-0" style={{ backgroundColor: isDark ? "#1a1e26" : "#ffffff" }} />
{!isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-rose-50/40 via-pink-50/40 to-orange-50/40" />
)}
{isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-gray-900/70 via-gray-800/70 to-gray-900/70" />
)}
{/* Subtle animated blobs */}
<div className="absolute inset-0 z-[2] overflow-hidden">
<motion.div
className={`absolute -top-10 left-10 h-64 w-64 rounded-full blur-3xl ${isDark ? "bg-pink-900/30 opacity-60" : "bg-pink-100 opacity-50"}`}
animate={{ x: [0, 60, 0], y: [0, 40, 0], scale: [1, 1.05, 1] }}
transition={{ duration: 18, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.div
className={`absolute -bottom-10 right-10 h-72 w-72 rounded-full blur-3xl ${isDark ? "bg-orange-900/30 opacity-60" : "bg-orange-100 opacity-50"}`}
animate={{ x: [0, -60, 0], y: [0, -40, 0], scale: [1, 1.08, 1] }}
transition={{ duration: 22, repeat: Infinity, ease: "easeInOut" }}
/>
</div>
<div className="container mx-auto px-4 relative z-10 text-foreground">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="mb-16 text-center"
>
<h2 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent">
Get in Touch
</h2>
<div className="mx-auto mb-6 h-1 w-24 rounded-full bg-gradient-to-r from-rose-500 to-pink-600" />
<p className="mx-auto max-w-2xl text-lg text-muted-foreground">
Ready to start your journey? Reach out to schedule a consultation.
</p>
</motion.div>
<div className="grid gap-12 lg:grid-cols-2">
{/* Left: Illustration replacing cards */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="relative"
>
<Card className="bg-card/60 backdrop-blur-sm border border-border/50 overflow-hidden">
<CardContent className="p-0">
{/* Use a high-quality, license-friendly illustration */}
<div className="relative">
<img
src="/3786819.jpg"
alt="Contact illustration"
className="mx-auto w-full max-w-xl p-6 select-none rounded-xl object-cover"
loading="lazy"
/>
<motion.div
className="pointer-events-none absolute inset-0"
animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 3, repeat: Infinity }}
style={{ background: "radial-gradient(600px circle at 20% 10%, rgba(244,114,182,0.15), transparent 40%), radial-gradient(600px circle at 80% 80%, rgba(251,146,60,0.12), transparent 40%)" }}
/>
</div>
<div className="px-6 pb-6">
<h3 className="text-2xl font-bold mb-2 text-foreground">Let's Begin Your Healing Journey</h3>
<p className="text-muted-foreground leading-relaxed">
Taking the first step toward therapy can feel daunting, but you're not alone. I'm here to support
you through every stage of your journey toward wellness and growth.
</p>
</div>
</CardContent>
</Card>
</motion.div>
{/* Right: Contact form */}
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 }}
>
<Card className="border border-border/50 bg-card/70 backdrop-blur-sm">
<CardContent className="p-6 md:p-8">
<h3 className="mb-6 text-2xl font-bold text-foreground">Send a Message</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Input
placeholder="Your Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
className="bg-card text-foreground dark:text-zinc-100 placeholder:text-muted-foreground dark:placeholder:text-zinc-400 border-border focus-visible:ring-rose-500"
/>
</div>
<div>
<Input
type="email"
placeholder="Email Address"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
className="bg-card text-foreground dark:text-zinc-100 placeholder:text-muted-foreground dark:placeholder:text-zinc-400 border-border focus-visible:ring-rose-500"
/>
</div>
<div>
<Input
type="tel"
placeholder="Phone Number"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="bg-card text-foreground dark:text-zinc-100 placeholder:text-muted-foreground dark:placeholder:text-zinc-400 border-border focus-visible:ring-rose-500"
/>
</div>
<div>
<Textarea
placeholder="Tell me a bit about what brings you to therapy..."
rows={6}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
required
className="bg-card text-foreground dark:text-zinc-100 placeholder:text-muted-foreground dark:placeholder:text-zinc-400 border-border focus-visible:ring-rose-500"
/>
</div>
<Button
type="submit"
className="w-full cursor-pointer bg-gradient-to-r from-rose-500 to-pink-600 text-white transition-all hover:from-rose-600 hover:to-pink-700 hover:scale-[1.02]"
size="lg"
>
<Send className="mr-2 h-5 w-5" />
Send Message
</Button>
</form>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</section>
);
}

181
components/Footer.tsx Normal file
View File

@ -0,0 +1,181 @@
'use client';
import { motion } from "framer-motion";
import { Heart, Mail, Phone, MapPin } from "lucide-react";
import { useEffect, useState } from "react";
export function Footer() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const checkTheme = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
const scrollToSection = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
};
const quickLinks = [
{ name: 'Home', href: '#home' },
{ name: 'About', href: '#about' },
{ name: 'Services', href: '#services' },
{ name: 'Contact', href: '#contact' },
];
return (
<footer
className="relative py-12 overflow-hidden border-t border-border/50"
style={{
backgroundColor: isDark ? '#1a1e26' : '#ffffff'
}}
>
{!isDark && (
<div className="absolute inset-0 bg-gradient-to-br from-rose-50/30 via-pink-50/30 to-orange-50/30" />
)}
{isDark && (
<div className="absolute inset-0 bg-gradient-to-br from-gray-900/40 via-gray-800/40 to-gray-900/40" />
)}
<div className="container mx-auto px-4 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8">
{/* Brand Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="lg:col-span-1"
>
<div className="flex items-center gap-2 mb-4">
<div className="bg-gradient-to-br from-rose-500 to-pink-600 p-2 rounded-xl">
<Heart className="h-5 w-5 text-white fill-white" />
</div>
<span className="font-bold text-lg bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent">
Attune Heart Therapy
</span>
</div>
<p className="text-sm text-muted-foreground mb-2">
Nathalie Mac-Guffie, LMHC, RPT-S, IMH-E, TF-CBT
</p>
<p className="text-sm text-muted-foreground">
Licensed Mental Health Counselor Florida
</p>
</motion.div>
{/* Quick Links */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
className="lg:col-span-1"
>
<h3 className="font-semibold mb-4 text-foreground">Quick Links</h3>
<ul className="space-y-2">
{quickLinks.map((link) => (
<li key={link.name}>
<button
onClick={() => scrollToSection(link.href.replace('#', ''))}
className="text-sm text-muted-foreground hover:text-rose-600 dark:hover:text-rose-400 transition-colors cursor-pointer hover:translate-x-1 inline-block transition-transform"
>
{link.name}
</button>
</li>
))}
</ul>
</motion.div>
{/* Contact Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="lg:col-span-1"
>
<h3 className="font-semibold mb-4 text-foreground">Contact</h3>
<ul className="space-y-3">
<li className="flex items-start gap-3">
<Phone className="h-4 w-4 mt-1 text-rose-600 dark:text-rose-400 flex-shrink-0" />
<a
href="tel:+19548073027"
className="text-sm text-muted-foreground hover:text-rose-600 dark:hover:text-rose-400 transition-colors"
>
(954) 807-3027
</a>
</li>
<li className="flex items-start gap-3">
<Mail className="h-4 w-4 mt-1 text-rose-600 dark:text-rose-400 flex-shrink-0" />
<a
href="mailto:info@attunehearttherapy.com"
className="text-sm text-muted-foreground hover:text-rose-600 dark:hover:text-rose-400 transition-colors"
>
info@attunehearttherapy.com
</a>
</li>
<li className="flex items-start gap-3">
<MapPin className="h-4 w-4 mt-1 text-rose-600 dark:text-rose-400 flex-shrink-0" />
<span className="text-sm text-muted-foreground">
Miami, Florida
</span>
</li>
</ul>
</motion.div>
{/* Services Summary */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
className="lg:col-span-1"
>
<h3 className="font-semibold mb-4 text-foreground">Services</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>Trauma-Focused Therapy</li>
<li>Play Therapy</li>
<li>Infant Mental Health</li>
<li>Dyadic Therapy</li>
<li>Social-Emotional Support</li>
</ul>
</motion.div>
</div>
{/* Bottom Section */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
className="mt-8 pt-8 border-t border-border/50"
>
<div className="grid grid-cols-1 items-center gap-3 text-center md:grid-cols-3 md:text-left">
<p className="text-sm text-muted-foreground md:justify-self-start">
© {new Date().getFullYear()} Attune Heart Therapy. All rights reserved.
</p>
<p className="text-xs text-muted-foreground md:justify-self-center">
This site is for informational purposes only and does not constitute medical advice.
</p>
<p className="text-sm text-muted-foreground md:justify-self-end md:text-right">
Providing compassionate, trauma-informed care for children and families.
</p>
</div>
</motion.div>
</div>
</footer>
);
}

163
components/Hero.tsx Normal file
View File

@ -0,0 +1,163 @@
'use client';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { ArrowRight, Calendar } from 'lucide-react';
import { useEffect, useState } from 'react';
export function HeroSection() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const checkTheme = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
return (
<section
id="home"
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20"
>
<div
className="absolute inset-0 z-0"
style={{
backgroundColor: isDark ? '#1a1e26' : '#ffffff'
}}
/>
{!isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-rose-50/50 via-pink-50/50 to-orange-50/50" />
)}
{isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-gray-900/80 via-gray-800/80 to-gray-900/80" />
)}
<div className="absolute inset-0 overflow-hidden z-[2]">
<motion.div
className="absolute top-20 left-10 w-72 h-72 bg-rose-100 dark:bg-rose-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-50 dark:opacity-70"
animate={{
x: [0, 100, 0],
y: [0, 50, 0],
scale: [1, 1.1, 1],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
<motion.div
className="absolute top-40 right-10 w-96 h-96 bg-pink-100 dark:bg-pink-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-50 dark:opacity-70"
animate={{
x: [0, -100, 0],
y: [0, 100, 0],
scale: [1, 1.2, 1],
}}
transition={{
duration: 25,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
<motion.div
className="absolute bottom-20 left-1/3 w-80 h-80 bg-orange-100 dark:bg-orange-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-50 dark:opacity-70"
animate={{
x: [0, 50, 0],
y: [0, -50, 0],
scale: [1, 1.15, 1],
}}
transition={{
duration: 22,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</div>
<div className="container mx-auto px-4 relative z-10">
<div className="max-w-4xl mx-auto text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.h1
className="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
Welcome to Attune Heart Therapy
</motion.h1>
<motion.p
className="text-xl md:text-2xl text-muted-foreground mb-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
>
Nathalie Mac Guffie, LCSW
</motion.p>
<motion.p
className="text-lg md:text-xl text-muted-foreground mb-8 max-w-2xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
>
Compassionate, evidence-based therapy to help you heal, grow, and
thrive. Creating a safe space for your journey toward emotional
wellness.
</motion.p>
<motion.div
className="flex flex-col sm:flex-row gap-4 justify-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
>
<Button
size="lg"
className="bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white group cursor-pointer hover:scale-105 hover:shadow-lg transition-all"
>
<Calendar className="mr-2 h-5 w-5" />
Book Appointment
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Button>
<Button
size="lg"
variant="outline"
className="cursor-pointer hover:scale-105 hover:bg-gray-100 dark:hover:bg-gray-800 hover:border-cyan-300 dark:hover:border-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-all"
>
Learn More
</Button>
</motion.div>
</motion.div>
</div>
</div>
<motion.div
className="absolute bottom-10 left-1/2 -translate-x-1/2"
animate={{ y: [0, 10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
>
<div className="w-6 h-10 border-2 border-cyan-500/50 dark:border-cyan-400/50 rounded-full flex items-start justify-center p-2">
<motion.div
className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full"
animate={{ y: [0, 12, 0] }}
transition={{ duration: 2, repeat: Infinity }}
/>
</div>
</motion.div>
</section>
);
}

94
components/Navbar.tsx Normal file
View File

@ -0,0 +1,94 @@
'use client';
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Heart } from "lucide-react";
import { ThemeToggle } from "@/components/ThemeToggle";
import { useEffect, useState } from "react";
export function Navbar() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const checkTheme = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
const scrollToSection = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
};
return (
<motion.nav
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5 }}
className="fixed top-0 left-0 right-0 z-50 border-b border-border/50"
style={{
backgroundColor: isDark ? '#1a1e26' : '#ffffff'
}}
>
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<motion.a
href="#home"
className="flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="bg-gradient-to-br from-rose-500 to-pink-600 p-2 rounded-xl">
<Heart className="h-5 w-5 text-white fill-white" />
</div>
<span className="font-bold text-lg bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent">
Attune Heart Therapy
</span>
</motion.a>
<div className="hidden md:flex items-center gap-6">
<button
onClick={() => scrollToSection("about")}
className="text-sm font-medium hover:text-primary transition-colors cursor-pointer px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-cyan-900/30"
>
About
</button>
<button
onClick={() => scrollToSection("services")}
className="text-sm font-medium hover:text-primary transition-colors cursor-pointer px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-cyan-900/30"
>
Services
</button>
<button
onClick={() => scrollToSection("contact")}
className="text-sm font-medium hover:text-primary transition-colors cursor-pointer px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-cyan-900/30"
>
Contact
</button>
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" className="hidden sm:inline-flex hover:opacity-90 hover:scale-105 transition-all dark:hover:bg-cyan-900/30" asChild>
<a href="#login">Sign In</a>
</Button>
<ThemeToggle />
<Button size="sm" className="hidden sm:inline-flex hover:opacity-90 hover:scale-105 transition-all dark:hover:bg-emerald-600" asChild>
<a href="tel:+19548073027">Book Now</a>
</Button>
</div>
</div>
</div>
</motion.nav>
);
}

30
components/Section.tsx Normal file
View File

@ -0,0 +1,30 @@
"use client";
import { type ReactNode } from "react";
type Props = {
id?: string;
title: string;
children: ReactNode;
};
export default function Section({ id, title, children }: Props) {
return (
<section id={id} className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<h2 className="text-2xl font-semibold tracking-tight text-zinc-900 opacity-0 animate-[fadein_500ms_ease-out_forwards] dark:text-zinc-100 sm:text-3xl">
{title}
</h2>
<div className="mt-5 text-zinc-700 opacity-0 animate-[fadein_500ms_ease-out_forwards] [animation-delay:60ms] dark:text-zinc-300">
{children}
</div>
<style jsx>{`
@keyframes fadein {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</section>
);
}

214
components/Services.tsx Normal file
View File

@ -0,0 +1,214 @@
'use client';
import { motion } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef, useEffect, useState } from "react";
import { Baby, Brain, HeartHandshake, Sparkles, Users2, Shield } from "lucide-react";
export function Services() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const checkTheme = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
const services = [
{
icon: Brain,
title: "Trauma-Focused Therapy",
description: "Evidence-based TF-CBT to help children process and heal from traumatic experiences in a safe, supportive environment.",
},
{
icon: Sparkles,
title: "Play Therapy",
description: "Child-centered play therapy allowing children to express themselves naturally and build emotional regulation skills.",
},
{
icon: Baby,
title: "Infant Mental Health",
description: "Specialized support for infants and toddlers, focusing on early attachment, developmental milestones, and caregiver relationships.",
},
{
icon: Users2,
title: "Dyadic Therapy",
description: "Strengthening parent-child relationships through interactive sessions that enhance communication and connection.",
},
{
icon: HeartHandshake,
title: "Social-Emotional Support",
description: "Building emotional literacy and self-regulation skills to help children navigate relationships and challenges.",
},
{
icon: Shield,
title: "Relationship-Based Care",
description: "Fostering healing through nurturing therapeutic relationships and caregiver collaboration.",
},
];
return (
<section
id="services"
ref={ref}
className="relative py-20 px-4 overflow-hidden"
>
<div
className="absolute inset-0 z-0"
style={{
backgroundColor: isDark ? '#1a1e26' : '#ffffff'
}}
/>
{!isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-rose-50/50 via-pink-50/50 to-orange-50/50" />
)}
{isDark && (
<div className="absolute inset-0 z-[1] bg-gradient-to-br from-gray-900/80 via-gray-800/80 to-gray-900/80" />
)}
<div className="absolute inset-0 overflow-hidden z-[2]">
<motion.div
className="absolute top-20 right-20 w-72 h-72 bg-pink-100 dark:bg-pink-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-40 dark:opacity-60"
animate={{
x: [0, -90, 0],
y: [0, 50, 0],
scale: [1, 1.2, 1],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
<motion.div
className="absolute bottom-10 left-20 w-96 h-96 bg-orange-100 dark:bg-orange-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-40 dark:opacity-60"
animate={{
x: [0, 70, 0],
y: [0, -60, 0],
scale: [1, 1.15, 1],
}}
transition={{
duration: 24,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</div>
<div className="container max-w-6xl mx-auto relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<motion.h2
className="text-4xl md:text-5xl font-bold mb-6 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }}
>
Specialized Services
</motion.h2>
<motion.p
className="text-xl text-muted-foreground max-w-3xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.4 }}
>
Comprehensive, evidence-based therapeutic support for children and families
</motion.p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{services.map((service, index) => {
const Icon = service.icon;
return (
<motion.div
key={service.title}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="group bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 hover:border-rose-500/50 hover:shadow-lg hover:shadow-rose-500/10 hover:scale-105 transition-all duration-300 cursor-pointer"
>
<motion.div
className="bg-gradient-to-br from-rose-500/20 via-pink-500/20 to-orange-500/20 dark:from-rose-500/30 dark:via-pink-500/30 dark:to-orange-500/30 w-14 h-14 rounded-xl flex items-center justify-center mb-4"
whileHover={{ scale: 1.1, rotate: 5 }}
transition={{ duration: 0.2 }}
>
<Icon className="h-7 w-7 text-rose-600 dark:text-rose-400" />
</motion.div>
<motion.h3
className="text-xl font-semibold mb-3 text-foreground"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.5, delay: 0.3 + index * 0.1 }}
>
{service.title}
</motion.h3>
<motion.p
className="text-muted-foreground leading-relaxed"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.5, delay: 0.4 + index * 0.1 }}
>
{service.description}
</motion.p>
</motion.div>
);
})}
</div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.6 }}
className="mt-16 bg-gradient-to-br from-rose-100/30 via-pink-100/30 to-orange-100/30 dark:from-rose-900/20 dark:via-pink-900/20 dark:to-orange-900/20 rounded-3xl p-8 border border-border/50 backdrop-blur-sm"
>
<motion.h3
className="text-2xl font-semibold mb-4 text-center bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.7 }}
>
Who I Work With
</motion.h3>
<div className="max-w-3xl mx-auto text-center">
<motion.p
className="text-muted-foreground leading-relaxed mb-4"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.8 }}
>
I specialize in working with <strong className="text-foreground">children under the age of 10</strong> who are
dealing with trauma, stressors, or social-emotional challenges and need understanding
and support.
</motion.p>
<motion.p
className="text-muted-foreground leading-relaxed"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.9 }}
>
The goal is to build a healthy foundation through nurturing relationships and
emotional literacy, helping children diminish distress and enhance self-regulation.
</motion.p>
</div>
</motion.div>
</div>
</section>
);
}

View File

@ -0,0 +1,57 @@
"use client";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: (t: Theme) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function useAppTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useAppTheme must be used within ThemeProvider");
return ctx;
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>("light");
const setTheme = (t: Theme) => setThemeState(t);
// Initialize from localStorage or system preference on mount
useEffect(() => {
const stored = typeof window !== "undefined" ? localStorage.getItem("theme") : null;
if (stored === "light" || stored === "dark") {
setThemeState(stored);
return;
}
const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
setThemeState(prefersDark ? "dark" : "light");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Apply class to <html> and persist
useEffect(() => {
const root = document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
localStorage.setItem("theme", theme);
}, [theme]);
const value = useMemo<ThemeContextValue>(() => ({
theme,
setTheme,
toggleTheme: () => setThemeState((t) => (t === "dark" ? "light" : "dark")),
}), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

View File

@ -0,0 +1,36 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const initialTheme = savedTheme || (prefersDark ? "dark" : "light");
setTheme(initialTheme);
document.documentElement.classList.toggle("dark", initialTheme === "dark");
}, []);
const toggleTheme = () => {
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
document.documentElement.classList.toggle("dark", newTheme === "dark");
};
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="relative rounded-full cursor-pointer hover:bg-gray-100 dark:hover:bg-cyan-900/30 transition-colors"
aria-label="Toggle theme"
>
<Sun className={`h-5 w-5 transition-all absolute ${theme === "light" ? "rotate-0 scale-100" : "rotate-90 scale-0"}`} />
<Moon className={`h-5 w-5 transition-all absolute ${theme === "dark" ? "rotate-0 scale-100" : "rotate-90 scale-0"}`} />
</Button>
);
}

60
components/ui/button.tsx Normal file
View File

@ -0,0 +1,60 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

92
components/ui/card.tsx Normal file
View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

21
components/ui/input.tsx Normal file
View File

@ -0,0 +1,21 @@
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(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

40
components/ui/sonner.tsx Normal file
View File

@ -0,0 +1,40 @@
"use client"
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@ -0,0 +1,8 @@
"use client";
// Simple toaster component - can be enhanced later with toast notifications
export function Toaster() {
return null;
}

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -12,18 +12,27 @@
"node": ">=20.9.0" "node": ">=20.9.0"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"lucide-react": "^0.552.0",
"next": "16.0.1",
"next-themes": "^0.4.6",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"next": "16.0.1" "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.1" "eslint-config-next": "16.0.1",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
} }
} }

View File

@ -8,15 +8,39 @@ importers:
.: .:
dependencies: dependencies:
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.2)(react@19.2.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
framer-motion:
specifier: ^12.23.24
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
lucide-react:
specifier: ^0.552.0
version: 0.552.0(react@19.2.0)
next: next:
specifier: 16.0.1 specifier: 16.0.1
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: react:
specifier: 19.2.0 specifier: 19.2.0
version: 19.2.0 version: 19.2.0
react-dom: react-dom:
specifier: 19.2.0 specifier: 19.2.0
version: 19.2.0(react@19.2.0) version: 19.2.0(react@19.2.0)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
devDependencies: devDependencies:
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4 specifier: ^4
@ -39,6 +63,9 @@ importers:
tailwindcss: tailwindcss:
specifier: ^4 specifier: ^4
version: 4.1.16 version: 4.1.16
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
typescript: typescript:
specifier: ^5 specifier: ^5
version: 5.9.3 version: 5.9.3
@ -394,6 +421,24 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'} engines: {node: '>=12.4.0'}
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.4':
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@rtsao/scc@1.1.0': '@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@ -785,9 +830,16 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
client-only@0.0.1: client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -1081,6 +1133,20 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
framer-motion@12.23.24:
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@ -1446,6 +1512,11 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.552.0:
resolution: {integrity: sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -1471,6 +1542,12 @@ packages:
minimist@1.2.8: minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
motion-dom@12.23.23:
resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -1487,6 +1564,12 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@16.0.1: next@16.0.1:
resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==} resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==}
engines: {node: '>=20.9.0'} engines: {node: '>=20.9.0'}
@ -1718,6 +1801,12 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1781,6 +1870,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
tailwindcss@4.1.16: tailwindcss@4.1.16:
resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
@ -1808,6 +1900,9 @@ packages:
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
type-check@0.4.0: type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -2236,6 +2331,19 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {} '@nolyfill/is-core-module@1.0.39': {}
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-slot@1.2.4(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.2
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
@ -2637,8 +2745,14 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
client-only@0.0.1: {} client-only@0.0.1: {}
clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@ -3082,6 +3196,15 @@ snapshots:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
motion-dom: 12.23.23
motion-utils: 12.23.6
tslib: 2.8.1
optionalDependencies:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
function-bind@1.1.2: {} function-bind@1.1.2: {}
function.prototype.name@1.1.8: function.prototype.name@1.1.8:
@ -3427,6 +3550,10 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lucide-react@0.552.0(react@19.2.0):
dependencies:
react: 19.2.0
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@ -3450,6 +3577,12 @@ snapshots:
minimist@1.2.8: {} minimist@1.2.8: {}
motion-dom@12.23.23:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
ms@2.1.3: {} ms@2.1.3: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
@ -3458,6 +3591,11 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
'@next/env': 16.0.1 '@next/env': 16.0.1
@ -3752,6 +3890,11 @@ snapshots:
side-channel-map: 1.0.1 side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2 side-channel-weakmap: 1.0.2
sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
stable-hash@0.0.5: {} stable-hash@0.0.5: {}
@ -3828,6 +3971,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
tailwind-merge@3.3.1: {}
tailwindcss@4.1.16: {} tailwindcss@4.1.16: {}
tapable@2.3.0: {} tapable@2.3.0: {}
@ -3854,6 +3999,8 @@ snapshots:
tslib@2.8.1: {} tslib@2.8.1: {}
tw-animate-css@1.4.0: {}
type-check@0.4.0: type-check@0.4.0:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1

BIN
public/3786819.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB