Compare commits
3 Commits
4d7201d03e
...
af29ec2a48
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af29ec2a48 | ||
|
|
458d9993e9 | ||
|
|
c1233f2643 |
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import SideNav from "@/components/side-nav";
|
||||
import SideNav from "@/app/(admin)/_components/side-nav";
|
||||
|
||||
export default function Booking() {
|
||||
return (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import SideNav from "@/components/side-nav";
|
||||
import SideNav from "@/app/(admin)/_components/side-nav";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import SideNav from "@/components/side-nav";
|
||||
import { Notifications, Notification } from "@/components/notifications";
|
||||
import SideNav from "@/app/(admin)/_components/side-nav";
|
||||
import { Notifications, Notification } from "@/app/(admin)/_components/notifications";
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([
|
||||
|
||||
7
app/(auth)/layout.tsx
Normal file
7
app/(auth)/layout.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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>
|
||||
);
|
||||
|
||||
78
app/page.tsx
78
app/page.tsx
@ -1,65 +1,25 @@
|
||||
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";
|
||||
import { Navbar } from "../components/Navbar";
|
||||
|
||||
|
||||
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>
|
||||
<Navbar />
|
||||
<HeroSection />
|
||||
|
||||
<About />
|
||||
|
||||
<Services />
|
||||
|
||||
<ContactSection />
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
13
app/providers.tsx
Normal file
13
app/providers.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "../components/ThemeProvider";
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
{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": {
|
||||
|
||||
@ -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