feat/authentication #5
189
app/globals.css
189
app/globals.css
@ -1,125 +1,94 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@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 {
|
:root {
|
||||||
--radius: 0.625rem;
|
--foreground-rgb: 0, 0, 0;
|
||||||
--background: oklch(1 0 0);
|
--background-start-rgb: 214, 219, 220;
|
||||||
--foreground: oklch(0.145 0 0);
|
--background-end-rgb: 255, 255, 255;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
@media (prefers-color-scheme: dark) {
|
||||||
--background: oklch(0.145 0 0);
|
:root {
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground-rgb: 255, 255, 255;
|
||||||
--card: oklch(0.205 0 0);
|
--background-start-rgb: 0, 0, 0;
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--background-end-rgb: 0, 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);
|
@layer base {
|
||||||
--secondary: oklch(0.269 0 0);
|
:root {
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--background: 0 0% 100%;
|
||||||
--muted: oklch(0.269 0 0);
|
--foreground: 0 0% 3.9%;
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--card: 0 0% 100%;
|
||||||
--accent: oklch(0.269 0 0);
|
--card-foreground: 0 0% 3.9%;
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--popover: 0 0% 100%;
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--popover-foreground: 0 0% 3.9%;
|
||||||
--border: oklch(1 0 0 / 10%);
|
--primary: 0 0% 9%;
|
||||||
--input: oklch(1 0 0 / 15%);
|
--primary-foreground: 0 0% 98%;
|
||||||
--ring: oklch(0.556 0 0);
|
--secondary: 0 0% 96.1%;
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--secondary-foreground: 0 0% 9%;
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--muted: 0 0% 96.1%;
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--muted-foreground: 0 0% 45.1%;
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--accent: 0 0% 96.1%;
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--accent-foreground: 0 0% 9%;
|
||||||
--sidebar: oklch(0.205 0 0);
|
--destructive: 0 84.2% 60.2%;
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--destructive-foreground: 0 0% 98%;
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--border: 0 0% 89.8%;
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--input: 0 0% 89.8%;
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--ring: 0 0% 3.9%;
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--chart-1: 12 76% 61%;
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--chart-2: 173 58% 39%;
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--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 {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
border-color: hsl(var(--border));
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
background-color: #ffffff;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
body.menu-open {
|
html.dark body {
|
||||||
overflow: hidden;
|
background-color: #1a1e26;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,28 @@
|
|||||||
import type { Metadata } from "next";
|
import './globals.css';
|
||||||
import { Poppins } from "next/font/google";
|
import type { Metadata } from 'next';
|
||||||
import "./globals.css";
|
import { Inter } from 'next/font/google';
|
||||||
|
import { Providers } from './providers';
|
||||||
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
|
||||||
const poppins = Poppins({
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
variable: "--font-poppins",
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["300", "400", "500", "600", "700"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Attune Heart Therapy",
|
title: 'Attune Heart Therapy | Nathalie Mac Guffie, LCSW | Miami, FL',
|
||||||
description: "Attune Heart Therapy",
|
description: 'Compassionate, evidence-based therapy in Miami, FL. Licensed Clinical Social Worker offering anxiety, depression, trauma therapy and more.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body
|
<body className={inter.className}>
|
||||||
className={`${poppins.variable} font-sans antialiased`}
|
<Providers>
|
||||||
>
|
{children}
|
||||||
{children}
|
<Toaster />
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
77
app/page.tsx
77
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() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<main>
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<HeroSection />
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
<About />
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
<Services />
|
||||||
width={100}
|
|
||||||
height={20}
|
<ContactSection />
|
||||||
priority
|
|
||||||
/>
|
<Footer />
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
</main>
|
||||||
<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>
|
|
||||||
</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",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "^0.552.0",
|
||||||
"next": "16.0.1",
|
"next": "16.0.1",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -31,6 +34,5 @@
|
|||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,18 +17,27 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 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:
|
lucide-react:
|
||||||
specifier: ^0.552.0
|
specifier: ^0.552.0
|
||||||
version: 0.552.0(react@19.2.0)
|
version: 0.552.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 16.0.1
|
specifier: 16.0.1
|
||||||
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
next-themes:
|
||||||
|
specifier: ^0.4.6
|
||||||
|
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react:
|
react:
|
||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0
|
version: 19.2.0
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0(react@19.2.0)
|
version: 19.2.0(react@19.2.0)
|
||||||
|
sonner:
|
||||||
|
specifier: ^2.0.7
|
||||||
|
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
@ -1124,6 +1133,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
framer-motion@12.23.24:
|
||||||
|
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
@ -1519,6 +1542,12 @@ packages:
|
|||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
|
motion-dom@12.23.23:
|
||||||
|
resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
|
||||||
|
|
||||||
|
motion-utils@12.23.6:
|
||||||
|
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@ -1535,6 +1564,12 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
next-themes@0.4.6:
|
||||||
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
next@16.0.1:
|
next@16.0.1:
|
||||||
resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==}
|
resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
@ -1766,6 +1801,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
sonner@2.0.7:
|
||||||
|
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -3155,6 +3196,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
|
|
||||||
|
framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.23.23
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
function.prototype.name@1.1.8:
|
function.prototype.name@1.1.8:
|
||||||
@ -3527,6 +3577,12 @@ snapshots:
|
|||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
|
motion-dom@12.23.23:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
|
||||||
|
motion-utils@12.23.6: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
@ -3535,6 +3591,11 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.0.1
|
'@next/env': 16.0.1
|
||||||
@ -3829,6 +3890,11 @@ snapshots:
|
|||||||
side-channel-map: 1.0.1
|
side-channel-map: 1.0.1
|
||||||
side-channel-weakmap: 1.0.2
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
stable-hash@0.0.5: {}
|
stable-hash@0.0.5: {}
|
||||||
|
|||||||
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