feat/authentication #5
18
app/(admin)/booking/page.tsx
Normal file
18
app/(admin)/booking/page.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import SideNav from "@/components/side-nav";
|
||||
|
||||
export default function Booking() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Side Navigation */}
|
||||
<SideNav />
|
||||
|
||||
<div className="md:ml-[250px]">
|
||||
<main className="p-4 sm:p-6 lg:p-8">
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
20
app/(admin)/dashboard/page.tsx
Normal file
20
app/(admin)/dashboard/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import SideNav from "@/components/side-nav";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Side Navigation */}
|
||||
<SideNav />
|
||||
|
||||
<div className="md:ml-[250px]">
|
||||
{/* Main Content */}
|
||||
<main className="p-4 sm:p-6 lg:p-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
7
app/(admin)/layout.tsx
Normal file
7
app/(admin)/layout.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
143
app/(admin)/login/page.tsx
Normal file
143
app/(admin)/login/page.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Heart, Eye, EyeOff, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Login() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center px-4 py-12">
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/doctors.png"
|
||||
alt="Medical professionals"
|
||||
fill
|
||||
className="object-cover object-center"
|
||||
priority
|
||||
sizes="100vw"
|
||||
/>
|
||||
{/* Overlay for better readability */}
|
||||
<div className="absolute inset-0 bg-black/20"></div>
|
||||
</div>
|
||||
|
||||
{/* Branding - Top Left */}
|
||||
<div className="absolute top-8 left-8 flex items-center gap-3 z-30">
|
||||
<Heart className="w-6 h-6 text-white" fill="white" />
|
||||
<span className="text-white text-xl font-semibold">Attune Heart Therapy</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Centered White Card - Login Form */}
|
||||
<div className="relative z-20 w-full max-w-md bg-white rounded-2xl shadow-2xl p-8">
|
||||
{/* Close Button */}
|
||||
<Button
|
||||
onClick={() => router.back()}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto mb-6 w-8 h-8 rounded-full"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-5 h-5 text-black" />
|
||||
</Button>
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className="text-3xl font-bold text-black 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 */}
|
||||
<form className="space-y-6" onSubmit={(e) => e.preventDefault()}>
|
||||
{/* Email Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-medium text-black">
|
||||
Email address
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
className="h-12 bg-white border-gray-300"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="text-sm font-medium text-black">
|
||||
Your password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Your password"
|
||||
className="h-12 bg-white border-gray-300 pr-12"
|
||||
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 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"
|
||||
className="w-full h-12 text-base font-semibold bg-[#4A90A4] hover:bg-[#3a7a8a] text-white"
|
||||
>
|
||||
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 border-gray-300 text-[#4A90A4] focus:ring-2 focus:ring-[#4A90A4] cursor-pointer"
|
||||
/>
|
||||
<span className="text-black">Remember me</span>
|
||||
</label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
app/(admin)/notifications/page.tsx
Normal file
83
app/(admin)/notifications/page.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import SideNav from "@/components/side-nav";
|
||||
import { Notifications, Notification } from "@/components/notifications";
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([
|
||||
{
|
||||
id: "1",
|
||||
type: "appointment",
|
||||
title: "New Appointment Request",
|
||||
message: "Sarah Johnson requested an appointment for tomorrow at 2:00 PM",
|
||||
time: "2 minutes ago",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "success",
|
||||
title: "Appointment Confirmed",
|
||||
message: "Your appointment with Michael Chen has been confirmed for today at 10:00 AM",
|
||||
time: "1 hour ago",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "warning",
|
||||
title: "Appointment Reminder",
|
||||
message: "You have an appointment in 30 minutes with Emily Davis",
|
||||
time: "3 hours ago",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "info",
|
||||
title: "New Message",
|
||||
message: "You received a new message from John Smith",
|
||||
time: "5 hours ago",
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "appointment",
|
||||
title: "Appointment Cancelled",
|
||||
message: "Robert Wilson cancelled his appointment scheduled for tomorrow",
|
||||
time: "1 day ago",
|
||||
read: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const handleMarkAsRead = (id: string) => {
|
||||
setNotifications((prev) =>
|
||||
prev.map((n) => (n.id === id ? { ...n, read: true } : n))
|
||||
);
|
||||
};
|
||||
|
||||
const handleDismiss = (id: string) => {
|
||||
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
||||
};
|
||||
|
||||
const handleMarkAllAsRead = () => {
|
||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Side Navigation */}
|
||||
<SideNav />
|
||||
|
||||
<div className="md:ml-[250px]">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Notifications
|
||||
notifications={notifications}
|
||||
onMarkAsRead={handleMarkAsRead}
|
||||
onDismiss={handleDismiss}
|
||||
onMarkAllAsRead={handleMarkAllAsRead}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
131
app/globals.css
131
app/globals.css
@ -1,26 +1,125 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-sans: var(--font-poppins);
|
||||
--font-mono: var(--font-poppins);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
body.menu-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Poppins } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const poppins = Poppins({
|
||||
variable: "--font-poppins",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Attune Heart Therapy",
|
||||
description: "Attune Heart Therapy",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -25,7 +21,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${poppins.variable} font-sans antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
|
||||
22
components.json
Normal file
22
components.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
174
components/notifications.tsx
Normal file
174
components/notifications.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Bell,
|
||||
X,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Info,
|
||||
Calendar,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: "success" | "warning" | "info" | "appointment";
|
||||
title: string;
|
||||
message: string;
|
||||
time: string;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
interface NotificationsProps {
|
||||
notifications: Notification[];
|
||||
onMarkAsRead?: (id: string) => void;
|
||||
onDismiss?: (id: string) => void;
|
||||
onMarkAllAsRead?: () => void;
|
||||
}
|
||||
|
||||
export function Notifications({
|
||||
notifications,
|
||||
onMarkAsRead,
|
||||
onDismiss,
|
||||
onMarkAllAsRead,
|
||||
}: NotificationsProps) {
|
||||
const unreadCount = notifications.filter((n) => !n.read).length;
|
||||
|
||||
const getIcon = (type: Notification["type"]) => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return <CheckCircle className="w-5 h-5 text-green-600" />;
|
||||
case "warning":
|
||||
return <AlertCircle className="w-5 h-5 text-orange-600" />;
|
||||
case "info":
|
||||
return <Info className="w-5 h-5 text-blue-600" />;
|
||||
case "appointment":
|
||||
return <Calendar className="w-5 h-5 text-rose-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getBgColor = (type: Notification["type"]) => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return "bg-[#4A90A4]/10 border-[#4A90A4]/30";
|
||||
case "warning":
|
||||
return "bg-rose-100 border-rose-300";
|
||||
case "info":
|
||||
return "bg-pink-50 border-pink-200";
|
||||
case "appointment":
|
||||
return "bg-gradient-to-br from-rose-50 to-pink-50 border-rose-300";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Bell className="w-6 h-6 text-gray-900" />
|
||||
<h2 className="text-2xl font-bold text-gray-900">Notifications</h2>
|
||||
{unreadCount > 0 && (
|
||||
<span className="px-2.5 py-0.5 bg-gradient-to-r from-rose-500 to-pink-500 text-white text-sm font-medium rounded-full">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{unreadCount > 0 && onMarkAllAsRead && (
|
||||
<Button variant="outline" size="sm" onClick={onMarkAllAsRead}>
|
||||
Mark all as read
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notifications List */}
|
||||
<div className="space-y-3">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Bell className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600">No notifications</p>
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={cn(
|
||||
"p-4 rounded-lg border-2 transition-all",
|
||||
getBgColor(notification.type),
|
||||
!notification.read && "ring-2 ring-offset-2 ring-rose-300"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">{getIcon(notification.type)}</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h3
|
||||
className={cn(
|
||||
"font-semibold mb-1",
|
||||
!notification.read ? "text-gray-900" : "text-gray-700"
|
||||
)}
|
||||
>
|
||||
{notification.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-2">{notification.message}</p>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<Clock className="w-3 h-3" />
|
||||
{notification.time}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!notification.read && onMarkAsRead && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => onMarkAsRead(notification.id)}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{onDismiss && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => onDismiss(notification.id)}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Notification Bell Component for Header
|
||||
export function NotificationBell({
|
||||
count,
|
||||
onClick,
|
||||
}: {
|
||||
count: number;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Button variant="ghost" size="icon" className="relative" onClick={onClick}>
|
||||
<Bell className="w-5 h-5" />
|
||||
{count > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center font-medium">
|
||||
{count > 9 ? "9+" : count}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
156
components/side-nav.tsx
Normal file
156
components/side-nav.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
LayoutGrid,
|
||||
Calendar,
|
||||
Settings,
|
||||
LogOut,
|
||||
Menu,
|
||||
X,
|
||||
Heart,
|
||||
} from "lucide-react";
|
||||
|
||||
const navItems = [
|
||||
{ label: "Dashboard", icon: LayoutGrid, href: "/dashboard" },
|
||||
{ label: "Book Appointment", icon: Calendar, href: "/booking" },
|
||||
];
|
||||
|
||||
export default function SideNav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const getActiveIndex = () => {
|
||||
return navItems.findIndex((item) => pathname?.includes(item.href)) ?? -1;
|
||||
};
|
||||
|
||||
// Handle body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.classList.add("menu-open");
|
||||
} else {
|
||||
document.body.classList.remove("menu-open");
|
||||
}
|
||||
return () => {
|
||||
document.body.classList.remove("menu-open");
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 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 items-center gap-3">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-rose-100 to-pink-100">
|
||||
<Heart className="w-5 h-5 text-rose-600" fill="currentColor" />
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-gray-900">Attune Heart Therapy</span>
|
||||
</div>
|
||||
<button onClick={() => setOpen((v) => !v)} aria-label="Open menu">
|
||||
{open ? <X size={28} /> : <Menu size={28} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Drawer Overlay */}
|
||||
<div
|
||||
className={`fixed inset-0 z-40 bg-black/30 transition-opacity duration-200 md:hidden ${
|
||||
open ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Side Navigation */}
|
||||
<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-[250px] min-w-[200px] md:translate-x-0 md:w-[250px] md:min-w-[250px] md:max-w-[250px] ${
|
||||
open ? "translate-x-0" : "-translate-x-full"
|
||||
} md:translate-x-0`}
|
||||
>
|
||||
{/* Logo Section */}
|
||||
<div className="flex-shrink-0 px-4 pb-4 flex flex-col gap-1 md:block mb-5 pt-16 md:pt-4">
|
||||
<div className="flex items-center gap-3 mb-1 ml-4 md:ml-6">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-gradient-to-br from-rose-100 to-pink-100">
|
||||
<Heart className="w-6 h-6 text-rose-600" fill="currentColor" />
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-gray-900">Attune Heart</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="flex-shrink-0 -mt-10 mb-4 mx-4 border-gray-200 md:block hidden" />
|
||||
|
||||
{/* Navigation Items */}
|
||||
<nav className="flex-1 overflow-y-auto flex flex-col gap-2 px-2 md:px-0">
|
||||
{navItems.map((item, idx) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = idx === getActiveIndex();
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center w-full" key={item.label}>
|
||||
{isActive && (
|
||||
<span
|
||||
className="absolute left-0 top-0 h-[45px] w-[3px] bg-[#4A90A4]"
|
||||
style={{ left: 0 }}
|
||||
/>
|
||||
)}
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className={`group flex items-center gap-3 py-3 pl-4 md:pl-4 pr-4 md:pr-4 transition-colors duration-200 focus:outline-none w-[90%] md:w-[90%] ml-2 md:ml-4 cursor-pointer justify-start ${
|
||||
isActive
|
||||
? "bg-[#4A90A4] text-white border border-[#4A90A4] rounded-[5px] shadow-sm"
|
||||
: "bg-transparent text-gray-600 hover:bg-[#4A90A4]/10 hover:text-[#4A90A4] rounded-lg"
|
||||
}`}
|
||||
style={isActive ? { height: 45 } : {}}
|
||||
>
|
||||
<Icon
|
||||
size={20}
|
||||
strokeWidth={isActive ? 2.2 : 1.5}
|
||||
className={
|
||||
isActive
|
||||
? "text-white"
|
||||
: "text-gray-700 group-hover:text-[#4A90A4]"
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="font-light leading-none text-[13px] md:text-[13px]"
|
||||
style={{ fontWeight: 300 }}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="mt-auto pt-4 pb-4 border-t border-gray-200">
|
||||
<Link
|
||||
href="/settings"
|
||||
onClick={() => setOpen(false)}
|
||||
className="group flex items-center gap-3 py-3 pl-4 md:pl-4 pr-4 md:pr-4 transition-colors duration-200 w-[90%] md:w-[90%] ml-2 md:ml-4 cursor-pointer justify-start text-gray-600 hover:bg-gray-50 hover:text-gray-900 rounded-lg"
|
||||
>
|
||||
<Settings size={20} strokeWidth={1.5} className="text-gray-700 group-hover:text-gray-900" />
|
||||
<span className="font-light leading-none text-[13px]" style={{ fontWeight: 300 }}>
|
||||
Settings
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
// Handle logout
|
||||
}}
|
||||
className="group flex items-center gap-3 py-3 pl-4 md:pl-4 pr-4 md:pr-4 transition-colors duration-200 w-[90%] md:w-[90%] ml-2 md:ml-4 cursor-pointer justify-start text-gray-600 hover:bg-gray-50 hover:text-gray-900 rounded-lg"
|
||||
>
|
||||
<LogOut size={20} strokeWidth={1.5} className="text-gray-700 group-hover:text-gray-900" />
|
||||
<span className="font-light leading-none text-[13px]" style={{ fontWeight: 300 }}>
|
||||
Logout
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
60
components/ui/button.tsx
Normal file
60
components/ui/button.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
21
components/ui/input.tsx
Normal file
21
components/ui/input.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
19
package.json
19
package.json
@ -12,18 +12,25 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.552.0",
|
||||
"next": "16.0.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"next": "16.0.1"
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.1"
|
||||
}
|
||||
"eslint-config-next": "16.0.1",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
||||
}
|
||||
|
||||
@ -8,6 +8,18 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(@types/react@19.2.2)(react@19.2.0)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
lucide-react:
|
||||
specifier: ^0.552.0
|
||||
version: 0.552.0(react@19.2.0)
|
||||
next:
|
||||
specifier: 16.0.1
|
||||
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@ -17,6 +29,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: 19.2.0
|
||||
version: 19.2.0(react@19.2.0)
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
devDependencies:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4
|
||||
@ -39,6 +54,9 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^4
|
||||
version: 4.1.16
|
||||
tw-animate-css:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
@ -394,6 +412,24 @@ packages:
|
||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||
engines: {node: '>=12.4.0'}
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.4':
|
||||
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
@ -785,9 +821,16 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -1446,6 +1489,11 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.552.0:
|
||||
resolution: {integrity: sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@ -1781,6 +1829,9 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
tailwind-merge@3.3.1:
|
||||
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||
|
||||
tailwindcss@4.1.16:
|
||||
resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
|
||||
|
||||
@ -1808,6 +1859,9 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tw-animate-css@1.4.0:
|
||||
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -2236,6 +2290,19 @@ snapshots:
|
||||
|
||||
'@nolyfill/is-core-module@1.0.39': {}
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.2.0)':
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
|
||||
'@radix-ui/react-slot@1.2.4(@types/react@19.2.2)(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
|
||||
react: 19.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
@ -2637,8 +2704,14 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@ -3427,6 +3500,10 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.552.0(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@ -3828,6 +3905,8 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
tailwind-merge@3.3.1: {}
|
||||
|
||||
tailwindcss@4.1.16: {}
|
||||
|
||||
tapable@2.3.0: {}
|
||||
@ -3854,6 +3933,8 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tw-animate-css@1.4.0: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
BIN
public/doctors.png
Normal file
BIN
public/doctors.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 571 KiB |
Loading…
Reference in New Issue
Block a user