diff --git a/app/globals.css b/app/globals.css
index a2dc41e..82c4b36 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,26 +1,93 @@
@import "tailwindcss";
:root {
- --background: #ffffff;
- --foreground: #171717;
-}
-
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
- --background: #0a0a0a;
- --foreground: #ededed;
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
}
}
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --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;
+ }
}
diff --git a/app/layout.tsx b/app/layout.tsx
index f7fa87e..5b3bb9b 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,33 +1,28 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import "./globals.css";
+import './globals.css';
+import type { Metadata } from 'next';
+import { Inter } from 'next/font/google';
+import { Providers } from './providers';
+import { Toaster } from '@/components/ui/toaster';
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
+const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: 'Attune Heart Therapy | Nathalie Mac Guffie, LCSW | Miami, FL',
+ description: 'Compassionate, evidence-based therapy in Miami, FL. Licensed Clinical Social Worker offering anxiety, depression, trauma therapy and more.',
};
export default function RootLayout({
children,
-}: Readonly<{
+}: {
children: React.ReactNode;
-}>) {
+}) {
return (
-
-
- {children}
+
+
+
+ {children}
+
+
);
diff --git a/app/page.tsx b/app/page.tsx
index 295f8fd..fced93b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -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() {
return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/app/providers.tsx b/app/providers.tsx
new file mode 100644
index 0000000..e759ace
--- /dev/null
+++ b/app/providers.tsx
@@ -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 (
+
+
+ {children}
+
+ );
+}
+
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..b7b9791
--- /dev/null
+++ b/components.json
@@ -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": {}
+}
diff --git a/components/About.tsx b/components/About.tsx
new file mode 100644
index 0000000..dbc69fe
--- /dev/null
+++ b/components/About.tsx
@@ -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 (
+
+
+ {!isDark && (
+
+ )}
+ {isDark && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ Meet Nathalie Mac-Guffie
+
+
+ A dedicated mental health professional specializing in helping children
+ under 10 and their families navigate trauma, emotional challenges, and
+ developmental needs.
+
+
+
+
+
+
+
+ My Approach
+
+
+ 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.
+
+
+ Together, we'll set realistic, measurable, and achievable goals with
+ clear objectives tailored to your family's unique needs.
+
+
+
+
+
+ {credentials.map((cred, index) => {
+ const Icon = cred.icon;
+ return (
+
+
+
+
+
+
+
+ {cred.title}
+
+
+ {cred.description}
+
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
+
diff --git a/components/ContactSection.tsx b/components/ContactSection.tsx
new file mode 100644
index 0000000..088e3d7
--- /dev/null
+++ b/components/ContactSection.tsx
@@ -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 (
+
+ );
+}
diff --git a/components/Footer.tsx b/components/Footer.tsx
new file mode 100644
index 0000000..56ed92b
--- /dev/null
+++ b/components/Footer.tsx
@@ -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 (
+
+ );
+}
diff --git a/components/Hero.tsx b/components/Hero.tsx
new file mode 100644
index 0000000..98b1b08
--- /dev/null
+++ b/components/Hero.tsx
@@ -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 (
+
+
+ {!isDark && (
+
+ )}
+ {isDark && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ Welcome to Attune Heart Therapy
+
+
+
+ Nathalie Mac Guffie, LCSW
+
+
+
+ Compassionate, evidence-based therapy to help you heal, grow, and
+ thrive. Creating a safe space for your journey toward emotional
+ wellness.
+
+
+
+
+
+ Book Appointment
+
+
+
+ Learn More
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/Navbar.tsx b/components/Navbar.tsx
new file mode 100644
index 0000000..9d340a5
--- /dev/null
+++ b/components/Navbar.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+ Attune Heart Therapy
+
+
+
+
+ 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
+
+ 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
+
+ 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
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/Section.tsx b/components/Section.tsx
new file mode 100644
index 0000000..caef6c9
--- /dev/null
+++ b/components/Section.tsx
@@ -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 (
+
+
+ {title}
+
+
+ {children}
+
+
+
+ );
+}
+
+
diff --git a/components/Services.tsx b/components/Services.tsx
new file mode 100644
index 0000000..7aaea79
--- /dev/null
+++ b/components/Services.tsx
@@ -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 (
+
+
+ {!isDark && (
+
+ )}
+ {isDark && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ Specialized Services
+
+
+ Comprehensive, evidence-based therapeutic support for children and families
+
+
+
+
+ {services.map((service, index) => {
+ const Icon = service.icon;
+ return (
+
+
+
+
+
+ {service.title}
+
+
+ {service.description}
+
+
+ );
+ })}
+
+
+
+
+ Who I Work With
+
+
+
+ I specialize in working with children under the age of 10 who are
+ dealing with trauma, stressors, or social-emotional challenges and need understanding
+ and support.
+
+
+ The goal is to build a healthy foundation through nurturing relationships and
+ emotional literacy, helping children diminish distress and enhance self-regulation.
+
+
+
+
+
+ );
+}
+
+
diff --git a/components/ThemeProvider.tsx b/components/ThemeProvider.tsx
new file mode 100644
index 0000000..8545a13
--- /dev/null
+++ b/components/ThemeProvider.tsx
@@ -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(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("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 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(() => ({
+ theme,
+ setTheme,
+ toggleTheme: () => setThemeState((t) => (t === "dark" ? "light" : "dark")),
+ }), [theme]);
+
+ return {children} ;
+}
+
+
diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx
new file mode 100644
index 0000000..883c400
--- /dev/null
+++ b/components/ThemeToggle.tsx
@@ -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 (
+
+
+
+
+ );
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..21409a0
--- /dev/null
+++ b/components/ui/button.tsx
@@ -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 & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..681ad98
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..8916905
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
new file mode 100644
index 0000000..9b20afe
--- /dev/null
+++ b/components/ui/sonner.tsx
@@ -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 (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..7f21b5e
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx
new file mode 100644
index 0000000..1d7fb87
--- /dev/null
+++ b/components/ui/toaster.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+// Simple toaster component - can be enhanced later with toast notifications
+export function Toaster() {
+ return null;
+}
+
+
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/package.json b/package.json
index 08b55f9..121eae8 100644
--- a/package.json
+++ b/package.json
@@ -8,22 +8,31 @@
"start": "next start",
"lint": "eslint"
},
- "engines": {
+ "engines": {
"node": ">=20.9.0"
},
"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-dom": "19.2.0",
- "next": "16.0.1"
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.3.1"
},
"devDependencies": {
- "typescript": "^5",
+ "@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
- "@tailwindcss/postcss": "^4",
- "tailwindcss": "^4",
"eslint": "^9",
- "eslint-config-next": "16.0.1"
+ "eslint-config-next": "16.0.1",
+ "tailwindcss": "^4",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^5"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 64de4bb..2a08c41 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,15 +8,39 @@ importers:
.:
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:
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)
+ next-themes:
+ specifier: ^0.4.6
+ version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react:
specifier: 19.2.0
version: 19.2.0
react-dom:
specifier: 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:
'@tailwindcss/postcss':
specifier: ^4
@@ -39,6 +63,9 @@ importers:
tailwindcss:
specifier: ^4
version: 4.1.16
+ tw-animate-css:
+ specifier: ^1.4.0
+ version: 1.4.0
typescript:
specifier: ^5
version: 5.9.3
@@ -394,6 +421,24 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
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':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@@ -785,9 +830,16 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1081,6 +1133,20 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
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:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -1446,6 +1512,11 @@ packages:
lru-cache@5.1.1:
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:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1471,6 +1542,12 @@ packages:
minimist@1.2.8:
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:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1487,6 +1564,12 @@ packages:
natural-compare@1.4.0:
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:
resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==}
engines: {node: '>=20.9.0'}
@@ -1718,6 +1801,12 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
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:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -1781,6 +1870,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ tailwind-merge@3.3.1:
+ resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
+
tailwindcss@4.1.16:
resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
@@ -1808,6 +1900,9 @@ packages:
tslib@2.8.1:
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:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -2236,6 +2331,19 @@ snapshots:
'@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': {}
'@swc/helpers@0.5.15':
@@ -2637,8 +2745,14 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
client-only@0.0.1: {}
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -3082,6 +3196,15 @@ snapshots:
dependencies:
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.prototype.name@1.1.8:
@@ -3427,6 +3550,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-react@0.552.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3450,6 +3577,12 @@ snapshots:
minimist@1.2.8: {}
+ motion-dom@12.23.23:
+ dependencies:
+ motion-utils: 12.23.6
+
+ motion-utils@12.23.6: {}
+
ms@2.1.3: {}
nanoid@3.3.11: {}
@@ -3458,6 +3591,11 @@ snapshots:
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):
dependencies:
'@next/env': 16.0.1
@@ -3752,6 +3890,11 @@ snapshots:
side-channel-map: 1.0.1
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: {}
stable-hash@0.0.5: {}
@@ -3828,6 +3971,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ tailwind-merge@3.3.1: {}
+
tailwindcss@4.1.16: {}
tapable@2.3.0: {}
@@ -3854,6 +3999,8 @@ snapshots:
tslib@2.8.1: {}
+ tw-animate-css@1.4.0: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
diff --git a/public/3786819.jpg b/public/3786819.jpg
new file mode 100644
index 0000000..cdb4bcc
Binary files /dev/null and b/public/3786819.jpg differ