Merge branch 'master' of http://35.207.46.142/ATTUNE-HEART-THERAPY/website into feat/authentication
This commit is contained in:
commit
3180e48fe1
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
310
app/(admin)/admin/settings/page.tsx
Normal file
310
app/(admin)/admin/settings/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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>
|
||||||
|
|||||||
680
app/(pages)/book-now/page.tsx
Normal file
680
app/(pages)/book-now/page.tsx
Normal 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
8
app/(pages)/layout.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function PagesLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
8
app/(user)/layout.tsx
Normal file
8
app/(user)/layout.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function UserLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
345
app/(user)/user/dashboard/page.tsx
Normal file
345
app/(user)/user/dashboard/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
313
app/(user)/user/settings/page.tsx
Normal file
313
app/(user)/user/settings/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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
15
app/icon.svg
Normal 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 |
@ -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({
|
||||||
|
|||||||
12
app/page.tsx
12
app/page.tsx
@ -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 />
|
||||||
|
|||||||
@ -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
183
components/ClientFocus.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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
160
components/Finances.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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.
|
||||||
|
|||||||
@ -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
141
components/Location.tsx
Normal 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
316
components/LoginDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 6–10), 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
182
components/Specialties.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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
143
components/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -7,3 +7,4 @@ export function Toaster() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
BIN
public/flagss.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
public/session.jpg
Normal file
BIN
public/session.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
BIN
public/woman.jpg
Normal file
BIN
public/woman.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
Loading…
Reference in New Issue
Block a user