Enhance application structure by adding new components for About, Services, Contact, and Footer sections. Implement theme management with ThemeProvider and ThemeToggle. Update layout and global styles for improved responsiveness and aesthetics. Integrate framer-motion for animations and add new dependencies for UI enhancements.
This commit is contained in:
commit
458d9993e9
187
app/globals.css
187
app/globals.css
@ -1,125 +1,94 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-poppins);
|
||||
--font-mono: var(--font-poppins);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
@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: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--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 {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
background-color: #ffffff;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
body.menu-open {
|
||||
overflow: hidden;
|
||||
html.dark body {
|
||||
background-color: #1a1e26;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +1,28 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Poppins } 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 poppins = Poppins({
|
||||
variable: "--font-poppins",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
});
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Attune Heart Therapy",
|
||||
description: "Attune Heart Therapy",
|
||||
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 (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${poppins.variable} font-sans antialiased`}
|
||||
>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Providers>
|
||||
{children}
|
||||
<Toaster />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
75
app/page.tsx
75
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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<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">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<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>
|
||||
<HeroSection />
|
||||
|
||||
<About />
|
||||
|
||||
<Services />
|
||||
|
||||
<ContactSection />
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
15
app/providers.tsx
Normal file
15
app/providers.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
201
components/About.tsx
Normal file
201
components/About.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
181
components/ContactSection.tsx
Normal file
181
components/ContactSection.tsx
Normal 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
181
components/Footer.tsx
Normal 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
163
components/Hero.tsx
Normal 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
94
components/Navbar.tsx
Normal 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
30
components/Section.tsx
Normal 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
214
components/Services.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
57
components/ThemeProvider.tsx
Normal file
57
components/ThemeProvider.tsx
Normal 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>;
|
||||
}
|
||||
|
||||
|
||||
36
components/ThemeToggle.tsx
Normal file
36
components/ThemeToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
92
components/ui/card.tsx
Normal file
92
components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
40
components/ui/sonner.tsx
Normal file
40
components/ui/sonner.tsx
Normal 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 }
|
||||
18
components/ui/textarea.tsx
Normal file
18
components/ui/textarea.tsx
Normal 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 }
|
||||
8
components/ui/toaster.tsx
Normal file
8
components/ui/toaster.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
// Simple toaster component - can be enhanced later with toast notifications
|
||||
export function Toaster() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -15,10 +15,13 @@
|
||||
"@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",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -31,6 +34,5 @@
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,18 +17,27 @@ importers:
|
||||
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
|
||||
@ -1124,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==}
|
||||
|
||||
@ -1519,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==}
|
||||
|
||||
@ -1535,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'}
|
||||
@ -1766,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'}
|
||||
@ -3155,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:
|
||||
@ -3527,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: {}
|
||||
@ -3535,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
|
||||
@ -3829,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: {}
|
||||
|
||||
BIN
public/3786819.jpg
Normal file
BIN
public/3786819.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
Loading…
Reference in New Issue
Block a user