Files
DetnalCare/components/landing/navbar.tsx
Iliyan Angelov 39077550ef Dental Care
2025-11-16 14:29:51 +02:00

652 lines
24 KiB
TypeScript

"use client";
import { MenuIcon, SearchIcon, LogOut, User, Shield } from "lucide-react";
import Link from "next/link";
import { useState, useEffect, useRef } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import Image from "next/image";
import { authClient } from "@/lib/auth-session/auth-client";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
import { ModeToggle } from "../ui/mode-toggle";
import { cn } from "@/lib/utils";
type User = {
id: string;
name: string;
email: string;
image?: string | null;
role?: string;
} | null;
type NavbarProps = {
user?: User;
isAdmin?: boolean;
};
const Navbar = ({ user, isAdmin: userIsAdmin }: NavbarProps) => {
const [isScrolled, setIsScrolled] = useState(false);
const router = useRouter();
useEffect(() => {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
setIsScrolled(window.scrollY > 50);
ticking = false;
});
ticking = true;
}
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const [showRequiredDialog, setShowRequiredDialog] = useState(false);
const [searchValue, setSearchValue] = useState("");
const [showSuggestions, setShowSuggestions] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const handleSignOut = async () => {
setShowLogoutDialog(false);
try {
await authClient.signOut();
toast.success("Signed out successfully");
router.push("/sign-in");
router.refresh();
} catch {
toast.error("Failed to sign out");
}
};
// Handle search submit
const handleSearch = (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (searchValue.trim()) {
router.push(`/search?query=${encodeURIComponent(searchValue.trim())}`);
}
};
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
const Services = [
{
title: "Preventive Care",
description:
"Cleanings, exams, and routine check-ups to keep smiles healthy",
href: "/services/preventive-care",
},
{
title: "Cosmetic Dentistry",
description: "Teeth whitening, veneers, and smile makeovers",
href: "/services/cosmetic-dentistry",
},
{
title: "Orthodontics",
description: "Braces and clear aligners for children and adults",
href: "/services/orthodontics",
},
{
title: "Pediatric Dentistry",
description: "Gentle, kid-friendly dental care for your little ones",
href: "/services/pediatric-dentistry",
},
{
title: "Emergency Care",
description:
"Same-day treatment for tooth pain, injuries, and urgent issues",
href: "/services/emergency-care",
},
{
title: "Patient Resources",
description: "New patient forms, insurance info, and financing options",
href: "/patient-resources",
},
];
// Filtered suggestions based on searchValue
const suggestions =
searchValue.trim().length > 0
? Services.filter(
(s) =>
s.title.toLowerCase().includes(searchValue.toLowerCase()) ||
s.description.toLowerCase().includes(searchValue.toLowerCase())
)
: [];
const aboutItems = [
{
title: "Our Story",
description: "Learn about Dental U Care's mission and values",
href: "/#about",
},
{
title: "Our Team",
description: "Meet our expert dental professionals",
href: "/#team",
},
{
title: "Features",
description: "Discover our online booking system features",
href: "/#features",
},
{
title: "Pricing",
description: "Transparent pricing for all dental services",
href: "/#pricing",
},
];
return (
<section className="sticky top-0 z-50 py-2">
<div
className={cn(
"container transition-all duration-300",
isScrolled && "px-6 lg:px-12"
)}
>
<nav
className={cn(
"flex items-center justify-between rounded-full px-6 py-6 transition-all duration-300 ",
isScrolled
? "border-2 border-accent dark:border-gray-900 bg-background/80 shadow-lg"
: "border-2 border-accent dark:border-gray-800 bg-background/80 shadow-lg"
)}
>
<Link href="/" className="flex items-center gap-2">
<Image
src="/tooth.svg"
alt="Dental U Care"
width={32}
height={32}
className="h-8 w-8"
/>
<span className="text-2xl font-semibold bg-gradient-to-r from-blue-600 to-blue-800 text-transparent bg-clip-text tracking-tighter">
Dental U Care
</span>
</Link>
<NavigationMenu className="hidden lg:block">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger className="bg-transparent hover:bg-transparent focus:bg-transparent data-[active]:bg-transparent data-[state=open]:bg-transparent">
About
</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid w-[500px] grid-cols-2 p-3">
{aboutItems.map((item, index) => (
<NavigationMenuLink
href={item.href}
key={index}
className="rounded-md p-3 transition-colors"
>
<div key={item.title}>
<p className="text-foreground mb-1 font-semibold">
{item.title}
</p>
<p className="text-muted-foreground text-sm">
{item.description}
</p>
</div>
</NavigationMenuLink>
))}
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger className="bg-transparent hover:bg-transparent focus:bg-transparent data-[active]:bg-transparent data-[state=open]:bg-transparent">
Services
</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid w-[600px] grid-cols-2 p-3">
{Services.map((service, index) => (
<NavigationMenuLink
href={service.href}
key={index}
className="rounded-md p-3 transition-colors"
>
<div key={service.title}>
<p className="text-foreground mb-1 font-semibold">
{service.title}
</p>
<p className="text-muted-foreground text-sm">
{service.description}
</p>
</div>
</NavigationMenuLink>
))}
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
href="/#pricing"
className="group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-all hover:text-accent-foreground hover:border-b-2 hover:border-primary focus:outline-none disabled:pointer-events-none disabled:opacity-50"
>
Pricing
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
href="/#contact"
className="group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-all hover:text-accent-foreground hover:border-b-2 hover:border-primary focus:outline-none disabled:pointer-events-none disabled:opacity-50"
>
Contact
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
<div className="hidden items-center gap-4 lg:flex">
<div className="relative">
<form onSubmit={handleSearch} className="contents">
<InputGroup
className={cn(
"w-64 transition-all duration-300 border border-gray-300 hover:border-primary hover:shadow-sm dark:border-gray-700 dark:hover:border-primary rounded-md",
isScrolled && "w-58"
)}
>
<InputGroupInput
ref={inputRef}
placeholder="Search services..."
className="border-0 focus-visible:ring-0"
value={searchValue}
onChange={(e) => {
setSearchValue(e.target.value);
setShowSuggestions(true);
}}
onFocus={() => setShowSuggestions(true)}
onBlur={() =>
setTimeout(() => setShowSuggestions(false), 100)
}
onKeyDown={(e) => {
if (e.key === "Enter") handleSearch();
}}
aria-label="Search services"
autoComplete="off"
/>
<InputGroupAddon>
<SearchIcon className="h-4 w-4" />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton size="sm" type="submit">
Search
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
{showSuggestions && suggestions.length > 0 && (
<ul className="absolute left-0 z-50 mt-1 w-full bg-background border border-gray-200 dark:border-gray-700 rounded-md shadow-lg max-h-56 overflow-auto">
{suggestions.map((s) => (
<li
key={s.title}
className="px-4 py-2 cursor-pointer hover:bg-accent"
onMouseDown={() => {
setShowSuggestions(false);
setSearchValue("");
router.push(s.href);
}}
>
<span className="font-semibold">{s.title}</span>
<span className="block text-xs text-muted-foreground">
{s.description}
</span>
</li>
))}
</ul>
)}
</form>
</div>
<ModeToggle />
{user ? (
<>
<Button
className={cn(isScrolled ? "hidden" : "lg:inline-flex")}
asChild
>
<Link href="/patient/book-appointment">Book Now</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full"
>
<Avatar className="h-8 w-8">
<AvatarImage
src={user.image || undefined}
alt={user.name}
/>
<AvatarFallback>
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{user.name}
</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{userIsAdmin && (
<>
<DropdownMenuItem asChild>
<Link href="/admin" className="cursor-pointer">
<Shield className="mr-2 h-4 w-4" />
<span>Dashboard</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
{user?.role === "dentist" && (
<>
<DropdownMenuItem asChild>
<Link href="/dentist" className="cursor-pointer">
<User className="mr-2 h-4 w-4" />
<span>Dashboard</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
{user?.role === "patient" && (
<>
<DropdownMenuItem asChild>
<Link href="/patient" className="cursor-pointer">
<User className="mr-2 h-4 w-4" />
<span>Dashboard</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onClick={() => setShowLogoutDialog(true)}
className="cursor-pointer text-red-600"
>
<LogOut className="mr-2 h-4 w-4" />
<span>Sign Out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
) : (
<>
<Button
variant="outline"
className={cn(isScrolled ? "hidden" : "lg:inline-flex")}
>
<Link href="/sign-in">Sign In</Link>
</Button>
<Button
className={cn(isScrolled ? "hidden" : "lg:inline-flex")}
onClick={() => setShowRequiredDialog(true)}
>
Book Now
</Button>
</>
)}
</div>
<Sheet>
<SheetTrigger asChild className="lg:hidden">
<Button variant="outline" size="icon">
<MenuIcon className="h-4 w-4" />
</Button>
</SheetTrigger>
<SheetContent side="top" className="max-h-screen overflow-auto">
<SheetHeader>
<SheetTitle>
<a href="#" className="flex items-center gap-2">
<Image
src="/tooth.svg"
alt="Dental U Care"
width={32}
height={32}
className="h-8 w-8"
/>
<span className="text-lg font-semibold tracking-tighter">
Dental U Care
</span>
</a>
</SheetTitle>
</SheetHeader>
<div className="flex flex-col p-4">
<Accordion type="single" collapsible className="mb-2 mt-4">
<AccordionItem value="about" className="border-none">
<AccordionTrigger className="text-base hover:no-underline">
About
</AccordionTrigger>
<AccordionContent>
<div className="grid gap-2">
{aboutItems.map((item, index) => (
<a
href={item.href}
key={index}
className="rounded-md p-3 transition-colors"
>
<div key={item.title}>
<p className="text-foreground mb-1 font-semibold">
{item.title}
</p>
<p className="text-muted-foreground text-sm">
{item.description}
</p>
</div>
</a>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="solutions" className="border-none">
<AccordionTrigger className="text-base hover:no-underline">
Services
</AccordionTrigger>
<AccordionContent>
<div className="grid md:grid-cols-2">
{Services.map((service, index) => (
<a
href={service.href}
key={index}
className="rounded-md p-3 transition-colors"
>
<div key={service.title}>
<p className="text-foreground mb-1 font-semibold">
{service.title}
</p>
<p className="text-muted-foreground text-sm">
{service.description}
</p>
</div>
</a>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex flex-col gap-6">
<Link href="/#contact" className="font-medium">
Contact
</Link>
</div>
<div className="mt-6 flex flex-col gap-4">
{user ? (
<>
<div className="flex items-center gap-3 rounded-lg border p-4">
<Avatar className="h-10 w-10">
<AvatarImage
src={user.image || undefined}
alt={user.name}
/>
<AvatarFallback>
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<p className="text-sm font-medium leading-none">
{user.name}
</p>
<p className="text-xs leading-none text-muted-foreground mt-1">
{user.email}
</p>
</div>
</div>
<Button asChild>
<Link href="/patient/book-appointment">Book Now</Link>
</Button>
{userIsAdmin && (
<Button variant="outline" asChild>
<Link href="/admin">
<Shield className="mr-2 h-4 w-4" />
Admin Dashboard
</Link>
</Button>
)}
{user?.role === "dentist" && (
<Button variant="outline" asChild>
<Link href="/dentist">
<User className="mr-2 h-4 w-4" />
Dentist Dashboard
</Link>
</Button>
)}
{user?.role === "patient" && (
<Button variant="outline" asChild>
<Link href="/patient">
<User className="mr-2 h-4 w-4" />
Dashboard
</Link>
</Button>
)}
<Button
variant="destructive"
onClick={() => setShowLogoutDialog(true)}
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Button>
</>
) : (
<>
<Button variant="outline">
<Link href="/sign-in">Sign in</Link>
</Button>
<Button onClick={() => setShowRequiredDialog(true)}>
Book Now
</Button>
</>
)}
</div>
</div>
</SheetContent>
</Sheet>
</nav>
</div>
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Log out</DialogTitle>
<DialogDescription>
Are you sure you want to log out?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="destructive" onClick={handleSignOut}>
Log out
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showRequiredDialog} onOpenChange={setShowRequiredDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Sign in required</DialogTitle>
<DialogDescription>
You need to sign in to book an appointment. Would you like to sign
in now?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button
onClick={() => {
setShowRequiredDialog(false);
router.push("/sign-in");
}}
>
Sign in
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
);
};
export { Navbar };