Update dependencies and improve styling in layout and global CSS. Added new packages for UI components and animations, and refactored CSS variables for better theme management.

This commit is contained in:
iamkiddy 2025-11-06 12:34:29 +00:00
parent b7dfd11d4f
commit 4d7201d03e
16 changed files with 927 additions and 34 deletions

View 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>
);
}

View 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
View 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
View 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>
);
}

View 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>
);
}

View File

@ -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;
}

View File

@ -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
View 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": {}
}

View 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
View 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
View 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
View 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
View 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))
}

View File

@ -8,22 +8,29 @@
"start": "next start",
"lint": "eslint"
},
"engines": {
"engines": {
"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"
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB