Merge branch 'master' of http://35.207.46.142/ATTUNE-HEART-THERAPY/website into feat/authentication

This commit is contained in:
iamkiddy 2025-11-23 19:06:39 +00:00
commit 3180e48fe1
39 changed files with 3580 additions and 424 deletions

View File

@ -4,11 +4,7 @@ import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { import {
Inbox, Inbox,
Calendar, Calendar,
@ -19,12 +15,16 @@ import {
Settings, Settings,
LogOut, LogOut,
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
import { ThemeToggle } from "@/components/ThemeToggle";
export function Header() { export function Header() {
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const [notificationsOpen, setNotificationsOpen] = useState(false); const [notificationsOpen, setNotificationsOpen] = useState(false);
const [userMenuOpen, setUserMenuOpen] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false);
const { theme } = useAppTheme();
const isDark = theme === "dark";
// Mock notifications data // Mock notifications data
const notifications = [ const notifications = [
@ -49,24 +49,26 @@ export function Header() {
const unreadCount = notifications.filter((n) => !n.read).length; const unreadCount = notifications.filter((n) => !n.read).length;
return ( return (
<header className="bg-white border-b border-gray-200 fixed top-0 left-0 right-0 z-50"> <header className={`fixed top-0 left-0 right-0 z-50 ${isDark ? "bg-gray-900 border-gray-800" : "bg-white border-gray-200"} border-b`}>
<div className="px-3 sm:px-4 md:px-6 lg:px-8"> <div className="px-3 sm:px-4 md:px-6 lg:px-8">
<div className="flex items-center justify-between h-14 sm:h-16"> <div className="flex items-center justify-between h-14 sm:h-16">
{/* Logo */} {/* Logo */}
<Link href="/dashboard" className="flex items-center gap-2 sm:gap-3"> <Link href="/" className="flex items-center gap-2 sm:gap-3">
<div className="flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-linear-to-r from-rose-100 to-pink-100"> <div className={`flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-lg ${isDark ? "bg-gray-800" : "bg-linear-to-r from-rose-100 to-pink-100"}`}>
<Heart className="w-4 h-4 sm:w-6 sm:h-6 text-rose-600" fill="currentColor" /> <Heart className={`w-4 h-4 sm:w-6 sm:h-6 ${isDark ? "text-rose-400" : "text-rose-600"}`} fill="currentColor" />
</div> </div>
<span className="text-base sm:text-lg md:text-xl font-semibold text-gray-900 hidden sm:inline">Attune Heart</span> <span className={`text-base sm:text-lg md:text-xl font-semibold hidden sm:inline ${isDark ? "text-white" : "text-gray-900"}`}>Attune Heart Therapy</span>
</Link> </Link>
{/* Navigation Links */} {/* Navigation Links */}
<nav className="flex items-center gap-0.5 sm:gap-1"> <nav className="flex items-center gap-0.5 sm:gap-1">
<Link <Link
href="/dashboard" href="/admin/dashboard"
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 md:px-4 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors ${ className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 md:px-4 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors ${
pathname === "/dashboard" pathname === "/admin/dashboard"
? "bg-linear-to-r from-rose-500 to-pink-600 text-white" ? "bg-linear-to-r from-rose-500 to-pink-600 text-white"
: isDark
? "text-gray-300 hover:bg-gray-800"
: "text-gray-600 hover:bg-gray-100" : "text-gray-600 hover:bg-gray-100"
}`} }`}
> >
@ -74,10 +76,12 @@ export function Header() {
<span className="hidden sm:inline">Dashboard</span> <span className="hidden sm:inline">Dashboard</span>
</Link> </Link>
<Link <Link
href="/booking" href="/admin/booking"
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 md:px-4 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors ${ className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 md:px-4 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors ${
pathname === "/booking" pathname === "/admin/booking"
? "bg-linear-to-r from-rose-500 to-pink-600 text-white" ? "bg-linear-to-r from-rose-500 to-pink-600 text-white"
: isDark
? "text-gray-300 hover:bg-gray-800"
: "text-gray-600 hover:bg-gray-100" : "text-gray-600 hover:bg-gray-100"
}`} }`}
> >
@ -88,23 +92,24 @@ export function Header() {
{/* Right Side Actions */} {/* Right Side Actions */}
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3"> <div className="flex items-center gap-1.5 sm:gap-2 md:gap-3">
<ThemeToggle />
<Popover open={notificationsOpen} onOpenChange={setNotificationsOpen}> <Popover open={notificationsOpen} onOpenChange={setNotificationsOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="relative w-8 h-8 sm:w-9 sm:h-9 md:w-10 md:h-10 cursor-pointer"> <Button variant="ghost" size="icon" className="relative w-8 h-8 sm:w-9 sm:h-9 md:w-10 md:h-10 cursor-pointer">
<Inbox className="w-5 h-5 sm:w-6 sm:h-6 md:w-8 md:h-8 text-gray-600" /> <Inbox className={`w-5 h-5 sm:w-6 sm:h-6 md:w-8 md:h-8 ${isDark ? "text-gray-300" : "text-gray-600"}`} />
{unreadCount > 0 && ( {unreadCount > 0 && (
<span className="absolute top-0.5 right-0.5 sm:top-1 sm:right-1 w-1.5 h-1.5 sm:w-2 sm:h-2 bg-green-500 rounded-full"></span> <span className="absolute top-0.5 right-0.5 sm:top-1 sm:right-1 w-1.5 h-1.5 sm:w-2 sm:h-2 bg-green-500 rounded-full"></span>
)} )}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[calc(100vw-2rem)] sm:w-80 md:w-96 p-0 bg-white shadow-xl" align="end"> <PopoverContent className={`w-[calc(100vw-2rem)] sm:w-80 md:w-96 p-0 shadow-xl border ${isDark ? "bg-gray-900 border-gray-800" : "bg-white border-gray-200"}`} align="end">
{/* Thumbtack Design at Top Right */} {/* Thumbtack Design at Top Right */}
<div className="relative"> <div className="relative">
<div className="absolute -top-2 right-8 w-4 h-4 bg-white border-l border-t border-gray-200 rotate-45"></div> <div className={`absolute -top-2 right-8 w-4 h-4 rotate-45 ${isDark ? "bg-gray-900 border-l border-t border-gray-800" : "bg-white border-l border-t border-gray-200"}`}></div>
<div className="absolute -top-1 right-8 w-2 h-2 bg-white translate-x-1/2"></div> <div className={`absolute -top-1 right-8 w-2 h-2 translate-x-1/2 ${isDark ? "bg-gray-900" : "bg-white"}`}></div>
</div> </div>
<div className="flex items-center justify-between p-4 border-b"> <div className={`flex items-center justify-between p-4 border-b ${isDark ? "border-gray-800" : ""}`}>
<h3 className="font-semibold text-gray-900">Notifications</h3> <h3 className={`font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>Notifications</h3>
{unreadCount > 0 && ( {unreadCount > 0 && (
<span className="px-2 py-1 text-xs font-medium bg-rose-100 text-rose-700 rounded-full"> <span className="px-2 py-1 text-xs font-medium bg-rose-100 text-rose-700 rounded-full">
{unreadCount} new {unreadCount} new
@ -113,24 +118,28 @@ export function Header() {
</div> </div>
<div className="max-h-96 overflow-y-auto"> <div className="max-h-96 overflow-y-auto">
{notifications.length === 0 ? ( {notifications.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className={`p-8 text-center ${isDark ? "text-gray-400" : "text-gray-500"}`}>
<Bell className="w-12 h-12 mx-auto mb-2 text-gray-300" /> <Bell className={`w-12 h-12 mx-auto mb-2 ${isDark ? "text-gray-600" : "text-gray-300"}`} />
<p className="text-sm">No notifications</p> <p className="text-sm">No notifications</p>
</div> </div>
) : ( ) : (
<div className="divide-y"> <div className={`divide-y ${isDark ? "divide-gray-800" : ""}`}>
{notifications.map((notification) => { {notifications.map((notification) => {
return ( return (
<div <div
key={notification.id} key={notification.id}
className={`p-4 hover:bg-gray-50 transition-colors cursor-pointer ${ className={`p-4 transition-colors cursor-pointer ${
!notification.read ? "bg-rose-50/50" : "" !notification.read
? isDark
? "bg-rose-500/10"
: "bg-rose-50/50"
: ""
}`} }`}
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p <p
className={`text-sm font-medium text-gray-900 ${ className={`text-sm font-medium ${isDark ? "text-white" : "text-gray-900"} ${
!notification.read ? "font-semibold" : "" !notification.read ? "font-semibold" : ""
}`} }`}
> >
@ -140,10 +149,10 @@ export function Header() {
<span className="shrink-0 w-2 h-2 bg-green-500 rounded-full mt-1"></span> <span className="shrink-0 w-2 h-2 bg-green-500 rounded-full mt-1"></span>
)} )}
</div> </div>
<p className="text-sm text-gray-600 mt-1"> <p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-600"}`}>
{notification.message} {notification.message}
</p> </p>
<p className="text-xs text-gray-400 mt-1"> <p className={`text-xs mt-1 ${isDark ? "text-gray-500" : "text-gray-400"}`}>
{notification.time} {notification.time}
</p> </p>
</div> </div>
@ -153,11 +162,11 @@ export function Header() {
</div> </div>
)} )}
</div> </div>
<div className="p-3 border-t bg-gray-50"> <div className={`p-3 border-t ${isDark ? "border-gray-800 bg-gray-900/80" : "bg-gray-50"}`}>
<Link <Link
href="/notifications" href="/admin/notifications"
onClick={() => setNotificationsOpen(false)} onClick={() => setNotificationsOpen(false)}
className="block w-full text-center text-sm font-medium text-rose-600 hover:text-rose-700 hover:underline transition-colors" className={`block w-full text-center text-sm font-medium hover:underline transition-colors ${isDark ? "text-rose-300 hover:text-rose-200" : "text-rose-600 hover:text-rose-700"}`}
> >
View all notifications View all notifications
</Link> </Link>
@ -166,27 +175,37 @@ export function Header() {
</Popover> </Popover>
<Popover open={userMenuOpen} onOpenChange={setUserMenuOpen}> <Popover open={userMenuOpen} onOpenChange={setUserMenuOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full bg-linear-to-r from-rose-100 to-pink-100 hover:from-rose-200 hover:to-pink-200 cursor-pointer w-8 h-8 sm:w-9 sm:h-9 md:w-10 md:h-10"> <Button
<UserCog className="w-4 h-4 sm:w-5 sm:h-5 text-rose-600" /> variant="ghost"
size="icon"
className={`rounded-full cursor-pointer w-8 h-8 sm:w-9 sm:h-9 md:w-10 md:h-10 ${
isDark
? "bg-gray-800 hover:bg-gray-700"
: "bg-linear-to-r from-rose-100 to-pink-100 hover:from-rose-200 hover:to-pink-200"
}`}
>
<UserCog className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? "text-rose-300" : "text-rose-600"}`} />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-56 sm:w-64 p-0 bg-white shadow-xl" align="end"> <PopoverContent className={`w-56 sm:w-64 p-0 shadow-xl border ${isDark ? "bg-gray-900 border-gray-800" : "bg-white border-gray-200"}`} align="end">
{/* Thumbtack Design at Top Right */} {/* Thumbtack Design at Top Right */}
<div className="relative"> <div className="relative">
<div className="absolute -top-2 right-8 w-4 h-4 bg-white border-l border-t border-gray-200 rotate-45"></div> <div className={`absolute -top-2 right-8 w-4 h-4 rotate-45 ${isDark ? "bg-gray-900 border-l border-t border-gray-800" : "bg-white border-l border-t border-gray-200"}`}></div>
<div className="absolute -top-1 right-8 w-2 h-2 bg-white translate-x-1/2"></div> <div className={`absolute -top-1 right-8 w-2 h-2 translate-x-1/2 ${isDark ? "bg-gray-900" : "bg-white"}`}></div>
</div> </div>
<div className="py-2"> <div className="py-2">
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setUserMenuOpen(false); setUserMenuOpen(false);
// Add settings navigation here router.push("/admin/settings");
}} }}
className="w-full flex items-center gap-3 px-4 py-3 justify-start hover:bg-gray-50 transition-colors cursor-pointer" className={`w-full flex items-center gap-3 px-4 py-3 justify-start transition-colors cursor-pointer ${
isDark ? "hover:bg-gray-800" : "hover:bg-gray-50"
}`}
> >
<Settings className="w-5 h-5 text-gray-600" /> <Settings className={`w-5 h-5 ${isDark ? "text-gray-300" : "text-gray-600"}`} />
<span className="text-sm font-medium text-gray-900">Settings</span> <span className={`text-sm font-medium ${isDark ? "text-white" : "text-gray-900"}`}>Settings</span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@ -194,7 +213,9 @@ export function Header() {
setUserMenuOpen(false); setUserMenuOpen(false);
router.push("/"); router.push("/");
}} }}
className="w-full flex items-center gap-3 px-4 py-3 justify-start hover:bg-gray-50 transition-colors cursor-pointer" className={`w-full flex items-center gap-3 px-4 py-3 justify-start transition-colors cursor-pointer ${
isDark ? "hover:bg-gray-800" : "hover:bg-gray-50"
}`}
> >
<LogOut className="w-5 h-5 text-red-500" /> <LogOut className="w-5 h-5 text-red-500" />
<span className="text-sm font-medium text-red-500">Logout</span> <span className="text-sm font-medium text-red-500">Logout</span>

View File

@ -12,6 +12,7 @@ import {
Clock, Clock,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useAppTheme } from "@/components/ThemeProvider";
export interface Notification { export interface Notification {
id: string; id: string;
@ -36,30 +37,32 @@ export function Notifications({
onMarkAllAsRead, onMarkAllAsRead,
}: NotificationsProps) { }: NotificationsProps) {
const unreadCount = notifications.filter((n) => !n.read).length; const unreadCount = notifications.filter((n) => !n.read).length;
const { theme } = useAppTheme();
const isDark = theme === "dark";
const getIcon = (type: Notification["type"]) => { const getIcon = (type: Notification["type"]) => {
switch (type) { switch (type) {
case "success": case "success":
return <CheckCircle className="w-5 h-5 text-green-600" />; return <CheckCircle className={`w-5 h-5 ${isDark ? "text-green-300" : "text-green-600"}`} />;
case "warning": case "warning":
return <AlertCircle className="w-5 h-5 text-orange-600" />; return <AlertCircle className={`w-5 h-5 ${isDark ? "text-orange-300" : "text-orange-600"}`} />;
case "info": case "info":
return <Info className="w-5 h-5 text-blue-600" />; return <Info className={`w-5 h-5 ${isDark ? "text-blue-300" : "text-blue-600"}`} />;
case "appointment": case "appointment":
return <Calendar className="w-5 h-5 text-rose-600" />; return <Calendar className={`w-5 h-5 ${isDark ? "text-rose-300" : "text-rose-600"}`} />;
} }
}; };
const getBgColor = (type: Notification["type"]) => { const getBgColor = (type: Notification["type"]) => {
switch (type) { switch (type) {
case "success": case "success":
return "bg-[#4A90A4]/10 border-[#4A90A4]/30"; return isDark ? "bg-green-500/10 border-green-500/30" : "bg-[#4A90A4]/10 border-[#4A90A4]/30";
case "warning": case "warning":
return "bg-rose-100 border-rose-300"; return isDark ? "bg-rose-500/10 border-rose-400/40" : "bg-rose-100 border-rose-300";
case "info": case "info":
return "bg-pink-50 border-pink-200"; return isDark ? "bg-pink-500/10 border-pink-400/40" : "bg-pink-50 border-pink-200";
case "appointment": case "appointment":
return "bg-gradient-to-br from-rose-50 to-pink-50 border-rose-300"; return isDark ? "bg-rose-500/10 border-rose-400/40" : "bg-gradient-to-br from-rose-50 to-pink-50 border-rose-300";
} }
}; };
@ -68,8 +71,8 @@ export function Notifications({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Bell className="w-6 h-6 text-gray-900" /> <Bell className={`w-6 h-6 ${isDark ? "text-white" : "text-gray-900"}`} />
<h2 className="text-2xl font-bold text-gray-900">Notifications</h2> <h2 className={`text-2xl font-bold ${isDark ? "text-white" : "text-gray-900"}`}>Notifications</h2>
{unreadCount > 0 && ( {unreadCount > 0 && (
<span className="px-2.5 py-0.5 bg-linear-to-r from-rose-500 to-pink-500 text-white text-sm font-medium rounded-full"> <span className="px-2.5 py-0.5 bg-linear-to-r from-rose-500 to-pink-500 text-white text-sm font-medium rounded-full">
{unreadCount} {unreadCount}
@ -77,7 +80,12 @@ export function Notifications({
)} )}
</div> </div>
{unreadCount > 0 && onMarkAllAsRead && ( {unreadCount > 0 && onMarkAllAsRead && (
<Button variant="outline" size="sm" onClick={onMarkAllAsRead}> <Button
variant="outline"
size="sm"
onClick={onMarkAllAsRead}
className={isDark ? "border-gray-700 text-gray-200 hover:bg-gray-800" : ""}
>
Mark all as read Mark all as read
</Button> </Button>
)} )}
@ -87,8 +95,8 @@ export function Notifications({
<div className="space-y-3"> <div className="space-y-3">
{notifications.length === 0 ? ( {notifications.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<Bell className="w-12 h-12 text-gray-400 mx-auto mb-4" /> <Bell className={`w-12 h-12 mx-auto mb-4 ${isDark ? "text-gray-600" : "text-gray-400"}`} />
<p className="text-gray-600">No notifications</p> <p className={isDark ? "text-gray-400" : "text-gray-600"}>No notifications</p>
</div> </div>
) : ( ) : (
notifications.map((notification) => ( notifications.map((notification) => (
@ -97,7 +105,7 @@ export function Notifications({
className={cn( className={cn(
"p-4 rounded-lg border-2 transition-all", "p-4 rounded-lg border-2 transition-all",
getBgColor(notification.type), getBgColor(notification.type),
!notification.read && "ring-2 ring-offset-2 ring-rose-300" !notification.read && (isDark ? "ring-2 ring-offset-2 ring-rose-400 ring-offset-gray-900" : "ring-2 ring-offset-2 ring-rose-300")
)} )}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
@ -108,13 +116,15 @@ export function Notifications({
<h3 <h3
className={cn( className={cn(
"font-semibold mb-1", "font-semibold mb-1",
!notification.read ? "text-gray-900" : "text-gray-700" !notification.read
? isDark ? "text-white" : "text-gray-900"
: isDark ? "text-gray-300" : "text-gray-700"
)} )}
> >
{notification.title} {notification.title}
</h3> </h3>
<p className="text-sm text-gray-600 mb-2">{notification.message}</p> <p className={`text-sm mb-2 ${isDark ? "text-gray-400" : "text-gray-600"}`}>{notification.message}</p>
<div className="flex items-center gap-2 text-xs text-gray-500"> <div className={`flex items-center gap-2 text-xs ${isDark ? "text-gray-500" : "text-gray-500"}`}>
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{notification.time} {notification.time}
</div> </div>
@ -125,7 +135,7 @@ export function Notifications({
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
onClick={() => onMarkAsRead(notification.id)} onClick={() => onMarkAsRead(notification.id)}
className="h-7 w-7" className={`h-7 w-7 ${isDark ? "text-gray-300 hover:bg-gray-800" : ""}`}
> >
<CheckCircle className="w-4 h-4" /> <CheckCircle className="w-4 h-4" />
</Button> </Button>
@ -135,7 +145,7 @@ export function Notifications({
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
onClick={() => onDismiss(notification.id)} onClick={() => onDismiss(notification.id)}
className="h-7 w-7" className={`h-7 w-7 ${isDark ? "text-gray-300 hover:bg-gray-800" : ""}`}
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</Button> </Button>

View File

@ -13,16 +13,19 @@ import {
X, X,
Heart, Heart,
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
const navItems = [ const navItems = [
{ label: "Dashboard", icon: LayoutGrid, href: "/dashboard" }, { label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" },
{ label: "Book Appointment", icon: Calendar, href: "/booking" }, { label: "Book Appointment", icon: Calendar, href: "/admin/booking" },
]; ];
export default function SideNav() { export default function SideNav() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const { theme } = useAppTheme();
const isDark = theme === "dark";
const getActiveIndex = () => { const getActiveIndex = () => {
return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1; return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1;
@ -43,13 +46,13 @@ export default function SideNav() {
return ( return (
<> <>
{/* Mobile Top Bar */} {/* Mobile Top Bar */}
<div className="flex md:hidden items-center justify-between px-4 py-3 border-b border-gray-200 bg-white z-30 fixed top-0 left-0 right-0"> <div className={`flex md:hidden items-center justify-between px-4 py-3 border-b z-30 fixed top-0 left-0 right-0 ${isDark ? "bg-gray-900 border-gray-800" : "bg-white border-gray-200"}`}>
<div className="flex items-center gap-3"> <Link href="/" className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-linear-to-r from-rose-100 to-pink-100"> <div className={`flex items-center justify-center w-8 h-8 rounded-lg ${isDark ? "bg-gray-800" : "bg-linear-to-r from-rose-100 to-pink-100"}`}>
<Heart className="w-5 h-5 text-rose-600" fill="currentColor" /> <Heart className={`w-5 h-5 ${isDark ? "text-rose-300" : "text-rose-600"}`} fill="currentColor" />
</div>
<span className="text-lg font-semibold text-gray-900">Attune Heart Therapy</span>
</div> </div>
<span className={`text-lg font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>Attune Heart Therapy</span>
</Link>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -70,21 +73,21 @@ export default function SideNav() {
{/* Side Navigation */} {/* Side Navigation */}
<aside <aside
className={`fixed top-0 left-0 z-50 h-screen bg-white border-r border-gray-200 flex flex-col transition-transform duration-200 w-[85vw] max-w-[200px] min-w-[160px] md:translate-x-0 md:w-[200px] md:min-w-[200px] md:max-w-[200px] ${ className={`fixed top-0 left-0 z-50 h-screen flex flex-col transition-transform duration-200 w-[85vw] max-w-[200px] min-w-[160px] md:translate-x-0 md:w-[200px] md:min-w-[200px] md:max-w-[200px] ${isDark ? "bg-gray-900 border-r border-gray-800" : "bg-white border-r border-gray-200"} ${
open ? "translate-x-0" : "-translate-x-full" open ? "translate-x-0" : "-translate-x-full"
} md:translate-x-0`} } md:translate-x-0`}
> >
{/* Logo Section */} {/* Logo Section */}
<div className="shrink-0 px-3 pb-4 flex flex-col gap-1 md:block mb-5 pt-16 md:pt-4"> <div className="shrink-0 px-3 pb-4 flex flex-col gap-1 md:block mb-5 pt-16 md:pt-4">
<div className="flex items-center gap-2 mb-1 ml-2 md:ml-3"> <Link href="/" className="flex items-center gap-2 mb-1 ml-2 md:ml-3">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-linear-to-r from-rose-100 to-pink-100"> <div className={`flex items-center justify-center w-8 h-8 rounded-lg ${isDark ? "bg-gray-800" : "bg-linear-to-r from-rose-100 to-pink-100"}`}>
<Heart className="w-5 h-5 text-rose-600" fill="currentColor" /> <Heart className={`w-5 h-5 ${isDark ? "text-rose-300" : "text-rose-600"}`} fill="currentColor" />
</div>
<span className="text-sm font-semibold text-gray-900">Attune Heart</span>
</div> </div>
<span className={`text-sm font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>Attune Heart</span>
</Link>
</div> </div>
<hr className="shrink-0 -mt-10 mb-4 mx-3 border-gray-200 md:block hidden" /> <hr className={`shrink-0 -mt-10 mb-4 mx-3 md:block hidden ${isDark ? "border-gray-800" : "border-gray-200"}`} />
{/* Navigation Items */} {/* Navigation Items */}
<nav className="flex-1 overflow-y-auto flex flex-col gap-2 px-2 md:px-0"> <nav className="flex-1 overflow-y-auto flex flex-col gap-2 px-2 md:px-0">
@ -106,6 +109,8 @@ export default function SideNav() {
className={`group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 focus:outline-none w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start ${ className={`group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 focus:outline-none w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start ${
isActive isActive
? "bg-linear-to-r from-rose-500 to-pink-600 text-white border border-rose-500 rounded-[5px] shadow-sm" ? "bg-linear-to-r from-rose-500 to-pink-600 text-white border border-rose-500 rounded-[5px] shadow-sm"
: isDark
? "bg-transparent text-gray-300 hover:bg-gray-800 hover:text-rose-300 rounded-lg"
: "bg-transparent text-gray-600 hover:bg-rose-50 hover:text-rose-600 rounded-lg" : "bg-transparent text-gray-600 hover:bg-rose-50 hover:text-rose-600 rounded-lg"
}`} }`}
style={isActive ? { height: 40 } : {}} style={isActive ? { height: 40 } : {}}
@ -116,6 +121,8 @@ export default function SideNav() {
className={ className={
isActive isActive
? "text-white" ? "text-white"
: isDark
? "text-gray-400 group-hover:text-rose-300"
: "text-gray-700 group-hover:text-rose-600" : "text-gray-700 group-hover:text-rose-600"
} }
/> />
@ -131,26 +138,55 @@ export default function SideNav() {
})} })}
{/* Bottom Actions */} {/* Bottom Actions */}
<div className="mt-auto pt-4 pb-4 border-t border-gray-200"> <div className={`mt-auto pt-4 pb-4 border-t ${isDark ? "border-gray-800" : "border-gray-200"}`}>
<div className="relative flex items-center w-full">
{pathname === "/admin/settings" && (
<span
className="absolute left-0 top-0 h-[40px] w-[3px] bg-linear-to-r from-rose-500 to-pink-600"
style={{ left: 0 }}
/>
)}
<Link <Link
href="/settings" href="/admin/settings"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start text-gray-600 hover:bg-gray-50 hover:text-gray-900 rounded-lg" className={`group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start rounded-lg ${
pathname === "/admin/settings"
? "bg-linear-to-r from-rose-500 to-pink-600 text-white border border-rose-500 rounded-[5px] shadow-sm"
: isDark
? "text-gray-300 hover:bg-gray-800 hover:text-rose-300"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
}`}
style={pathname === "/admin/settings" ? { height: 40 } : {}}
> >
<Settings size={18} strokeWidth={1.5} className="text-gray-700 group-hover:text-gray-900" /> <Settings
size={18}
strokeWidth={pathname === "/admin/settings" ? 2.2 : 1.5}
className={
pathname === "/admin/settings"
? "text-white"
: isDark
? "text-gray-400 group-hover:text-rose-300"
: "text-gray-700 group-hover:text-gray-900"
}
/>
<span className="font-light leading-none text-[12px]" style={{ fontWeight: 300 }}> <span className="font-light leading-none text-[12px]" style={{ fontWeight: 300 }}>
Settings Settings
</span> </span>
</Link> </Link>
</div>
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
router.push("/"); router.push("/");
}} }}
className="group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start text-gray-600 hover:bg-gray-50 hover:text-gray-900 rounded-lg" className={`group flex items-center gap-2 py-2.5 pl-3 md:pl-3 pr-3 md:pr-3 transition-colors duration-200 w-[90%] md:w-[90%] ml-1 md:ml-2 cursor-pointer justify-start rounded-lg ${
isDark
? "text-gray-300 hover:bg-gray-800 hover:text-rose-300"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
}`}
> >
<LogOut size={18} strokeWidth={1.5} className="text-gray-700 group-hover:text-gray-900" /> <LogOut size={18} strokeWidth={1.5} className={isDark ? "text-gray-400 group-hover:text-rose-300" : "text-gray-700 group-hover:text-gray-900"} />
<span className="font-light leading-none text-[12px]" style={{ fontWeight: 300 }}> <span className="font-light leading-none text-[12px]" style={{ fontWeight: 300 }}>
Logout Logout
</span> </span>

View File

@ -9,6 +9,7 @@ import {
FileText, FileText,
MoreVertical, MoreVertical,
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
interface User { interface User {
ID: number; ID: number;
@ -54,6 +55,8 @@ export default function Booking() {
const [bookings, setBookings] = useState<Booking[]>([]); const [bookings, setBookings] = useState<Booking[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const { theme } = useAppTheme();
const isDark = theme === "dark";
useEffect(() => { useEffect(() => {
// Simulate API call // Simulate API call
@ -127,7 +130,22 @@ export default function Booking() {
}; };
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status.toLowerCase()) { const normalized = status.toLowerCase();
if (isDark) {
switch (normalized) {
case "scheduled":
return "bg-blue-500/20 text-blue-200";
case "completed":
return "bg-green-500/20 text-green-200";
case "cancelled":
return "bg-red-500/20 text-red-200";
case "pending":
return "bg-yellow-500/20 text-yellow-200";
default:
return "bg-gray-700 text-gray-200";
}
}
switch (normalized) {
case "scheduled": case "scheduled":
return "bg-blue-100 text-blue-700"; return "bg-blue-100 text-blue-700";
case "completed": case "completed":
@ -142,7 +160,20 @@ export default function Booking() {
}; };
const getPaymentStatusColor = (status: string) => { const getPaymentStatusColor = (status: string) => {
switch (status.toLowerCase()) { const normalized = status.toLowerCase();
if (isDark) {
switch (normalized) {
case "paid":
return "bg-green-500/20 text-green-200";
case "pending":
return "bg-yellow-500/20 text-yellow-200";
case "failed":
return "bg-red-500/20 text-red-200";
default:
return "bg-gray-700 text-gray-200";
}
}
switch (normalized) {
case "paid": case "paid":
return "bg-green-100 text-green-700"; return "bg-green-100 text-green-700";
case "pending": case "pending":
@ -166,102 +197,104 @@ export default function Booking() {
); );
return ( return (
<div className="min-h-screen bg-gray-50"> <div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
{/* Main Content */} {/* Main Content */}
<main className="p-3 sm:p-4 md:p-6 lg:p-8"> <main className="p-3 sm:p-4 md:p-6 lg:p-8">
{/* Page Header */} {/* Page Header */}
<div className="mb-4 sm:mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4"> <div className="mb-4 sm:mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div> <div>
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-1"> <h1 className={`text-xl sm:text-2xl font-semibold mb-1 ${isDark ? "text-white" : "text-gray-900"}`}>
Bookings Bookings
</h1> </h1>
<p className="text-xs sm:text-sm text-gray-500"> <p className={`text-xs sm:text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Manage and view all appointment bookings Manage and view all appointment bookings
</p> </p>
</div> </div>
<button className="w-full sm:w-auto px-3 sm:px-4 py-2 bg-gray-900 text-white rounded-lg text-xs sm:text-sm font-medium hover:bg-gray-800 transition-colors"> <button className={`w-full sm:w-auto px-3 sm:px-4 py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors ${
isDark ? "bg-rose-500 text-white hover:bg-rose-600" : "bg-gray-900 text-white hover:bg-gray-800"
}`}>
+ New Booking + New Booking
</button> </button>
</div> </div>
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-400"></div> <div className={`animate-spin rounded-full h-8 w-8 border-b-2 ${isDark ? "border-gray-600" : "border-gray-400"}`}></div>
</div> </div>
) : filteredBookings.length === 0 ? ( ) : filteredBookings.length === 0 ? (
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center"> <div className={`rounded-lg border p-12 text-center ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<Calendar className="w-12 h-12 text-gray-400 mx-auto mb-4" /> <Calendar className={`w-12 h-12 mx-auto mb-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<p className="text-gray-600 font-medium mb-1">No bookings found</p> <p className={`font-medium mb-1 ${isDark ? "text-gray-200" : "text-gray-600"}`}>No bookings found</p>
<p className="text-sm text-gray-500"> <p className={`text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{searchTerm {searchTerm
? "Try adjusting your search terms" ? "Try adjusting your search terms"
: "Create a new booking to get started"} : "Create a new booking to get started"}
</p> </p>
</div> </div>
) : ( ) : (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden"> <div className={`rounded-lg border overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className={`${isDark ? "bg-gray-800 border-b border-gray-700" : "bg-gray-50 border-b border-gray-200"}`}>
<tr> <tr>
<th className="px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Patient Patient
</th> </th>
<th className="px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell"> <th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden md:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Date & Time Date & Time
</th> </th>
<th className="px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden lg:table-cell"> <th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden lg:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Duration Duration
</th> </th>
<th className="px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Status Status
</th> </th>
<th className="px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden lg:table-cell"> <th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden lg:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Payment Payment
</th> </th>
<th className="px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden xl:table-cell"> <th className={`px-3 sm:px-4 md:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider hidden xl:table-cell ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Amount Amount
</th> </th>
<th className="px-3 sm:px-4 md:px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className={`px-3 sm:px-4 md:px-6 py-3 text-right text-xs font-medium uppercase tracking-wider ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Actions Actions
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className={`${isDark ? "bg-gray-800 divide-gray-700" : "bg-white divide-gray-200"}`}>
{filteredBookings.map((booking) => ( {filteredBookings.map((booking) => (
<tr <tr
key={booking.ID} key={booking.ID}
className="hover:bg-gray-50 transition-colors" className={`transition-colors ${isDark ? "hover:bg-gray-700" : "hover:bg-gray-50"}`}
> >
<td className="px-3 sm:px-4 md:px-6 py-4"> <td className="px-3 sm:px-4 md:px-6 py-4">
<div className="flex items-center"> <div className="flex items-center">
<div className="shrink-0 h-8 w-8 sm:h-10 sm:w-10 rounded-full bg-gray-100 flex items-center justify-center"> <div className={`shrink-0 h-8 w-8 sm:h-10 sm:w-10 rounded-full flex items-center justify-center ${isDark ? "bg-gray-700" : "bg-gray-100"}`}>
<User className="w-4 h-4 sm:w-5 sm:h-5 text-gray-600" /> <User className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? "text-gray-200" : "text-gray-600"}`} />
</div> </div>
<div className="ml-2 sm:ml-4 min-w-0"> <div className="ml-2 sm:ml-4 min-w-0">
<div className="text-xs sm:text-sm font-medium text-gray-900 truncate"> <div className={`text-xs sm:text-sm font-medium truncate ${isDark ? "text-white" : "text-gray-900"}`}>
{booking.user.first_name} {booking.user.last_name} {booking.user.first_name} {booking.user.last_name}
</div> </div>
<div className="text-xs sm:text-sm text-gray-500 truncate hidden sm:block"> <div className={`text-xs sm:text-sm truncate hidden sm:block ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{booking.user.email} {booking.user.email}
</div> </div>
<div className="text-xs text-gray-500 sm:hidden mt-0.5"> <div className={`text-xs sm:hidden mt-0.5 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{formatDate(booking.scheduled_at)} {formatDate(booking.scheduled_at)}
</div> </div>
</div> </div>
</div> </div>
</td> </td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap hidden md:table-cell"> <td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap hidden md:table-cell">
<div className="text-xs sm:text-sm text-gray-900"> <div className={`text-xs sm:text-sm ${isDark ? "text-white" : "text-gray-900"}`}>
{formatDate(booking.scheduled_at)} {formatDate(booking.scheduled_at)}
</div> </div>
<div className="text-xs sm:text-sm text-gray-500 flex items-center gap-1"> <div className={`text-xs sm:text-sm flex items-center gap-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{formatTime(booking.scheduled_at)} {formatTime(booking.scheduled_at)}
</div> </div>
</td> </td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-xs sm:text-sm text-gray-900 hidden lg:table-cell"> <td className={`px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-xs sm:text-sm hidden lg:table-cell ${isDark ? "text-white" : "text-gray-900"}`}>
{booking.duration} min {booking.duration} min
</td> </td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap"> <td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap">
@ -282,7 +315,7 @@ export default function Booking() {
{booking.payment_status} {booking.payment_status}
</span> </span>
</td> </td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-xs sm:text-sm font-medium text-gray-900 hidden xl:table-cell"> <td className={`px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-xs sm:text-sm font-medium hidden xl:table-cell ${isDark ? "text-white" : "text-gray-900"}`}>
${booking.amount} ${booking.amount}
</td> </td>
<td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-3 sm:px-4 md:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
@ -292,7 +325,7 @@ export default function Booking() {
href={booking.jitsi_room_url} href={booking.jitsi_room_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-1.5 sm:p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" className={`p-1.5 sm:p-2 rounded-lg transition-colors ${isDark ? "text-gray-300 hover:text-white hover:bg-gray-700" : "text-gray-600 hover:text-gray-900 hover:bg-gray-100"}`}
title="Join Meeting" title="Join Meeting"
> >
<Video className="w-4 h-4" /> <Video className="w-4 h-4" />
@ -300,13 +333,13 @@ export default function Booking() {
)} )}
{booking.notes && ( {booking.notes && (
<button <button
className="p-1.5 sm:p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" className={`p-1.5 sm:p-2 rounded-lg transition-colors ${isDark ? "text-gray-300 hover:text-white hover:bg-gray-700" : "text-gray-600 hover:text-gray-900 hover:bg-gray-100"}`}
title="View Notes" title="View Notes"
> >
<FileText className="w-4 h-4" /> <FileText className="w-4 h-4" />
</button> </button>
)} )}
<button className="p-1.5 sm:p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"> <button className={`p-1.5 sm:p-2 rounded-lg transition-colors ${isDark ? "text-gray-300 hover:text-white hover:bg-gray-700" : "text-gray-600 hover:text-gray-900 hover:bg-gray-100"}`}>
<MoreVertical className="w-4 h-4" /> <MoreVertical className="w-4 h-4" />
</button> </button>
</div> </div>

View File

@ -19,6 +19,7 @@ import {
ArrowUpRight, ArrowUpRight,
ArrowDownRight, ArrowDownRight,
} from "lucide-react"; } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
interface DashboardStats { interface DashboardStats {
total_users: number; total_users: number;
@ -35,6 +36,8 @@ export default function Dashboard() {
const [stats, setStats] = useState<DashboardStats | null>(null); const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [timePeriod, setTimePeriod] = useState<string>("last_month"); const [timePeriod, setTimePeriod] = useState<string>("last_month");
const { theme } = useAppTheme();
const isDark = theme === "dark";
useEffect(() => { useEffect(() => {
// Simulate API call // Simulate API call
@ -122,36 +125,43 @@ export default function Dashboard() {
]; ];
const getTrendClasses = (trendUp: boolean) => {
if (isDark) {
return trendUp ? "bg-green-500/20 text-green-200" : "bg-red-500/20 text-red-200";
}
return trendUp ? "bg-green-50 text-green-700" : "bg-red-50 text-red-700";
};
return ( return (
<div className="min-h-screen bg-gray-50"> <div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
{/* Main Content */} {/* Main Content */}
<main className="p-4 sm:p-6 lg:p-8 space-y-6"> <main className="p-4 sm:p-6 lg:p-8 space-y-6">
{/* Welcome Section */} {/* Welcome Section */}
<div className="flex items-center justify-between mb-6"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div> <div>
<h1 className="text-2xl font-semibold text-gray-900 mb-1"> <h1 className={`text-2xl font-semibold mb-1 ${isDark ? "text-white" : "text-gray-900"}`}>
Welcome Back! Hammond Welcome Back! Hammond
</h1> </h1>
<p className="text-sm text-gray-500"> <p className={`text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Here's an overview of your practice today Here's an overview of your practice today
</p> </p>
</div> </div>
<Select value={timePeriod} onValueChange={setTimePeriod}> <Select value={timePeriod} onValueChange={setTimePeriod}>
<SelectTrigger className="w-[180px] cursor-pointer"> <SelectTrigger className={`w-full sm:w-[200px] cursor-pointer ${isDark ? "bg-gray-800 border-gray-700 text-gray-100" : "bg-white border-gray-200 text-gray-900"}`}>
<SelectValue placeholder="Select period" /> <SelectValue placeholder="Select period" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-white cursor-pointer"> <SelectContent className={`${isDark ? "bg-gray-800 border-gray-700 text-gray-100" : "bg-white border-gray-200 text-gray-900"} cursor-pointer`}>
<SelectItem value="last_week">Last Week</SelectItem> <SelectItem className={isDark ? "focus:bg-gray-700" : ""} value="last_week">Last Week</SelectItem>
<SelectItem value="last_month">Last Month</SelectItem> <SelectItem className={isDark ? "focus:bg-gray-700" : ""} value="last_month">Last Month</SelectItem>
<SelectItem value="last_year">Last Year</SelectItem> <SelectItem className={isDark ? "focus:bg-gray-700" : ""} value="last_year">Last Year</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-400"></div> <div className={`animate-spin rounded-full h-8 w-8 border-b-2 ${isDark ? "border-gray-600" : "border-gray-400"}`}></div>
</div> </div>
) : ( ) : (
<> <>
@ -162,17 +172,13 @@ export default function Dashboard() {
return ( return (
<div <div
key={index} key={index}
className="bg-white rounded-lg border border-gray-200 p-4 sm:p-5 md:p-6 hover:shadow-md transition-shadow" className={`rounded-lg border p-4 sm:p-5 md:p-6 hover:shadow-md transition-shadow ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}
> >
<div className="flex items-start justify-between mb-3 sm:mb-4"> <div className="flex items-start justify-between mb-3 sm:mb-4">
<div className="p-2 sm:p-2.5 rounded-lg bg-gray-50"> <div className={`p-2 sm:p-2.5 rounded-lg ${isDark ? "bg-gray-700" : "bg-gray-50"}`}>
<Icon className="w-4 h-4 sm:w-5 sm:h-5 text-gray-600" /> <Icon className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? "text-gray-200" : "text-gray-600"}`} />
</div> </div>
<div className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${ <div className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${getTrendClasses(card.trendUp)}`}>
card.trendUp
? "bg-green-50 text-green-700"
: "bg-red-50 text-red-700"
}`}>
{card.trendUp ? ( {card.trendUp ? (
<ArrowUpRight className="w-3 h-3" /> <ArrowUpRight className="w-3 h-3" />
) : ( ) : (
@ -183,13 +189,13 @@ export default function Dashboard() {
</div> </div>
<div> <div>
<h3 className="text-xs font-medium text-rose-600 mb-1 sm:mb-2 uppercase tracking-wider"> <h3 className={`text-xs font-medium mb-1 sm:mb-2 uppercase tracking-wider ${isDark ? "text-rose-300" : "text-rose-600"}`}>
{card.title} {card.title}
</h3> </h3>
<p className="text-xl sm:text-2xl font-bold text-gray-900 mb-1"> <p className={`text-xl sm:text-2xl font-bold mb-1 ${isDark ? "text-white" : "text-gray-900"}`}>
{card.value} {card.value}
</p> </p>
<p className="text-xs text-gray-500"> <p className={`text-xs ${isDark ? "text-gray-400" : "text-gray-500"}`}>
vs last month vs last month
</p> </p>
</div> </div>

View File

@ -2,6 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { Bell } from "lucide-react"; import { Bell } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
interface Notification { interface Notification {
id: string; id: string;
@ -51,16 +52,18 @@ export default function NotificationsPage() {
]); ]);
const unreadCount = notifications.filter((n) => !n.read).length; const unreadCount = notifications.filter((n) => !n.read).length;
const { theme } = useAppTheme();
const isDark = theme === "dark";
return ( return (
<div className="min-h-screen bg-gray-50"> <div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
{/* Main Content */} {/* Main Content */}
<main className="p-3 sm:p-4 md:p-6 lg:p-8"> <main className="p-3 sm:p-4 md:p-6 lg:p-8">
{/* Page Header */} {/* Page Header */}
<div className="mb-4 sm:mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4"> <div className="mb-4 sm:mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Bell className="w-5 h-5 sm:w-6 sm:h-6 text-gray-900" /> <Bell className={`w-5 h-5 sm:w-6 sm:h-6 ${isDark ? "text-white" : "text-gray-900"}`} />
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900"> <h1 className={`text-xl sm:text-2xl font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
Notifications Notifications
</h1> </h1>
{unreadCount > 0 && ( {unreadCount > 0 && (
@ -72,26 +75,30 @@ export default function NotificationsPage() {
</div> </div>
{/* Notifications List */} {/* Notifications List */}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden"> <div className={`rounded-lg border overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
{notifications.length === 0 ? ( {notifications.length === 0 ? (
<div className="p-8 sm:p-12 text-center text-gray-500"> <div className={`p-8 sm:p-12 text-center ${isDark ? "text-gray-400" : "text-gray-500"}`}>
<Bell className="w-12 h-12 mx-auto mb-2 text-gray-300" /> <Bell className={`w-12 h-12 mx-auto mb-2 ${isDark ? "text-gray-600" : "text-gray-300"}`} />
<p className="text-sm">No notifications</p> <p className="text-sm">No notifications</p>
</div> </div>
) : ( ) : (
<div className="divide-y"> <div className={`divide-y ${isDark ? "divide-gray-700" : ""}`}>
{notifications.map((notification) => { {notifications.map((notification) => {
return ( return (
<div <div
key={notification.id} key={notification.id}
className={`p-4 sm:p-6 hover:bg-gray-50 transition-colors cursor-pointer ${ className={`p-4 sm:p-6 transition-colors cursor-pointer ${
!notification.read ? "bg-rose-50/50" : "" !notification.read
? isDark
? "bg-rose-500/10"
: "bg-rose-50/50"
: ""
}`} }`}
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p <p
className={`text-sm sm:text-base font-medium text-gray-900 ${ className={`text-sm sm:text-base font-medium ${isDark ? "text-white" : "text-gray-900"} ${
!notification.read ? "font-semibold" : "" !notification.read ? "font-semibold" : ""
}`} }`}
> >
@ -101,10 +108,10 @@ export default function NotificationsPage() {
<span className="shrink-0 w-2 h-2 bg-green-500 rounded-full mt-1"></span> <span className="shrink-0 w-2 h-2 bg-green-500 rounded-full mt-1"></span>
)} )}
</div> </div>
<p className="text-sm text-gray-600 mt-1"> <p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-600"}`}>
{notification.message} {notification.message}
</p> </p>
<p className="text-xs text-gray-400 mt-1"> <p className={`text-xs mt-1 ${isDark ? "text-gray-500" : "text-gray-400"}`}>
{notification.time} {notification.time}
</p> </p>
</div> </div>

View File

@ -0,0 +1,310 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import {
User,
Mail,
Phone,
Save,
ArrowLeft,
Lock,
Eye,
EyeOff,
} from "lucide-react";
import Link from "next/link";
import { useAppTheme } from "@/components/ThemeProvider";
export default function AdminSettingsPage() {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
fullName: "Hammond",
email: "admin@attuneheart.com",
phone: "+1 (555) 123-4567",
});
const [passwordData, setPasswordData] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
const [showPasswords, setShowPasswords] = useState({
current: false,
new: false,
confirm: false,
});
const { theme } = useAppTheme();
const isDark = theme === "dark";
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handlePasswordChange = (field: string, value: string) => {
setPasswordData((prev) => ({
...prev,
[field]: value,
}));
};
const togglePasswordVisibility = (field: "current" | "new" | "confirm") => {
setShowPasswords((prev) => ({
...prev,
[field]: !prev[field],
}));
};
const handleSave = async () => {
setLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoading(false);
// In a real app, you would show a success message here
};
const handlePasswordSave = async () => {
if (passwordData.newPassword !== passwordData.confirmPassword) {
// In a real app, you would show an error message here
alert("New passwords do not match");
return;
}
if (passwordData.newPassword.length < 8) {
// In a real app, you would show an error message here
alert("Password must be at least 8 characters long");
return;
}
setLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoading(false);
// Reset password fields
setPasswordData({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
// In a real app, you would show a success message here
};
return (
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
{/* Main Content */}
<main className="p-4 sm:p-6 lg:p-8 space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div className="flex items-center gap-4">
<Link href="/admin/dashboard">
<Button variant="ghost" size="icon" className={isDark ? "hover:bg-gray-800 text-gray-300" : "hover:bg-gray-100"}>
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<div>
<h1 className={`text-2xl font-semibold mb-1 ${isDark ? "text-white" : "text-gray-900"}`}>
Settings
</h1>
<p className={`text-sm ${isDark ? "text-gray-400" : "text-gray-600"}`}>
Manage your account settings and practice information
</p>
</div>
</div>
<Button
onClick={handleSave}
disabled={loading}
className="w-full sm:w-auto bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save Changes
</Button>
</div>
<div className="max-w-4xl mx-auto">
<div className="space-y-6">
{/* Profile Information */}
<Card className={isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}>
<CardHeader>
<div className="flex items-center gap-2">
<User className={`w-5 h-5 ${isDark ? "text-gray-300" : "text-gray-600"}`} />
<CardTitle className={isDark ? "text-white" : "text-gray-900"}>Profile Information</CardTitle>
</div>
<CardDescription className={isDark ? "text-gray-400" : "text-gray-600"}>
Update your personal information and contact details
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Full Name
</label>
<div className="relative">
<User className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<Input
type="text"
value={formData.fullName}
onChange={(e) => handleInputChange("fullName", e.target.value)}
className={`pl-10 ${isDark ? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400" : ""}`}
placeholder="Enter your full name"
/>
</div>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Email Address
</label>
<div className="relative">
<Mail className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<Input
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
className={`pl-10 ${isDark ? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400" : ""}`}
placeholder="Enter your email"
/>
</div>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Phone Number
</label>
<div className="relative">
<Phone className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<Input
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)}
className={`pl-10 ${isDark ? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400" : ""}`}
placeholder="Enter your phone number"
/>
</div>
</div>
</CardContent>
</Card>
{/* Change Password */}
<Card className={isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}>
<CardHeader>
<div className="flex items-center gap-2">
<Lock className={`w-5 h-5 ${isDark ? "text-gray-300" : "text-gray-600"}`} />
<CardTitle className={isDark ? "text-white" : "text-gray-900"}>Change Password</CardTitle>
</div>
<CardDescription className={isDark ? "text-gray-400" : "text-gray-600"}>
Update your password to keep your account secure
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Current Password
</label>
<div className="relative">
<Lock className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<Input
type={showPasswords.current ? "text" : "password"}
value={passwordData.currentPassword}
onChange={(e) => handlePasswordChange("currentPassword", e.target.value)}
className={`pl-10 pr-10 ${isDark ? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400" : ""}`}
placeholder="Enter your current password"
/>
<button
type="button"
onClick={() => togglePasswordVisibility("current")}
className={`absolute right-3 top-1/2 transform -translate-y-1/2 ${isDark ? "text-gray-400 hover:text-gray-300" : "text-gray-400 hover:text-gray-600"}`}
>
{showPasswords.current ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
New Password
</label>
<div className="relative">
<Lock className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<Input
type={showPasswords.new ? "text" : "password"}
value={passwordData.newPassword}
onChange={(e) => handlePasswordChange("newPassword", e.target.value)}
className={`pl-10 pr-10 ${isDark ? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400" : ""}`}
placeholder="Enter your new password"
/>
<button
type="button"
onClick={() => togglePasswordVisibility("new")}
className={`absolute right-3 top-1/2 transform -translate-y-1/2 ${isDark ? "text-gray-400 hover:text-gray-300" : "text-gray-400 hover:text-gray-600"}`}
>
{showPasswords.new ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
<p className={`text-xs ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Password must be at least 8 characters long
</p>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? "text-gray-300" : "text-gray-700"}`}>
Confirm New Password
</label>
<div className="relative">
<Lock className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? "text-gray-500" : "text-gray-400"}`} />
<Input
type={showPasswords.confirm ? "text" : "password"}
value={passwordData.confirmPassword}
onChange={(e) => handlePasswordChange("confirmPassword", e.target.value)}
className={`pl-10 pr-10 ${isDark ? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400" : ""}`}
placeholder="Confirm your new password"
/>
<button
type="button"
onClick={() => togglePasswordVisibility("confirm")}
className={`absolute right-3 top-1/2 transform -translate-y-1/2 ${isDark ? "text-gray-400 hover:text-gray-300" : "text-gray-400 hover:text-gray-600"}`}
>
{showPasswords.confirm ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
</div>
<div className="pt-2">
<Button
onClick={handlePasswordSave}
disabled={loading || !passwordData.currentPassword || !passwordData.newPassword || !passwordData.confirmPassword}
className="bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
) : (
<Lock className="w-4 h-4 mr-2" />
)}
Update Password
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</main>
</div>
);
}

View File

@ -7,8 +7,11 @@ import { Heart, Eye, EyeOff, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useAppTheme } from "@/components/ThemeProvider";
export default function Login() { export default function Login() {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
const router = useRouter(); const router = useRouter();
@ -18,8 +21,8 @@ export default function Login() {
{/* Background Image */} {/* Background Image */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<Image <Image
src="/doctors.png" src="/woman.jpg"
alt="Medical professionals" alt="Therapy and counseling session with African American clients"
fill fill
className="object-cover object-center" className="object-cover object-center"
priority priority
@ -38,52 +41,56 @@ export default function Login() {
{/* Centered White Card - Login Form */} {/* Centered White Card - Login Form */}
<div className="relative z-20 w-full max-w-md bg-white rounded-2xl shadow-2xl p-8"> <div className={`relative z-20 w-full max-w-md rounded-2xl shadow-2xl p-8 ${isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white'}`}>
{/* Header with Close Button */}
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
{/* Heading */}
<h1 className="text-3xl font-bold bg-gradient-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2">
Welcome back
</h1>
{/* Sign Up Prompt */}
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
New to Attune Heart Therapy?{" "}
<Link href="/signup" className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}>
Sign up
</Link>
</p>
</div>
{/* Close Button */} {/* Close Button */}
<Button <Button
onClick={() => router.back()} onClick={() => router.back()}
variant="ghost" variant="ghost"
size="icon" size="icon"
className="ml-auto mb-6 w-8 h-8 rounded-full" className={`flex-shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
aria-label="Close" aria-label="Close"
> >
<X className="w-5 h-5" />
</Button> </Button>
</div>
{/* Heading */}
<h1 className="text-3xl font-bold bg-linear-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2">
Welcome back
</h1>
{/* Sign Up Prompt */}
<p className="text-gray-600 mb-8">
New to Attune Heart Therapy?{" "}
<Link href="/signup" className="text-blue-600 underline font-medium">
Sign up
</Link>
</p>
{/* Login Form */} {/* Login Form */}
<form className="space-y-6" onSubmit={(e) => { <form className="space-y-6" onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
router.push("/dashboard"); router.push("/");
}}> }}>
{/* Email Field */} {/* Email Field */}
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium text-black"> <label htmlFor="email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address Email address
</label> </label>
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="Email address" placeholder="Email address"
className="h-12 bg-white border-gray-300" className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
</div> </div>
{/* Password Field */} {/* Password Field */}
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium text-black"> <label htmlFor="password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Your password Your password
</label> </label>
<div className="relative"> <div className="relative">
@ -91,7 +98,7 @@ export default function Login() {
id="password" id="password"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder="Your password" placeholder="Your password"
className="h-12 bg-white border-gray-300 pr-12" className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
<Button <Button
@ -99,7 +106,7 @@ export default function Login() {
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 text-gray-500 hover:text-gray-700" className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
aria-label={showPassword ? "Hide password" : "Show password"} aria-label={showPassword ? "Hide password" : "Show password"}
> >
{showPassword ? ( {showPassword ? (
@ -114,7 +121,7 @@ export default function Login() {
{/* Submit Button */} {/* Submit Button */}
<Button <Button
type="submit" type="submit"
className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all" className="w-full h-12 text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all"
> >
Log in Log in
</Button> </Button>
@ -126,13 +133,13 @@ export default function Login() {
type="checkbox" type="checkbox"
checked={rememberMe} checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)} onChange={(e) => setRememberMe(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-rose-600 focus:ring-2 focus:ring-rose-500 cursor-pointer" className={`w-4 h-4 rounded text-rose-600 focus:ring-2 focus:ring-rose-500 cursor-pointer ${isDark ? 'border-gray-600 bg-gray-700' : 'border-gray-300'}`}
/> />
<span className="text-black">Remember me</span> <span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span>
</label> </label>
<Link <Link
href="/forgot-password" href="/forgot-password"
className="text-blue-600 hover:text-blue-700 font-medium" className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
> >
Forgot password? Forgot password?
</Link> </Link>

View File

@ -0,0 +1,680 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAppTheme } from "@/components/ThemeProvider";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Calendar,
Clock,
User,
Mail,
Phone,
MessageSquare,
ArrowLeft,
Heart,
CheckCircle2,
CheckCircle,
Loader2,
} from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { LoginDialog } from "@/components/LoginDialog";
interface User {
ID: number;
CreatedAt?: string;
UpdatedAt?: string;
DeletedAt?: string | null;
first_name: string;
last_name: string;
email: string;
phone: string;
location: string;
date_of_birth?: string;
is_admin?: boolean;
bookings?: null;
}
interface Booking {
ID: number;
CreatedAt: string;
UpdatedAt: string;
DeletedAt: string | null;
user_id: number;
user: User;
scheduled_at: string;
duration: number;
status: string;
jitsi_room_id: string;
jitsi_room_url: string;
payment_id: string;
payment_status: string;
amount: number;
notes: string;
}
interface BookingsResponse {
bookings: Booking[];
limit: number;
offset: number;
total: number;
}
export default function BookNowPage() {
const router = useRouter();
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
preferredDays: [] as string[],
preferredTimes: [] as string[],
message: "",
});
const [loading, setLoading] = useState(false);
const [booking, setBooking] = useState<Booking | null>(null);
const [error, setError] = useState<string | null>(null);
const [showLoginDialog, setShowLoginDialog] = useState(false);
// Handle submit button click
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Open login dialog instead of submitting directly
setShowLoginDialog(true);
};
const handleLoginSuccess = async () => {
// After successful login, proceed with booking submission
await submitBooking();
};
const submitBooking = async () => {
setLoading(true);
setError(null);
try {
if (formData.preferredDays.length === 0) {
setError("Please select at least one available day.");
setLoading(false);
return;
}
if (formData.preferredTimes.length === 0) {
setError("Please select at least one preferred time.");
setLoading(false);
return;
}
// For now, we'll use the first selected day and first selected time
// This can be adjusted based on your backend requirements
const firstDay = formData.preferredDays[0];
const firstTime = formData.preferredTimes[0];
const timeMap: { [key: string]: string } = {
morning: "09:00",
lunchtime: "12:00",
afternoon: "14:00",
};
const time24 = timeMap[firstTime] || "09:00";
// Get next occurrence of the first selected day
const today = new Date();
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const targetDayIndex = days.indexOf(firstDay);
let daysUntilTarget = (targetDayIndex - today.getDay() + 7) % 7;
if (daysUntilTarget === 0) daysUntilTarget = 7; // Next week if today
const targetDate = new Date(today);
targetDate.setDate(today.getDate() + daysUntilTarget);
const dateString = targetDate.toISOString().split("T")[0];
// Combine date and time into scheduled_at (ISO format)
const dateTimeString = `${dateString}T${time24}:00Z`;
// Prepare request payload
const payload = {
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
phone: formData.phone,
scheduled_at: dateTimeString,
duration: 60, // Default to 60 minutes
preferred_days: formData.preferredDays,
preferred_times: formData.preferredTimes,
notes: formData.message || "",
};
// Simulate API call - Replace with actual API endpoint
const response = await fetch("/api/bookings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).catch(() => {
// Fallback to mock data if API is not available
return null;
});
let bookingData: Booking;
if (response && response.ok) {
const data: BookingsResponse = await response.json();
bookingData = data.bookings[0];
} else {
// Mock response for development - matches the API structure provided
await new Promise((resolve) => setTimeout(resolve, 1000));
bookingData = {
ID: Math.floor(Math.random() * 1000),
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString(),
DeletedAt: null,
user_id: 1,
user: {
ID: 1,
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString(),
DeletedAt: null,
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
phone: formData.phone,
location: "",
date_of_birth: "0001-01-01T00:00:00Z",
is_admin: false,
bookings: null,
},
scheduled_at: dateTimeString,
duration: 60,
status: "scheduled",
jitsi_room_id: `booking-${Math.floor(Math.random() * 1000)}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
jitsi_room_url: `https://meet.jit.si/booking-${Math.floor(Math.random() * 1000)}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
payment_id: "",
payment_status: "pending",
amount: 52,
notes: formData.message || "Initial consultation session",
};
}
setBooking(bookingData);
setLoading(false);
// Redirect to home after 2 seconds
setTimeout(() => {
router.push("/");
}, 2000);
} catch (err) {
setError("Failed to submit booking. Please try again.");
setLoading(false);
console.error("Booking error:", err);
}
};
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleDayToggle = (day: string) => {
setFormData((prev) => {
const days = prev.preferredDays.includes(day)
? prev.preferredDays.filter((d) => d !== day)
: [...prev.preferredDays, day];
return { ...prev, preferredDays: days };
});
};
const handleTimeToggle = (time: string) => {
setFormData((prev) => {
const times = prev.preferredTimes.includes(time)
? prev.preferredTimes.filter((t) => t !== time)
: [...prev.preferredTimes, time];
return { ...prev, preferredTimes: times };
});
};
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
return (
<div className={`min-h-screen ${isDark ? 'bg-gray-900' : 'bg-white'}`}>
{/* Main Content */}
<main className="min-h-screen flex">
{/* Left Side - Image (Fixed) */}
<div className={`hidden lg:block fixed top-0 left-0 h-screen w-1/2 overflow-hidden z-10 bg-gradient-to-br ${isDark ? 'from-gray-900 via-gray-800 to-gray-900' : 'from-rose-100 via-pink-50 to-orange-50'}`}>
<div className="absolute inset-0">
<Image
src="/session.jpg"
alt="Therapy session with diverse clients"
fill
className="object-cover"
priority
sizes="50vw"
/>
<div className="absolute inset-0 bg-black/50"></div>
</div>
{/* Logo at Top */}
<div className="absolute top-0 left-0 right-0 z-20 flex items-center p-6">
<Link href="/" className="flex items-center gap-2">
<div className="bg-gradient-to-r 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 drop-shadow-lg ${isDark ? 'text-rose-400' : 'text-rose-500'}`}>
Attune Heart Therapy
</span>
</Link>
</div>
{/* Overlay Content - Lower Position */}
<div className="relative z-10 w-full h-full flex items-end justify-center px-12 pb-20">
<div className="space-y-4 text-center max-w-sm">
<h2 className="text-xl md:text-2xl font-bold leading-tight text-white drop-shadow-lg">
Begin Your Journey to Wellness
</h2>
<p className="text-sm md:text-base text-white/95 leading-relaxed drop-shadow-md">
Take the first step towards healing and growth. Our compassionate team is here to support you every step of the way.
</p>
{/* Features List */}
<div className="space-y-2 pt-3">
<div className="flex items-center justify-center gap-2">
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
</div>
<span className="text-white/95 text-xs md:text-sm">Safe and confidential environment</span>
</div>
<div className="flex items-center justify-center gap-2">
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
</div>
<span className="text-white/95 text-xs md:text-sm">Experienced licensed therapists</span>
</div>
<div className="flex items-center justify-center gap-2">
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
</div>
<span className="text-white/95 text-xs md:text-sm">Personalized treatment plans</span>
</div>
<div className="flex items-center justify-center gap-2">
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
</div>
<span className="text-white/95 text-xs md:text-sm">Flexible scheduling options</span>
</div>
</div>
</div>
</div>
</div>
{/* Right Side - Form (Scrollable) */}
<div className={`w-full lg:w-1/2 lg:ml-auto fixed top-0 right-0 h-screen overflow-y-auto custom-scrollbar ${isDark ? 'bg-gray-900' : 'bg-white'}`}>
<div className="flex items-start justify-center min-h-full">
<div className="w-full max-w-2xl">
{/* Page Header */}
<div className="pt-4 sm:pt-6 lg:pt-8 px-4 sm:px-6 lg:px-12 pb-4 sm:pb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className={`flex items-center gap-2 mb-3 sm:mb-4 ${isDark ? 'text-white hover:bg-gray-800' : 'text-black hover:bg-gray-100'}`}
>
<ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="hidden sm:inline text-sm sm:text-base">Back</span>
</Button>
<div>
<h1 className={`text-xl sm:text-2xl font-semibold mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Book Your Appointment
</h1>
<p className={`text-xs sm:text-sm ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
Fill out the form below and we'll get back to you to confirm your appointment
</p>
</div>
</div>
{/* Booking Form or Success Message */}
<div className="px-4 sm:px-6 lg:px-12 pb-6 sm:pb-8 lg:pb-12">
{booking ? (
<div className={`rounded-xl sm:rounded-2xl shadow-lg p-4 sm:p-6 lg:p-8 border ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
<div className="text-center space-y-4">
<div className={`mx-auto w-16 h-16 rounded-full flex items-center justify-center ${isDark ? 'bg-green-900/30' : 'bg-green-100'}`}>
<CheckCircle className={`w-8 h-8 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
</div>
<div>
<h2 className={`text-2xl font-semibold mb-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Booking Confirmed!
</h2>
<p className={isDark ? 'text-gray-300' : 'text-gray-600'}>
Your appointment has been successfully booked.
</p>
</div>
<div className={`rounded-lg p-6 space-y-4 text-left ${isDark ? 'bg-gray-700/50' : 'bg-gray-50'}`}>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Booking ID</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>#{booking.ID}</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Patient</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>
{booking.user.first_name} {booking.user.last_name}
</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Scheduled Time</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(booking.scheduled_at)}</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Duration</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>{booking.duration} minutes</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Status</p>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${isDark ? 'bg-blue-900/50 text-blue-200' : 'bg-blue-100 text-blue-800'}`}>
{booking.status}
</span>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Amount</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>${booking.amount}</p>
</div>
{booking.notes && (
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Notes</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>{booking.notes}</p>
</div>
)}
</div>
<div className="pt-4 flex flex-col sm:flex-row gap-3 justify-center">
<Button
onClick={() => {
setBooking(null);
setFormData({
firstName: "",
lastName: "",
email: "",
phone: "",
preferredDays: [],
preferredTimes: [],
message: "",
});
}}
variant="outline"
>
Book Another Appointment
</Button>
<Button
onClick={() => router.push("/")}
className="bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
Return to Home
</Button>
</div>
</div>
</div>
) : (
<>
<div className={`rounded-xl sm:rounded-2xl shadow-lg p-4 sm:p-6 lg:p-8 border ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
{error && (
<div className={`mb-6 p-4 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Personal Information Section */}
<div className="space-y-4">
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
<User className={`w-5 h-5 ${isDark ? 'text-rose-400' : 'text-rose-600'}`} />
Personal Information
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<label
htmlFor="firstName"
className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
First Name *
</label>
<Input
id="firstName"
type="text"
placeholder="John"
value={formData.firstName}
onChange={(e) =>
handleChange("firstName", e.target.value)
}
required
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
/>
</div>
<div className="space-y-2">
<label
htmlFor="lastName"
className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
Last Name *
</label>
<Input
id="lastName"
type="text"
placeholder="Doe"
value={formData.lastName}
onChange={(e) =>
handleChange("lastName", e.target.value)
}
required
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
/>
</div>
</div>
<div className="space-y-2">
<label
htmlFor="email"
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Mail className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Email Address *
</label>
<Input
id="email"
type="email"
placeholder="john.doe@example.com"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
required
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
/>
</div>
<div className="space-y-2">
<label
htmlFor="phone"
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Phone className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Phone Number *
</label>
<Input
id="phone"
type="tel"
placeholder="+1 (555) 123-4567"
value={formData.phone}
onChange={(e) => handleChange("phone", e.target.value)}
required
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
/>
</div>
</div>
{/* Appointment Details Section */}
<div className={`space-y-4 pt-6 border-t ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
<Calendar className={`w-5 h-5 ${isDark ? 'text-rose-400' : 'text-rose-600'}`} />
Appointment Details
</h2>
<div className="space-y-4">
<div className="space-y-2">
<label
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Available Days *
</label>
<div className="flex flex-wrap gap-3">
{['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'].map((day) => (
<label
key={day}
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
formData.preferredDays.includes(day)
? isDark
? 'bg-rose-600 border-rose-500 text-white'
: 'bg-rose-500 border-rose-500 text-white'
: isDark
? 'bg-gray-700 border-gray-600 text-gray-300 hover:border-rose-500'
: 'bg-white border-gray-300 text-gray-700 hover:border-rose-500'
}`}
>
<input
type="checkbox"
checked={formData.preferredDays.includes(day)}
onChange={() => handleDayToggle(day)}
className="sr-only"
/>
<span className="text-sm font-medium">{day}</span>
</label>
))}
</div>
</div>
<div className="space-y-2">
<label
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Clock className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Preferred Time *
</label>
<div className="flex flex-wrap gap-3">
{[
{ value: 'morning', label: 'Morning' },
{ value: 'lunchtime', label: 'Lunchtime' },
{ value: 'afternoon', label: 'Afternoon' }
].map((time) => (
<label
key={time.value}
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
formData.preferredTimes.includes(time.value)
? isDark
? 'bg-rose-600 border-rose-500 text-white'
: 'bg-rose-500 border-rose-500 text-white'
: isDark
? 'bg-gray-700 border-gray-600 text-gray-300 hover:border-rose-500'
: 'bg-white border-gray-300 text-gray-700 hover:border-rose-500'
}`}
>
<input
type="checkbox"
checked={formData.preferredTimes.includes(time.value)}
onChange={() => handleTimeToggle(time.value)}
className="sr-only"
/>
<span className="text-sm font-medium">{time.label}</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* Additional Message Section */}
<div className={`space-y-4 pt-6 border-t ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
<label
htmlFor="message"
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<MessageSquare className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Additional Message (Optional)
</label>
<textarea
id="message"
rows={4}
placeholder="Tell us about any specific concerns or preferences..."
value={formData.message}
onChange={(e) => handleChange("message", e.target.value)}
className={`w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-rose-500 focus-visible:border-rose-500 disabled:cursor-not-allowed disabled:opacity-50 ${isDark ? 'border-gray-600 bg-gray-700 text-white placeholder:text-gray-400 focus-visible:ring-rose-400 focus-visible:border-rose-400' : 'border-gray-300 bg-white text-gray-900 placeholder:text-gray-500'}`}
/>
</div>
{/* Submit Button */}
<div className="pt-6">
<Button
type="submit"
size="lg"
disabled={loading}
className="w-full bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all h-12 text-base font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
"Request Appointment"
)}
</Button>
<p className={`text-xs text-center mt-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
We'll review your request and get back to you within 24 hours
to confirm your appointment.
</p>
</div>
</form>
</div>
{/* Contact Information */}
<div className="mt-6 text-center">
<p className={isDark ? 'text-gray-300' : 'text-gray-600'}>
Prefer to book by phone?{" "}
<a
href="tel:+17548162311"
className={`font-medium underline ${isDark ? 'text-rose-400 hover:text-rose-300' : 'text-rose-600 hover:text-rose-700'}`}
>
Call us at (754) 816-2311
</a>
</p>
</div>
</>
)}
</div>
</div>
</div>
</div>
</main>
{/* Login Dialog */}
<LoginDialog
open={showLoginDialog}
onOpenChange={setShowLoginDialog}
onLoginSuccess={handleLoginSuccess}
/>
</div>
);
}

8
app/(pages)/layout.tsx Normal file
View File

@ -0,0 +1,8 @@
export default function PagesLayout({ children }: { children: React.ReactNode }) {
return (
<div>
{children}
</div>
)
}

8
app/(user)/layout.tsx Normal file
View File

@ -0,0 +1,8 @@
export default function UserLayout({ children }: { children: React.ReactNode }) {
return (
<div>
{children}
</div>
)
}

View File

@ -0,0 +1,345 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import {
Calendar,
Clock,
User,
Mail,
Phone,
Heart,
CalendarPlus,
Video,
CheckCircle2,
XCircle,
CalendarCheck,
ArrowUpRight,
Settings,
} from "lucide-react";
import Link from "next/link";
import { Navbar } from "@/components/Navbar";
import { useAppTheme } from "@/components/ThemeProvider";
interface Booking {
ID: number;
scheduled_at: string;
duration: number;
status: string;
amount: number;
notes: string;
jitsi_room_url?: string;
}
export default function UserDashboard() {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [bookings, setBookings] = useState<Booking[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate API call to fetch user bookings
const fetchBookings = async () => {
setLoading(true);
try {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500));
// Mock data - in real app, this would fetch from API
const mockBookings: Booking[] = [
{
ID: 1,
scheduled_at: "2025-01-15T10:00:00Z",
duration: 60,
status: "scheduled",
amount: 150,
notes: "Initial consultation",
jitsi_room_url: "https://meet.jit.si/sample-room",
},
];
setBookings(mockBookings);
} catch (error) {
console.error("Failed to fetch bookings:", error);
} finally {
setLoading(false);
}
};
fetchBookings();
}, []);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
};
const upcomingBookings = bookings.filter(
(booking) => booking.status === "scheduled"
);
const completedBookings = bookings.filter(
(booking) => booking.status === "completed"
);
const cancelledBookings = bookings.filter(
(booking) => booking.status === "cancelled"
);
const statCards = [
{
title: "Upcoming Appointments",
value: upcomingBookings.length,
icon: CalendarCheck,
trend: "+2",
trendUp: true,
},
{
title: "Completed Sessions",
value: completedBookings.length,
icon: CheckCircle2,
trend: "+5",
trendUp: true,
},
{
title: "Total Appointments",
value: bookings.length,
icon: Calendar,
trend: "+12%",
trendUp: true,
},
{
title: "Total Spent",
value: `$${bookings.reduce((sum, b) => sum + b.amount, 0)}`,
icon: Heart,
trend: "+18%",
trendUp: true,
},
];
return (
<div className={`min-h-screen ${isDark ? 'bg-gray-900' : 'bg-gray-50'}`}>
<Navbar />
{/* Main Content */}
<main className="container mx-auto px-4 sm:px-6 lg:px-8 space-y-6 pt-20 sm:pt-24 pb-8">
{/* Welcome Section */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div>
<h1 className={`text-2xl font-semibold mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Welcome Back!
</h1>
<p className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Here's an overview of your appointments
</p>
</div>
<div className="flex items-center gap-3">
<Link href="/user/settings" className="flex-1 sm:flex-initial">
<Button
variant="outline"
className={`w-full sm:w-auto ${isDark ? 'hover:bg-gray-800 border-gray-700 text-gray-300' : 'hover:bg-gray-100'}`}
>
<Settings className="w-4 h-4 mr-2" />
Settings
</Button>
</Link>
<Link href="/book-now" className="flex-1 sm:flex-initial">
<Button
className="w-full sm:w-auto bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
<CalendarPlus className="w-4 h-4 mr-2" />
Book Appointment
</Button>
</Link>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className={`animate-spin rounded-full h-8 w-8 border-b-2 ${isDark ? 'border-gray-600' : 'border-gray-400'}`}></div>
</div>
) : (
<>
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
{statCards.map((card, index) => {
const Icon = card.icon;
return (
<div
key={index}
className={`rounded-lg border p-4 sm:p-5 md:p-6 hover:shadow-md transition-shadow ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}
>
<div className="flex items-start justify-between mb-3 sm:mb-4">
<div className={`p-2 sm:p-2.5 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
<Icon className={`w-4 h-4 sm:w-5 sm:h-5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
</div>
<div
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
card.trendUp
? isDark ? "bg-green-900/30 text-green-400" : "bg-green-50 text-green-700"
: isDark ? "bg-red-900/30 text-red-400" : "bg-red-50 text-red-700"
}`}
>
{card.trendUp ? (
<ArrowUpRight className="w-3 h-3" />
) : (
<ArrowUpRight className="w-3 h-3" />
)}
<span>{card.trend}</span>
</div>
</div>
<div>
<h3 className={`text-xs font-medium mb-1 sm:mb-2 uppercase tracking-wider ${isDark ? 'text-rose-400' : 'text-rose-600'}`}>
{card.title}
</h3>
<p className={`text-xl sm:text-2xl font-bold mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
{card.value}
</p>
<p className={`text-xs font-medium ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>vs last month</p>
</div>
</div>
);
})}
</div>
{/* Upcoming Appointments Section */}
{upcomingBookings.length > 0 && (
<div className={`rounded-lg border p-4 sm:p-5 md:p-6 ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
<h2 className={`text-lg font-semibold mb-4 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Upcoming Appointments
</h2>
<div className="space-y-3">
{upcomingBookings.map((booking) => (
<div
key={booking.ID}
className={`border rounded-lg p-4 hover:shadow-md transition-shadow ${isDark ? 'border-gray-700 bg-gray-700/50' : 'border-gray-200'}`}
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
<span className={`font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{formatDate(booking.scheduled_at)}
</span>
</div>
<div className="flex items-center gap-2 mb-2">
<Clock className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
<span className={isDark ? 'text-gray-300' : 'text-gray-700'}>
{formatTime(booking.scheduled_at)}
</span>
<span className={`text-sm font-medium ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
({booking.duration} minutes)
</span>
</div>
{booking.notes && (
<p className={`text-sm mt-2 font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
{booking.notes}
</p>
)}
</div>
<div className="flex flex-col sm:items-end gap-3">
<div className="flex items-center gap-2">
<span className={`px-3 py-1 rounded-full text-sm font-medium ${isDark ? 'bg-green-900/30 text-green-400' : 'bg-green-50 text-green-700'}`}>
{booking.status.charAt(0).toUpperCase() +
booking.status.slice(1)}
</span>
<span className={`text-lg font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}>
${booking.amount}
</span>
</div>
{booking.jitsi_room_url && (
<a
href={booking.jitsi_room_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors"
>
<Video className="w-4 h-4" />
Join Session
</a>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Account Information */}
<div className={`rounded-lg border p-4 sm:p-5 md:p-6 ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
<h2 className={`text-lg font-semibold mb-4 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Account Information
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
<User className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Full Name
</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
John Doe
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
<Mail className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Email
</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
john.doe@example.com
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
<Phone className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Phone
</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
+1 (555) 123-4567
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-50'}`}>
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Member Since
</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
January 2025
</p>
</div>
</div>
</div>
</div>
</>
)}
</main>
</div>
);
}

View File

@ -0,0 +1,313 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import {
User,
Mail,
Phone,
Save,
ArrowLeft,
Lock,
Eye,
EyeOff,
} from "lucide-react";
import Link from "next/link";
import { Navbar } from "@/components/Navbar";
import { useAppTheme } from "@/components/ThemeProvider";
export default function SettingsPage() {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
fullName: "John Doe",
email: "john.doe@example.com",
phone: "+1 (555) 123-4567",
});
const [passwordData, setPasswordData] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
const [showPasswords, setShowPasswords] = useState({
current: false,
new: false,
confirm: false,
});
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handlePasswordChange = (field: string, value: string) => {
setPasswordData((prev) => ({
...prev,
[field]: value,
}));
};
const togglePasswordVisibility = (field: "current" | "new" | "confirm") => {
setShowPasswords((prev) => ({
...prev,
[field]: !prev[field],
}));
};
const handleSave = async () => {
setLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoading(false);
// In a real app, you would show a success message here
};
const handlePasswordSave = async () => {
if (passwordData.newPassword !== passwordData.confirmPassword) {
// In a real app, you would show an error message here
alert("New passwords do not match");
return;
}
if (passwordData.newPassword.length < 8) {
// In a real app, you would show an error message here
alert("Password must be at least 8 characters long");
return;
}
setLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoading(false);
// Reset password fields
setPasswordData({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
// In a real app, you would show a success message here
};
return (
<div className={`min-h-screen ${isDark ? 'bg-gray-900' : 'bg-gray-50'}`}>
<Navbar />
{/* Main Content */}
<main className="container mx-auto px-4 sm:px-6 lg:px-8 space-y-6 pt-20 sm:pt-24 pb-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div className="flex items-center gap-4">
<Link href="/user/dashboard">
<Button variant="ghost" size="icon" className={isDark ? 'hover:bg-gray-800 text-gray-300' : 'hover:bg-gray-100'}>
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<div>
<h1 className={`text-2xl font-semibold mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Settings
</h1>
<p className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Manage your account settings and preferences
</p>
</div>
</div>
<Button
onClick={handleSave}
disabled={loading}
className="w-full sm:w-auto bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save Changes
</Button>
</div>
<div className="max-w-4xl mx-auto">
<div className="space-y-6">
{/* Profile Information */}
<Card className={isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}>
<CardHeader>
<div className="flex items-center gap-2">
<User className={`w-5 h-5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
<CardTitle className={isDark ? 'text-white' : 'text-gray-900'}>Profile Information</CardTitle>
</div>
<CardDescription className={isDark ? 'text-gray-400' : 'text-gray-600'}>
Update your personal information and contact details
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
Full Name
</label>
<div className="relative">
<User className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
<Input
type="text"
value={formData.fullName}
onChange={(e) => handleInputChange("fullName", e.target.value)}
className={`pl-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
placeholder="Enter your full name"
/>
</div>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
Email Address
</label>
<div className="relative">
<Mail className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
<Input
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
className={`pl-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
placeholder="Enter your email"
/>
</div>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
Phone Number
</label>
<div className="relative">
<Phone className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
<Input
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)}
className={`pl-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
placeholder="Enter your phone number"
/>
</div>
</div>
</CardContent>
</Card>
{/* Change Password */}
<Card className={isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}>
<CardHeader>
<div className="flex items-center gap-2">
<Lock className={`w-5 h-5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`} />
<CardTitle className={isDark ? 'text-white' : 'text-gray-900'}>Change Password</CardTitle>
</div>
<CardDescription className={isDark ? 'text-gray-400' : 'text-gray-600'}>
Update your password to keep your account secure
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
Current Password
</label>
<div className="relative">
<Lock className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
<Input
type={showPasswords.current ? "text" : "password"}
value={passwordData.currentPassword}
onChange={(e) => handlePasswordChange("currentPassword", e.target.value)}
className={`pl-10 pr-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
placeholder="Enter your current password"
/>
<button
type="button"
onClick={() => togglePasswordVisibility("current")}
className={`absolute right-3 top-1/2 transform -translate-y-1/2 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-400 hover:text-gray-600'}`}
>
{showPasswords.current ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
New Password
</label>
<div className="relative">
<Lock className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
<Input
type={showPasswords.new ? "text" : "password"}
value={passwordData.newPassword}
onChange={(e) => handlePasswordChange("newPassword", e.target.value)}
className={`pl-10 pr-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
placeholder="Enter your new password"
/>
<button
type="button"
onClick={() => togglePasswordVisibility("new")}
className={`absolute right-3 top-1/2 transform -translate-y-1/2 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-400 hover:text-gray-600'}`}
>
{showPasswords.new ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
<p className={`text-xs ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
Password must be at least 8 characters long
</p>
</div>
<div className="space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
Confirm New Password
</label>
<div className="relative">
<Lock className={`absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
<Input
type={showPasswords.confirm ? "text" : "password"}
value={passwordData.confirmPassword}
onChange={(e) => handlePasswordChange("confirmPassword", e.target.value)}
className={`pl-10 pr-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
placeholder="Confirm your new password"
/>
<button
type="button"
onClick={() => togglePasswordVisibility("confirm")}
className={`absolute right-3 top-1/2 transform -translate-y-1/2 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-400 hover:text-gray-600'}`}
>
{showPasswords.confirm ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
</div>
<div className="pt-2">
<Button
onClick={handlePasswordSave}
disabled={loading || !passwordData.currentPassword || !passwordData.newPassword || !passwordData.confirmPassword}
className="bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
) : (
<Lock className="w-4 h-4 mr-2" />
)}
Update Password
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</main>
</div>
);
}

View File

@ -91,4 +91,29 @@
.text-balance { .text-balance {
text-wrap: balance; text-wrap: balance;
} }
/* Custom Pink Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgb(255 228 230);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgb(244 63 94);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgb(225 29 72);
}
/* Firefox */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgb(244 63 94) rgb(255 228 230);
}
} }

15
app/icon.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 24 24">
<defs>
<linearGradient id="iconGradient" x1="4" y1="4" x2="20" y2="20" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#f43f5e" />
<stop offset="50%" stop-color="#ec4899" />
<stop offset="100%" stop-color="#fb923c" />
</linearGradient>
</defs>
<rect width="24" height="24" rx="6" fill="url(#iconGradient)" />
<path
d="M19.5 12.571 12 20 4.5 12.571a5.48 5.48 0 0 1 .12-7.77 5.34 5.34 0 0 1 7.74.208l.64.714.64-.714a5.34 5.34 0 0 1 7.74-.208 5.48 5.48 0 0 1 .12 7.77Z"
fill="#ffffff"
/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@ -9,6 +9,9 @@ const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Attune Heart Therapy | Nathalie Mac Guffie, LCSW | Miami, FL', 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.', description: 'Compassionate, evidence-based therapy in Miami, FL. Licensed Clinical Social Worker offering anxiety, depression, trauma therapy and more.',
icons: {
icon: '/icon.svg',
},
}; };
export default function RootLayout({ export default function RootLayout({

View File

@ -3,6 +3,10 @@ import { Footer } from "../components/Footer";
import { HeroSection } from "@/components/Hero"; import { HeroSection } from "@/components/Hero";
import { About } from "@/components/About"; import { About } from "@/components/About";
import { Services } from "@/components/Services"; import { Services } from "@/components/Services";
import { Specialties } from "@/components/Specialties";
import { ClientFocus } from "@/components/ClientFocus";
import { Finances } from "@/components/Finances";
import { Location } from "@/components/Location";
import { ContactSection } from "@/components/ContactSection"; import { ContactSection } from "@/components/ContactSection";
import { Navbar } from "../components/Navbar"; import { Navbar } from "../components/Navbar";
@ -17,6 +21,14 @@ export default function Home() {
<Services /> <Services />
<Specialties />
<ClientFocus />
<Finances />
<Location />
<ContactSection /> <ContactSection />
<Footer /> <Footer />

View File

@ -2,28 +2,15 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useInView } from "framer-motion"; import { useInView } from "framer-motion";
import { useRef, useEffect, useState } from "react"; import { useRef } from "react";
import { Award, Heart, Users } from "lucide-react"; import { Award, Heart, Users } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
export function About() { export function About() {
const ref = useRef(null); const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" }); const isInView = useInView(ref, { once: true, margin: "-100px" });
const [isDark, setIsDark] = useState(false); const { theme } = useAppTheme();
const isDark = theme === "dark";
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 = [ const credentials = [
{ {
@ -114,7 +101,7 @@ export function About() {
className="text-center mb-16" className="text-center mb-16"
> >
<motion.h2 <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" className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 sm:mb-6 px-4 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
@ -122,7 +109,7 @@ export function About() {
Meet Nathalie Mac-Guffie Meet Nathalie Mac-Guffie
</motion.h2> </motion.h2>
<motion.p <motion.p
className="text-xl text-muted-foreground max-w-3xl mx-auto" className="text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-4"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.4 }}
@ -133,21 +120,40 @@ export function About() {
</motion.p> </motion.p>
</motion.div> </motion.div>
<div className="grid md:grid-cols-2 gap-12 items-center mb-16"> <div className="grid md:grid-cols-2 gap-8 md:gap-12 items-center mb-12 md:mb-16 px-4">
<motion.div <motion.div
initial={{ opacity: 0, x: -50 }} initial={{ opacity: 0, x: -50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}} animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }} 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"> <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-2xl md:rounded-3xl p-6 md:p-8 border border-border/50 backdrop-blur-sm">
<motion.h3 <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" className="text-xl sm:text-2xl font-semibold mb-3 md:mb-4 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}} animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.3 }} transition={{ duration: 0.8, delay: 0.3 }}
> >
My Approach My Approach
</motion.h3> </motion.h3>
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6">
<div className="relative flex-shrink-0">
<img
src="/hshot.jpeg"
alt="Nathalie Mac-Guffie supporting clients"
className="w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 select-none rounded-full object-cover shadow-lg"
loading="lazy"
/>
<motion.div
className="pointer-events-none absolute inset-0 rounded-full"
animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 3, repeat: Infinity }}
style={{
background:
"radial-gradient(circle, rgba(153,246,228,0.18), transparent 70%), radial-gradient(circle, rgba(20,184,166,0.12), transparent 70%)",
}}
/>
</div>
<div className="text-center sm:text-left">
<p className="text-muted-foreground mb-4 leading-relaxed"> <p className="text-muted-foreground mb-4 leading-relaxed">
I provide person-centered guidance, following your child's lead while I provide person-centered guidance, following your child's lead while
drawing out their strengths and incorporating effective coping skills. drawing out their strengths and incorporating effective coping skills.
@ -159,6 +165,8 @@ export function About() {
clear objectives tailored to your family's unique needs. clear objectives tailored to your family's unique needs.
</p> </p>
</div> </div>
</div>
</div>
</motion.div> </motion.div>
<motion.div <motion.div
@ -209,6 +217,71 @@ export function About() {
})} })}
</motion.div> </motion.div>
</div> </div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.5 }}
className="bg-card/60 dark:bg-background/80 backdrop-blur-sm border border-border/50 rounded-3xl p-8 mt-16"
>
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-8">
<div className="max-w-2xl">
<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.6 }}
>
Professional Qualifications
</motion.h3>
<p className="text-muted-foreground leading-relaxed">
Nathalie Mac-Guffie brings more than three decades of licensed experience serving children, caregivers, and families
across South Florida. Her practice is grounded in ongoing professional development and nationally recognized credentials.
</p>
</div>
<motion.a
href="https://www.psychologytoday.com/us/therapists/nathalie-mac-guffie-miami-fl/1203864"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center self-start rounded-xl border border-border px-4 py-2 text-sm font-medium text-rose-600 dark:text-rose-400 hover:underline transition"
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.7 }}
>
View Psychology Today Verification
</motion.a>
</div>
<div className="mt-8 grid gap-6 md:grid-cols-2">
<div className="space-y-4">
<div>
<p className="text-sm font-semibold text-foreground uppercase tracking-wide">Verification</p>
<p className="text-sm text-muted-foreground">
Verified by Psychology Today Licensed by the State of Florida (LMHC #MH5585)
</p>
</div>
<div>
<p className="text-sm font-semibold text-foreground uppercase tracking-wide">Education</p>
<p className="text-sm text-muted-foreground">
Barry University Master of Science in Mental Health Counseling (1994)
</p>
</div>
</div>
<div className="space-y-4">
<div>
<p className="text-sm font-semibold text-foreground uppercase tracking-wide">Additional Credentials</p>
<p className="text-sm text-muted-foreground">
Licensed Mental Health Counselor Registered Play Therapist-Supervisor (Association of Play Therapy)
</p>
</div>
<div>
<p className="text-sm font-semibold text-foreground uppercase tracking-wide">Specialized Expertise</p>
<p className="text-sm text-muted-foreground">
Trauma-Focused CBT Infant Mental Health Dyadic and Relationship-Based Therapy
</p>
</div>
</div>
</div>
</motion.div>
</div> </div>
</section> </section>
); );

183
components/ClientFocus.tsx Normal file
View File

@ -0,0 +1,183 @@
'use client';
import { motion } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef } from "react";
import { Users, UserCheck, Globe } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
import Image from "next/image";
export function ClientFocus() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const { theme } = useAppTheme();
const isDark = theme === "dark";
const ages = [
"Children (0 to 10)",
"Teen",
"Adults",
"Elders (65+)"
];
return (
<section
id="client-focus"
ref={ref}
className="relative py-20 px-4 overflow-hidden"
>
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
{/* Minimal overlay - allowing background image to show at near original opaqueness */}
<div
className="absolute inset-0 z-[1]"
style={{
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.15)' : 'rgba(255, 255, 255, 0.10)'
}}
/>
{/* Very subtle gradient overlay */}
{!isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-rose-50/10 via-pink-50/8 to-orange-50/10" />
)}
{isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-gray-900/10 via-gray-800/8 to-gray-900/10" />
)}
{/* Subtle animated blobs */}
<div className="absolute inset-0 overflow-hidden z-[3]">
<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-30 dark:opacity-50"
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-30 dark:opacity-50"
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 }}
>
Who I work with
</motion.h2>
</motion.div>
<div className="grid md:grid-cols-3 gap-6">
{/* Age */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 }}
className="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"
>
<div className="flex items-center gap-3 mb-4">
<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">
<Users className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-xl font-semibold text-foreground">Age</h3>
</div>
<ul className="space-y-2">
{ages.map((age, index) => (
<motion.li
key={age}
initial={{ opacity: 0, x: -10 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.3, delay: 0.3 + index * 0.05 }}
className="text-muted-foreground"
>
{age}
</motion.li>
))}
</ul>
</motion.div>
{/* Participants */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }}
className="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"
>
<div className="flex items-center gap-3 mb-4">
<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">
<UserCheck className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-xl font-semibold text-foreground">Participants</h3>
</div>
<p className="text-muted-foreground">Individuals</p>
</motion.div>
{/* Ethnicity */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.4 }}
className="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"
>
<div className="flex items-center gap-3 mb-4">
<h3 className="text-xl font-semibold text-foreground text-center justify-center">Providing Support to all of South Florida's Diverse Communities</h3>
</div>
<div className="space-y-3">
<div className="flex justify-center items-center">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.5, delay: 0.5 }}
className="relative w-full max-w-md h-auto"
>
<Image
src="/flagss.png"
alt="Organization of American States Flags"
width={400}
height={267}
className="w-full h-auto object-contain rounded-lg"
priority
/>
</motion.div>
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@ -1,18 +1,20 @@
"use client"; "use client";
import { motion, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { useEffect, useRef, useState } from "react"; import { useRef, useState } from "react";
import { Send } from "lucide-react"; import { Send } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { toast } from "sonner"; import { toast } from "sonner";
import { useAppTheme } from "@/components/ThemeProvider";
export function ContactSection() { export function ContactSection() {
const ref = useRef(null); const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" }); const isInView = useInView(ref, { once: true, margin: "-100px" });
const [isDark, setIsDark] = useState(false); const { theme } = useAppTheme();
const isDark = theme === "dark";
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
email: "", email: "",
@ -20,15 +22,6 @@ export function ContactSection() {
message: "", 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) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
toast("Message Received", { toast("Message Received", {
@ -87,16 +80,16 @@ export function ContactSection() {
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
className="mb-16 text-center" 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"> <h2 className="text-3xl sm: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 px-4">
Get in Touch Get in Touch
</h2> </h2>
<div className="mx-auto mb-6 h-1 w-24 rounded-full bg-gradient-to-r from-rose-500 to-pink-600" /> <div className="mx-auto mb-4 sm:mb-6 h-1 w-20 sm: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"> <p className="mx-auto max-w-2xl text-base sm:text-lg text-muted-foreground px-4">
Ready to start your journey? Reach out to schedule a consultation. Ready to start your journey? Reach out to schedule a consultation.
</p> </p>
</motion.div> </motion.div>
<div className="grid gap-12 lg:grid-cols-2 lg:items-stretch"> <div className="grid gap-8 sm:gap-10 lg:gap-12 lg:grid-cols-2 lg:items-stretch">
{/* Left: Illustration replacing cards */} {/* Left: Illustration replacing cards */}
<motion.div <motion.div
initial={{ opacity: 0, x: -50 }} initial={{ opacity: 0, x: -50 }}
@ -104,7 +97,7 @@ export function ContactSection() {
transition={{ duration: 0.6, delay: 0.2 }} transition={{ duration: 0.6, delay: 0.2 }}
className="relative flex" className="relative flex"
> >
<Card 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 backdrop-blur-sm border border-border/50 overflow-hidden relative h-full flex flex-col rounded-3xl"> <Card className="bg-gradient-to-br from-rose-100/15 via-pink-100/15 to-orange-100/15 dark:from-rose-900/15 dark:via-pink-900/15 dark:to-orange-900/15 backdrop-blur-sm border border-border/50 overflow-hidden relative h-full flex flex-col rounded-3xl">
<CardContent className="p-0 flex-1 flex flex-col"> <CardContent className="p-0 flex-1 flex flex-col">
{/* Background image for the card */} {/* Background image for the card */}
<div <div
@ -139,8 +132,8 @@ export function ContactSection() {
</div> </div>
{/* Text content */} {/* Text content */}
<div className="flex-1 text-center sm:text-left flex flex-col justify-center"> <div className="flex-1 text-center sm:text-left flex flex-col justify-center">
<h3 className="text-2xl font-bold mb-4 text-foreground">Let's Begin Your Healing Journey</h3> <h3 className="text-xl sm:text-2xl font-bold mb-3 sm:mb-4 text-foreground">Let's Begin Your Healing Journey</h3>
<div className="space-y-3 text-muted-foreground leading-relaxed"> <div className="space-y-2 sm:space-y-3 text-sm sm:text-base text-muted-foreground leading-relaxed">
<p> <p>
Taking the first step toward therapy can feel daunting, but you're not alone. I'm here to support 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. you through every stage of your journey toward wellness and growth.
@ -168,8 +161,8 @@ export function ContactSection() {
transition={{ duration: 0.6, delay: 0.4 }} transition={{ duration: 0.6, delay: 0.4 }}
> >
<Card className="border border-border/50 bg-card/70 backdrop-blur-sm"> <Card className="border border-border/50 bg-card/70 backdrop-blur-sm">
<CardContent className="p-6 md:p-8"> <CardContent className="p-4 sm:p-6 md:p-8">
<h3 className="mb-6 text-2xl font-bold text-foreground">Send a Message</h3> <h3 className="mb-4 sm:mb-6 text-xl sm:text-2xl font-bold text-foreground">Send a Message</h3>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<Input <Input

160
components/Finances.tsx Normal file
View File

@ -0,0 +1,160 @@
'use client';
import { motion } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef } from "react";
import { CreditCard, DollarSign } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
export function Finances() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const { theme } = useAppTheme();
const isDark = theme === "dark";
const paymentMethods = [
"American Express",
"Discover",
"Mastercard",
"Visa"
];
return (
<section
id="finances"
ref={ref}
className="relative py-20 px-4 overflow-hidden"
>
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
{/* Minimal overlay - allowing background image to show at near original opaqueness */}
<div
className="absolute inset-0 z-[1]"
style={{
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.15)' : 'rgba(255, 255, 255, 0.10)'
}}
/>
{/* Very subtle gradient overlay */}
{!isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-rose-50/10 via-pink-50/8 to-orange-50/10" />
)}
{isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-gray-900/10 via-gray-800/8 to-gray-900/10" />
)}
{/* Subtle animated blobs */}
<div className="absolute inset-0 overflow-hidden z-[3]">
<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-30 dark:opacity-50"
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-30 dark:opacity-50"
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 }}
>
Finances
</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 }}
>
Transparent pricing and flexible payment options to support your therapeutic journey
</motion.p>
</motion.div>
<div className="grid md:grid-cols-2 gap-6 mb-8">
{/* Fees */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 }}
className="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"
>
<div className="flex items-center gap-3 mb-4">
<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">
<DollarSign className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-xl font-semibold text-foreground">Fees</h3>
</div>
<p className="text-2xl font-bold text-foreground mb-2">Individual Sessions</p>
<p className="text-3xl font-bold bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent">$175</p>
</motion.div>
{/* Payment Methods */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }}
className="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"
>
<div className="flex items-center gap-3 mb-4">
<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">
<CreditCard className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-xl font-semibold text-foreground">Payment Methods</h3>
</div>
<div className="space-y-2">
{paymentMethods.map((method, index) => (
<motion.p
key={method}
className="text-muted-foreground"
initial={{ opacity: 0, x: -10 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.3, delay: 0.4 + index * 0.05 }}
>
{method}
</motion.p>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@ -2,26 +2,11 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Heart, Mail, Phone, MapPin } from "lucide-react"; import { Heart, Mail, Phone, MapPin } from "lucide-react";
import { useEffect, useState } from "react"; import { useAppTheme } from "@/components/ThemeProvider";
import Link from "next/link";
export function Footer() { export function Footer() {
const [isDark, setIsDark] = useState(false); const { theme } = useAppTheme();
const isDark = theme === "dark";
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 scrollToSection = (id: string) => {
const element = document.getElementById(id); const element = document.getElementById(id);
@ -52,8 +37,8 @@ export function Footer() {
<div className="absolute inset-0 bg-gradient-to-br from-gray-900/40 via-gray-800/40 to-gray-900/40" /> <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="container mx-auto px-4 sm:px-6 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8 mb-6 sm:mb-8">
{/* Brand Section */} {/* Brand Section */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@ -123,10 +108,10 @@ export function Footer() {
<li className="flex items-start gap-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" /> <Phone className="h-4 w-4 mt-1 text-rose-600 dark:text-rose-400 flex-shrink-0" />
<a <a
href="tel:+19548073027" href="tel:+17548162311"
className="text-sm text-muted-foreground hover:text-rose-600 dark:hover:text-rose-400 transition-colors" className="text-sm text-muted-foreground hover:text-rose-600 dark:hover:text-rose-400 transition-colors"
> >
(954) 807-3027 (754) 816-2311
</a> </a>
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
@ -174,9 +159,9 @@ export function Footer() {
transition={{ duration: 0.6, delay: 0.4 }} transition={{ duration: 0.6, delay: 0.4 }}
className="mt-8 pt-8 border-t border-border/50" 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"> <div className="grid grid-cols-1 items-center gap-3 text-center sm:grid-cols-3 sm:text-left">
<p className="text-sm text-muted-foreground md:justify-self-start"> <p className="text-sm text-muted-foreground md:justify-self-start">
© {new Date().getFullYear()} Attune Heart Therapy. All rights reserved. © {new Date().getFullYear()} Attune Heart Therapy, LLC. All rights reserved.
</p> </p>
<p className="text-xs text-muted-foreground md:justify-self-center"> <p className="text-xs text-muted-foreground md:justify-self-center">
This site is for informational purposes only and does not constitute medical advice. This site is for informational purposes only and does not constitute medical advice.

View File

@ -3,25 +3,11 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ArrowRight, Calendar } from 'lucide-react'; import { ArrowRight, Calendar } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useAppTheme } from '@/components/ThemeProvider';
export function HeroSection() { export function HeroSection() {
const [isDark, setIsDark] = useState(false); const { theme } = useAppTheme();
const isDark = theme === "dark";
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 ( return (
<section <section
@ -32,7 +18,7 @@ export function HeroSection() {
<div <div
className="absolute inset-0 z-0" className="absolute inset-0 z-0"
style={{ style={{
backgroundImage: `url('https://images.unsplash.com/photo-1506126613408-eca07ce68773?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80')`, backgroundImage: `url('/large.jpeg')`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
@ -106,7 +92,7 @@ export function HeroSection() {
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
> >
<motion.h1 <motion.h1
className="text-5xl md:text-7xl font-bold mb-6 text-white drop-shadow-lg" className="text-3xl sm:text-4xl md:text-5xl lg:text-7xl font-bold mb-4 sm:mb-6 text-white drop-shadow-lg px-4"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
@ -115,16 +101,16 @@ export function HeroSection() {
</motion.h1> </motion.h1>
<motion.p <motion.p
className="text-xl md:text-2xl text-white/95 mb-4 drop-shadow-md" className="text-lg sm:text-xl md:text-2xl text-white/95 mb-3 sm:mb-4 drop-shadow-md px-4"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.4 }}
> >
Nathalie Mac Guffie, LCSW Nathalie Mac-Guffie, LMHC
</motion.p> </motion.p>
<motion.p <motion.p
className="text-lg md:text-xl text-white/90 mb-8 max-w-2xl mx-auto drop-shadow-md" className="text-base sm:text-lg md:text-xl text-white/90 mb-6 sm:mb-8 max-w-2xl mx-auto drop-shadow-md px-4"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }} transition={{ duration: 0.8, delay: 0.6 }}
@ -143,15 +129,24 @@ export function HeroSection() {
<Button <Button
size="lg" 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" 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"
asChild
> >
<a href="/book-now">
<Calendar className="mr-2 h-5 w-5" /> <Calendar className="mr-2 h-5 w-5" />
Book Appointment Request Appointment
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</a>
</Button> </Button>
<Button <Button
size="lg" size="lg"
variant="outline" 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" 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"
onClick={() => {
const servicesSection = document.getElementById('services');
if (servicesSection) {
servicesSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}}
> >
Learn More Learn More
</Button> </Button>

141
components/Location.tsx Normal file
View File

@ -0,0 +1,141 @@
'use client';
import { motion } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef } from "react";
import { MapPin, Phone, Building2 } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
export function Location() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const { theme } = useAppTheme();
const isDark = theme === "dark";
return (
<section
id="location"
ref={ref}
className="relative py-20 px-4 overflow-hidden"
>
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
{/* Minimal overlay - allowing background image to show at near original opaqueness */}
<div
className="absolute inset-0 z-[1]"
style={{
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.15)' : 'rgba(255, 255, 255, 0.10)'
}}
/>
{/* Very subtle gradient overlay */}
{!isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-rose-50/10 via-pink-50/8 to-orange-50/10" />
)}
{isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-gray-900/10 via-gray-800/8 to-gray-900/10" />
)}
{/* Subtle animated blobs */}
<div className="absolute inset-0 overflow-hidden z-[3]">
<motion.div
className="absolute top-20 left-20 w-72 h-72 bg-cyan-100 dark:bg-cyan-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-30 dark:opacity-50"
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-20 w-96 h-96 bg-emerald-100 dark:bg-emerald-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-30 dark:opacity-50"
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 }}
>
Location
</motion.h2>
</motion.div>
<div className="grid md:grid-cols-2 gap-6 mb-8">
{/* Primary Location */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }}
className="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 transition-all duration-300"
>
<div className="flex items-center gap-3 mb-4">
<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">
<MapPin className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-xl font-semibold text-foreground">Primary Location</h3>
</div>
<p className="text-muted-foreground mb-2">Hollywood, FL</p>
<div className="flex items-center gap-2 text-muted-foreground">
<Phone className="h-4 w-4" />
<span>(754) 816-2311</span>
</div>
</motion.div>
{/* Additional Location */}
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.3 }}
className="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 transition-all duration-300"
>
<div className="flex items-center gap-3 mb-4">
<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">
<Building2 className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-xl font-semibold text-foreground">Additional Location</h3>
</div>
<p className="text-muted-foreground mb-2">South Miami, FL</p>
<div className="flex items-center gap-2 text-muted-foreground">
<Phone className="h-4 w-4" />
<span>(754) 816-2311</span>
</div>
</motion.div>
</div>
</div>
</section>
);
}

316
components/LoginDialog.tsx Normal file
View File

@ -0,0 +1,316 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAppTheme } from "@/components/ThemeProvider";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Eye, EyeOff, Loader2, X } from "lucide-react";
interface LoginDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onLoginSuccess: () => void;
}
// Login Dialog component
export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogProps) {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [isSignup, setIsSignup] = useState(false);
const [loginData, setLoginData] = useState({
email: "",
password: "",
});
const [signupData, setSignupData] = useState({
fullName: "",
email: "",
phone: "",
});
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [loginLoading, setLoginLoading] = useState(false);
const [signupLoading, setSignupLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginLoading(true);
setError(null);
try {
// Simulate login API call
await new Promise((resolve) => setTimeout(resolve, 1000));
// After successful login, close dialog and call success callback
setShowPassword(false);
setLoginLoading(false);
onOpenChange(false);
onLoginSuccess();
} catch (err) {
setError("Login failed. Please try again.");
setLoginLoading(false);
}
};
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setSignupLoading(true);
setError(null);
try {
// Simulate signup API call
await new Promise((resolve) => setTimeout(resolve, 1000));
// After successful signup, automatically log in and proceed
setSignupLoading(false);
onOpenChange(false);
onLoginSuccess();
} catch (err) {
setError("Signup failed. Please try again.");
setSignupLoading(false);
}
};
const handleSwitchToSignup = () => {
setIsSignup(true);
setError(null);
setLoginData({ email: "", password: "" });
};
const handleSwitchToLogin = () => {
setIsSignup(false);
setError(null);
setSignupData({ fullName: "", email: "", phone: "" });
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
className={`sm:max-w-md ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}
>
{/* Header with Close Button */}
<div className="flex items-start justify-between mb-2">
<DialogHeader className="flex-1">
<DialogTitle className="text-3xl font-bold bg-gradient-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent">
{isSignup ? "Create an account" : "Welcome back"}
</DialogTitle>
<DialogDescription className={isDark ? 'text-gray-400' : 'text-gray-600'}>
{isSignup
? "Sign up to complete your booking"
: "Please log in to complete your booking"}
</DialogDescription>
</DialogHeader>
{/* Close Button */}
<button
onClick={() => onOpenChange(false)}
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition-colors ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Signup Form */}
{isSignup ? (
<form className="space-y-6 mt-4" onSubmit={handleSignup}>
{error && (
<div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
</div>
)}
{/* Full Name Field */}
<div className="space-y-2">
<label htmlFor="signup-fullName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Full Name *
</label>
<Input
id="signup-fullName"
type="text"
placeholder="John Doe"
value={signupData.fullName}
onChange={(e) => setSignupData({ ...signupData, fullName: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required
/>
</div>
{/* Email Field */}
<div className="space-y-2">
<label htmlFor="signup-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address *
</label>
<Input
id="signup-email"
type="email"
placeholder="Email address"
value={signupData.email}
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required
/>
</div>
{/* Phone Field */}
<div className="space-y-2">
<label htmlFor="signup-phone" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Phone Number *
</label>
<Input
id="signup-phone"
type="tel"
placeholder="+1 (555) 123-4567"
value={signupData.phone}
onChange={(e) => setSignupData({ ...signupData, phone: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required
/>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={signupLoading}
className="w-full h-12 text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{signupLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating account...
</>
) : (
"Sign up"
)}
</Button>
{/* Switch to Login */}
<p className={`text-sm text-center ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Already have an account?{" "}
<button
type="button"
onClick={handleSwitchToLogin}
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Log in
</button>
</p>
</form>
) : (
/* Login Form */
<form className="space-y-6 mt-4" onSubmit={handleLogin}>
{error && (
<div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
</div>
)}
{/* Email Field */}
<div className="space-y-2">
<label htmlFor="login-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address
</label>
<Input
id="login-email"
type="email"
placeholder="Email address"
value={loginData.email}
onChange={(e) => setLoginData({ ...loginData, email: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required
/>
</div>
{/* Password Field */}
<div className="space-y-2">
<label htmlFor="login-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Your password
</label>
<div className="relative">
<Input
id="login-password"
type={showPassword ? "text" : "password"}
placeholder="Your password"
value={loginData.password}
onChange={(e) => setLoginData({ ...loginData, password: e.target.value })}
className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword(!showPassword)}
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</Button>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={loginLoading}
className="w-full h-12 text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{loginLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Logging in...
</>
) : (
"Log in"
)}
</Button>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between text-sm">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className={`w-4 h-4 rounded text-rose-600 focus:ring-2 focus:ring-rose-500 cursor-pointer ${isDark ? 'border-gray-600 bg-gray-700' : 'border-gray-300'}`}
/>
<span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span>
</label>
<button
type="button"
className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
onClick={() => onOpenChange(false)}
>
Forgot password?
</button>
</div>
{/* Sign Up Prompt */}
<p className={`text-sm text-center ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
New to Attune Heart Therapy?{" "}
<button
type="button"
onClick={handleSwitchToSignup}
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Sign up
</button>
</p>
</form>
)}
</DialogContent>
</Dialog>
);
}

View File

@ -1,36 +1,50 @@
'use client'; 'use client';
import { motion } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Heart } from "lucide-react"; import { Heart, Menu, X } from "lucide-react";
import { ThemeToggle } from "@/components/ThemeToggle"; import { ThemeToggle } from "@/components/ThemeToggle";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LoginDialog } from "@/components/LoginDialog";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import { useAppTheme } from "@/components/ThemeProvider";
export function Navbar() { export function Navbar() {
const [isDark, setIsDark] = useState(false); const { theme } = useAppTheme();
const isDark = theme === "dark";
useEffect(() => { const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const checkTheme = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
setIsDark(document.documentElement.classList.contains('dark')); const router = useRouter();
}; const pathname = usePathname();
const isUserDashboard = pathname?.startsWith("/user/dashboard");
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
const scrollToSection = (id: string) => { const scrollToSection = (id: string) => {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
element.scrollIntoView({ behavior: "smooth" }); element.scrollIntoView({ behavior: "smooth" });
setMobileMenuOpen(false); // Close mobile menu after navigation
} }
}; };
const handleLoginSuccess = () => {
// Redirect to user dashboard after successful login
router.push("/user/dashboard");
setMobileMenuOpen(false);
};
// Close mobile menu when clicking outside
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [mobileMenuOpen]);
return ( return (
<motion.nav <motion.nav
initial={{ y: -100 }} initial={{ y: -100 }}
@ -41,54 +55,169 @@ export function Navbar() {
backgroundColor: isDark ? '#1a1e26' : '#ffffff' backgroundColor: isDark ? '#1a1e26' : '#ffffff'
}} }}
> >
<div className="container mx-auto px-4"> <div className="container mx-auto px-3 sm:px-4">
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-14 sm:h-16">
<motion.a <motion.div
href="#home" className="flex items-center gap-1.5 sm:gap-2"
className="flex items-center gap-2"
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
<div className="bg-linear-to-r from-rose-500 to-pink-600 p-2 rounded-xl"> <Link href="/" className="flex items-center gap-1.5 sm:gap-2">
<Heart className="h-5 w-5 text-white fill-white" /> <div className="bg-gradient-to-r from-rose-500 to-pink-600 p-1.5 sm:p-2 rounded-lg sm:rounded-xl">
<Heart className="h-4 w-4 sm:h-5 sm:w-5 text-white fill-white" />
</div> </div>
<span className="font-bold text-lg bg-linear-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent"> <span className="font-bold text-sm sm:text-base md:text-lg bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent">
Attune Heart Therapy Attune Heart Therapy
</span> </span>
</motion.a> </Link>
</motion.div>
<div className="hidden md:flex items-center gap-6"> {/* Desktop Navigation */}
{!isUserDashboard && (
<div className="hidden lg:flex items-center gap-4 xl:gap-6">
<button <button
onClick={() => scrollToSection("about")} 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" className={`text-sm font-medium transition-colors cursor-pointer px-3 py-2 rounded-lg ${isDark ? 'text-gray-300 hover:text-white hover:bg-gray-800' : 'text-gray-700 hover:text-primary hover:bg-gray-100'}`}
> >
About About
</button> </button>
<button <button
onClick={() => scrollToSection("services")} 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" className={`text-sm font-medium transition-colors cursor-pointer px-3 py-2 rounded-lg ${isDark ? 'text-gray-300 hover:text-white hover:bg-gray-800' : 'text-gray-700 hover:text-primary hover:bg-gray-100'}`}
> >
Services Services
</button> </button>
<button <button
onClick={() => scrollToSection("contact")} 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" className={`text-sm font-medium transition-colors cursor-pointer px-3 py-2 rounded-lg ${isDark ? 'text-gray-300 hover:text-white hover:bg-gray-800' : 'text-gray-700 hover:text-primary hover:bg-gray-100'}`}
> >
Contact Contact
</button> </button>
</div> </div>
)}
<div className="flex items-center gap-2"> {/* Desktop Actions */}
<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> <div className="hidden lg:flex items-center gap-2">
<a href="/login">Sign In</a> {!isUserDashboard && (
<Button
size="sm"
variant="outline"
className={`hover:opacity-90 hover:scale-105 transition-all text-xs sm:text-sm ${isDark ? 'border-gray-700 text-gray-300 hover:bg-gray-800' : ''}`}
onClick={() => setLoginDialogOpen(true)}
>
Sign In
</Button> </Button>
)}
<ThemeToggle /> <ThemeToggle />
<Button size="sm" className="hidden sm:inline-flex hover:opacity-90 hover:scale-105 transition-all dark:hover:bg-emerald-600" asChild> <Button size="sm" className="hover:opacity-90 hover:scale-105 transition-all text-xs sm:text-sm" asChild>
<a href="tel:+19548073027">Book Now</a> <a href="/book-now">Book Now</a>
</Button>
</div>
{/* Mobile Actions */}
<div className="flex lg:hidden items-center gap-1.5 sm:gap-2">
<ThemeToggle />
<Button
variant="ghost"
size="icon"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="hover:bg-gray-100 dark:hover:bg-gray-800 h-9 w-9 sm:h-10 sm:w-10"
aria-label="Toggle menu"
>
{mobileMenuOpen ? (
<X className="h-5 w-5 sm:h-6 sm:w-6" />
) : (
<Menu className="h-5 w-5 sm:h-6 sm:w-6" />
)}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setMobileMenuOpen(false)}
/>
{/* Mobile Menu Panel */}
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="fixed top-14 sm:top-16 right-0 bottom-0 w-[280px] sm:w-80 max-w-[85vw] z-50 lg:hidden overflow-y-auto"
style={{
backgroundColor: isDark ? '#1a1e26' : '#ffffff'
}}
>
<div className="flex flex-col p-4 sm:p-6 space-y-3 sm:space-y-4">
{/* Mobile Navigation Links */}
{!isUserDashboard && (
<>
<button
onClick={() => scrollToSection("about")}
className={`text-left text-sm sm:text-base font-medium py-2.5 sm:py-3 px-3 sm:px-4 rounded-lg transition-colors ${isDark ? 'text-gray-300 hover:bg-gray-800' : 'text-gray-700 hover:bg-gray-100'}`}
>
About
</button>
<button
onClick={() => scrollToSection("services")}
className={`text-left text-sm sm:text-base font-medium py-2.5 sm:py-3 px-3 sm:px-4 rounded-lg transition-colors ${isDark ? 'text-gray-300 hover:bg-gray-800' : 'text-gray-700 hover:bg-gray-100'}`}
>
Services
</button>
<button
onClick={() => scrollToSection("contact")}
className={`text-left text-sm sm:text-base font-medium py-2.5 sm:py-3 px-3 sm:px-4 rounded-lg transition-colors ${isDark ? 'text-gray-300 hover:bg-gray-800' : 'text-gray-700 hover:bg-gray-100'}`}
>
Contact
</button>
</>
)}
<div className={`border-t pt-3 sm:pt-4 mt-3 sm:mt-4 space-y-2 sm:space-y-3 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
{!isUserDashboard && (
<Button
variant="outline"
className={`w-full justify-start text-sm sm:text-base ${isDark ? 'border-gray-700 text-gray-300 hover:bg-gray-800' : ''}`}
onClick={() => {
setLoginDialogOpen(true);
setMobileMenuOpen(false);
}}
>
Sign In
</Button>
)}
<Button
className="w-full justify-start bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white text-sm sm:text-base"
asChild
>
<Link href="/book-now" onClick={() => setMobileMenuOpen(false)}>
Book Now
</Link>
</Button>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
{/* Login Dialog */}
<LoginDialog
open={loginDialogOpen}
onOpenChange={setLoginDialogOpen}
onLoginSuccess={handleLoginSuccess}
/>
</motion.nav> </motion.nav>
); );
} }

View File

@ -2,65 +2,58 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useInView } from "framer-motion"; import { useInView } from "framer-motion";
import { useRef, useEffect, useState } from "react"; import { useRef } from "react";
import { Baby, Brain, HeartHandshake, Sparkles, Users2, Shield } from "lucide-react"; import { Baby, Brain, HeartHandshake, Sparkles, Users2, Shield, Users, Heart, Feather } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
export function Services() { export function Services() {
const ref = useRef(null); const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" }); const isInView = useInView(ref, { once: true, margin: "-100px" });
const [isDark, setIsDark] = useState(false); const { theme } = useAppTheme();
const isDark = theme === "dark";
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 = [ const services = [
{ {
icon: Brain, icon: Users,
title: "Trauma-Focused Therapy", title: "Child & Adolescent Support",
description: "Evidence-based TF-CBT to help children process and heal from traumatic experiences in a safe, supportive environment.", description:
backgroundImage: "https://images.unsplash.com/photo-1519494026892-80bbd2d6fd0d?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80", "Developmentally attuned support for children, teens, adults, and elders—navigating adoption stories, peer relationships, and social-emotional growth alongside caregivers.",
backgroundImage: "https://images.unsplash.com/photo-1503454537195-1dcabb73ffb9?auto=format&fit=crop&w=1000&q=80",
}, },
{ {
icon: Sparkles, icon: Sparkles,
title: "Play Therapy", title: "Coping Skills Coaching",
description: "Child-centered play therapy allowing children to express themselves naturally and build emotional regulation skills.", description:
backgroundImage: "https://images.unsplash.com/photo-1503454537195-1dcabb73ffb9?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80", "Solution-focused strategies that build resilience, emotional literacy, and daily coping tools for individuals across the lifespan.",
backgroundImage: "https://images.unsplash.com/photo-1531512073830-ba890ca4eba2?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80",
}, },
{ {
icon: Baby, icon: Heart,
title: "Infant Mental Health", title: "Self-Esteem Building",
description: "Specialized support for infants and toddlers, focusing on early attachment, developmental milestones, and caregiver relationships.", description:
backgroundImage: "https://images.unsplash.com/photo-1515488042361-ee00e0ddd4e4?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80", "Strength-based interventions that nurture confidence, self-worth, and advocacy skills for children, teens, adults, and elders.",
backgroundImage: "https://images.unsplash.com/photo-1526778548025-fa2f459cd5c1?auto=format&fit=crop&w=1000&q=80",
}, },
{ {
icon: Users2, icon: Users2,
title: "Dyadic Therapy", title: "Family & Caregiver Collaboration",
description: "Strengthening parent-child relationships through interactive sessions that enhance communication and connection.", description:
"Dyadic therapy that reduces family conflict, strengthens parenting partnerships, and supports intergenerational understanding.",
backgroundImage: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80", backgroundImage: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80",
}, },
{ {
icon: HeartHandshake, icon: Feather,
title: "Social-Emotional Support", title: "Life Transitions & Perinatal Care",
description: "Building emotional literacy and self-regulation skills to help children navigate relationships and challenges.", description:
backgroundImage: "https://images.unsplash.com/photo-1497486751825-1233686d5d80?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80", "Guidance through perinatal adjustments, caregiving shifts, and later-life transitions with compassion for each stage of adulthood.",
backgroundImage: "https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?auto=format&fit=crop&w=1000&q=80",
}, },
{ {
icon: Shield, icon: Shield,
title: "Relationship-Based Care", title: "Trauma & PTSD Recovery",
description: "Fostering healing through nurturing therapeutic relationships and caregiver collaboration.", description:
backgroundImage: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80", "Trauma-focused CBT and play-based interventions that promote emotional safety, caregiver bonding, and long-term healing.",
backgroundImage: "https://images.unsplash.com/photo-1519494026892-80bbd2d6fd0d?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80",
}, },
]; ];
@ -135,24 +128,24 @@ export function Services() {
className="text-center mb-16" className="text-center mb-16"
> >
<motion.h2 <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" className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 sm:mb-6 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-600 bg-clip-text text-transparent px-4"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
> >
Specialized Services Services
</motion.h2> </motion.h2>
<motion.p <motion.p
className="text-xl text-muted-foreground max-w-3xl mx-auto" className="text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-4"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.4 }}
> >
Comprehensive, evidence-based therapeutic support for children and families Comprehensive, relationship-based therapy anchored in clinical expertise and compassionate collaboration.
</motion.p> </motion.p>
</motion.div> </motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 px-4">
{services.map((service, index) => { {services.map((service, index) => {
const Icon = service.icon; const Icon = service.icon;
return ( return (
@ -161,9 +154,8 @@ export function Services() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }} 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" className="group bg-card/50 backdrop-blur-sm rounded-xl sm:rounded-2xl p-4 sm: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"
> >
{/* Content */}
<div className="relative z-10"> <div className="relative z-10">
<motion.div <motion.div
className="w-16 h-16 rounded-xl overflow-hidden mb-4 shadow-lg" className="w-16 h-16 rounded-xl overflow-hidden mb-4 shadow-lg"
@ -202,7 +194,7 @@ export function Services() {
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.6 }} 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" className="mt-16 bg-gradient-to-br from-rose-100/25 via-pink-100/25 to-orange-100/25 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 <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" 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"
@ -219,9 +211,8 @@ export function Services() {
animate={isInView ? { opacity: 1 } : {}} animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.8 }} 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 I collaborate closely with caregivers and individuals to foster emotional literacy,
dealing with trauma, stressors, or social-emotional challenges and need understanding self-regulation, and secure relationships during times of transition or trauma.
and support.
</motion.p> </motion.p>
<motion.p <motion.p
className="text-muted-foreground leading-relaxed" className="text-muted-foreground leading-relaxed"
@ -229,8 +220,8 @@ export function Services() {
animate={isInView ? { opacity: 1 } : {}} animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.9 }} transition={{ duration: 0.8, delay: 0.9 }}
> >
The goal is to build a healthy foundation through nurturing relationships and Therapy is tailored for children (age 610), teens, adults, and older adults, with services offered to individuals and a
emotional literacy, helping children diminish distress and enhance self-regulation. special focus on supporting Black and African American families in Miami and Hollywood, Florida.
</motion.p> </motion.p>
</div> </div>
</motion.div> </motion.div>

182
components/Specialties.tsx Normal file
View File

@ -0,0 +1,182 @@
'use client';
import { motion } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef } from "react";
import { Star, Award } from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
export function Specialties() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const { theme } = useAppTheme();
const isDark = theme === "dark";
const topSpecialties = [
"Child or Adolescent",
"Coping Skills",
"Self Esteem"
];
const expertise = [
"Adoption",
"Family Conflict",
"Geriatric and Seniors",
"Life Transitions",
"Parenting",
"Peer Relationships",
"Perinatal Mental Health",
"Trauma and PTSD"
];
return (
<section
id="specialties"
ref={ref}
className="relative py-20 px-4 overflow-hidden"
>
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
{/* Minimal overlay - allowing background image to show at near original opaqueness */}
<div
className="absolute inset-0 z-[1]"
style={{
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.15)' : 'rgba(255, 255, 255, 0.10)'
}}
/>
{/* Very subtle gradient overlay */}
{!isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-rose-50/10 via-pink-50/8 to-orange-50/10" />
)}
{isDark && (
<div className="absolute inset-0 z-[2] bg-gradient-to-br from-gray-900/10 via-gray-800/8 to-gray-900/10" />
)}
{/* Subtle animated blobs */}
<div className="absolute inset-0 overflow-hidden z-[3]">
<motion.div
className="absolute top-20 left-20 w-72 h-72 bg-cyan-100 dark:bg-cyan-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-30 dark:opacity-50"
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-20 w-96 h-96 bg-emerald-100 dark:bg-emerald-900/20 rounded-full mix-blend-multiply dark:mix-blend-lighten filter blur-xl opacity-30 dark:opacity-50"
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 }}
>
Specialties and Expertise
</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 therapeutic support tailored to your unique needs
</motion.p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8">
{/* Top Specialties */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }}
className="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border/50 hover:border-rose-500/50 hover:shadow-lg hover:shadow-rose-500/10 transition-all duration-300"
>
<div className="flex items-center gap-3 mb-6">
<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">
<Star className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-2xl font-semibold text-foreground">Top Specialties</h3>
</div>
<ul className="space-y-4">
{topSpecialties.map((specialty, index) => (
<motion.li
key={specialty}
initial={{ opacity: 0, x: -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5, delay: 0.4 + index * 0.1 }}
className="flex items-center gap-3 text-lg text-muted-foreground"
>
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-rose-500 to-pink-600" />
<span className="text-foreground">{specialty}</span>
</motion.li>
))}
</ul>
</motion.div>
{/* Expertise */}
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.3 }}
className="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border/50 hover:border-rose-500/50 hover:shadow-lg hover:shadow-rose-500/10 transition-all duration-300"
>
<div className="flex items-center gap-3 mb-6">
<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">
<Award className="h-6 w-6 text-rose-600 dark:text-rose-400" />
</div>
<h3 className="text-2xl font-semibold text-foreground">Expertise</h3>
</div>
<ul className="space-y-4">
{expertise.map((item, index) => (
<motion.li
key={item}
initial={{ opacity: 0, x: 20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5, delay: 0.5 + index * 0.1 }}
className="flex items-center gap-3 text-lg text-muted-foreground"
>
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-rose-500 to-pink-600" />
<span className="text-foreground">{item}</span>
</motion.li>
))}
</ul>
</motion.div>
</div>
</div>
</section>
);
}

View File

@ -1,32 +1,18 @@
"use client";
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react"; import { useAppTheme } from "@/components/ThemeProvider";
export function ThemeToggle() { export function ThemeToggle() {
const [theme, setTheme] = useState<"light" | "dark">("light"); const { theme, toggleTheme } = useAppTheme();
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 ( return (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={toggleTheme} onClick={toggleTheme}
className="relative rounded-full cursor-pointer hover:bg-gray-100 dark:hover:bg-cyan-900/30 transition-colors" className="relative rounded-full cursor-pointer hover:bg-transparent transition-colors"
aria-label="Toggle theme" aria-label="Toggle theme"
> >
<Sun className={`h-5 w-5 transition-all absolute ${theme === "light" ? "rotate-0 scale-100" : "rotate-90 scale-0"}`} /> <Sun className={`h-5 w-5 transition-all absolute ${theme === "light" ? "rotate-0 scale-100" : "rotate-90 scale-0"}`} />

143
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -42,7 +42,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border-0 p-1 shadow-md",
className className
)} )}
{...props} {...props}
@ -230,7 +230,7 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border-0 p-1 shadow-lg",
className className
)} )}
{...props} {...props}

View File

@ -30,7 +30,7 @@ function PopoverContent({
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-gray-200 p-4 shadow-md outline-hidden",
className className
)} )}
{...props} {...props}

View File

@ -62,7 +62,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border-0 shadow-md",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className

View File

@ -7,3 +7,4 @@ export function Toaster() {

View File

@ -12,6 +12,7 @@
"node": ">=20.9.0" "node": ">=20.9.0"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",

View File

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-dropdown-menu': '@radix-ui/react-dropdown-menu':
specifier: ^2.1.16 specifier: ^2.1.16
version: 2.1.16(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 2.1.16(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -504,6 +507,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-dialog@1.1.15':
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.1': '@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies: peerDependencies:
@ -2801,6 +2817,28 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.2 '@types/react': 19.2.2
'@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
aria-hidden: 1.2.6
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)': '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies: dependencies:
react: 19.2.0 react: 19.2.0

BIN
public/flagss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
public/session.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
public/woman.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB