Task 4: Add PatientBanner component with full/condensed modes
- Create useScrollCondensation hook with IntersectionObserver - PatientBanner with 80px full mode and 48px condensed mode - Smooth height transition (200ms) on scroll past 100px - Status dot (green) and badge for 'Open to opportunities' - Action buttons: Download CV, Email, LinkedIn - NHS blue outlined buttons with hover fill effect - Proper GPhC number formatting with tooltip - Sticky positioning for persistent visibility
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import { Download, Mail, Linkedin } from 'lucide-react'
|
||||
import { patient } from '@/data/patient'
|
||||
import { useScrollCondensation } from '@/hooks/useScrollCondensation'
|
||||
|
||||
export function PatientBanner() {
|
||||
const { isCondensed, sentinelRef } = useScrollCondensation({ threshold: 100 })
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={sentinelRef}
|
||||
className="h-0 w-full absolute top-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<header
|
||||
className={`
|
||||
sticky top-0 z-40 w-full
|
||||
bg-pmr-banner border-b border-slate-600
|
||||
transition-all duration-200 ease-out
|
||||
${isCondensed ? 'h-12' : 'h-20'}
|
||||
`}
|
||||
role="banner"
|
||||
>
|
||||
{isCondensed ? (
|
||||
<CondensedBanner />
|
||||
) : (
|
||||
<FullBanner />
|
||||
)}
|
||||
</header>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FullBanner() {
|
||||
return (
|
||||
<div className="h-full px-4 lg:px-6 flex flex-col justify-center">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h1 className="font-inter font-semibold text-white text-lg tracking-tight">
|
||||
{patient.name}
|
||||
</h1>
|
||||
<StatusDot status={patient.status} />
|
||||
<span className="text-slate-400 text-sm">{patient.status}</span>
|
||||
<StatusBadge badge={patient.badge} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 flex-wrap text-sm text-slate-300">
|
||||
<span>
|
||||
<span className="text-slate-500">DOB:</span> {patient.dob}
|
||||
</span>
|
||||
<span className="text-slate-500">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-slate-500">NHS No:</span>{' '}
|
||||
<span className="font-geist" title={patient.nhsNumberTooltip}>
|
||||
{patient.nhsNumber}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-slate-500">|</span>
|
||||
<span>{patient.address}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 flex-wrap text-sm text-slate-300">
|
||||
<a
|
||||
href={`tel:${patient.phone}`}
|
||||
className="hover:text-white transition-colors"
|
||||
>
|
||||
{patient.phone}
|
||||
</a>
|
||||
<span className="text-slate-500">|</span>
|
||||
<a
|
||||
href={`mailto:${patient.email}`}
|
||||
className="hover:text-white transition-colors"
|
||||
>
|
||||
{patient.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<ActionButton
|
||||
icon={<Download size={14} />}
|
||||
label="Download CV"
|
||||
href="/cv.pdf"
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<Mail size={14} />}
|
||||
label="Email"
|
||||
href={`mailto:${patient.email}`}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<Linkedin size={14} />}
|
||||
label="LinkedIn"
|
||||
href={`https://${patient.linkedin}`}
|
||||
external
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CondensedBanner() {
|
||||
return (
|
||||
<div className="h-full px-4 lg:px-6 flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<h1 className="font-inter font-semibold text-white text-sm tracking-tight truncate">
|
||||
{patient.name}
|
||||
</h1>
|
||||
<span className="text-slate-500">|</span>
|
||||
<span className="flex items-center gap-1 text-sm text-slate-300">
|
||||
<span className="text-slate-500">NHS No:</span>{' '}
|
||||
<span className="font-geist" title={patient.nhsNumberTooltip}>
|
||||
{patient.nhsNumber}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-slate-500">|</span>
|
||||
<StatusDot status={patient.status} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<ActionButton
|
||||
icon={<Download size={14} />}
|
||||
label="Download CV"
|
||||
href="/cv.pdf"
|
||||
compact
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<Mail size={14} />}
|
||||
label="Email"
|
||||
href={`mailto:${patient.email}`}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface StatusDotProps {
|
||||
status: string
|
||||
}
|
||||
|
||||
function StatusDot({ status }: StatusDotProps) {
|
||||
const colorClass = status === 'Active' ? 'bg-pmr-green' : 'bg-slate-400'
|
||||
return (
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${colorClass} flex-shrink-0`}
|
||||
aria-label={`Status: ${status}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface StatusBadgeProps {
|
||||
badge: string
|
||||
}
|
||||
|
||||
function StatusBadge({ badge }: StatusBadgeProps) {
|
||||
return (
|
||||
<span className="px-2 py-0.5 bg-pmr-nhsblue text-white text-xs font-medium rounded-sm">
|
||||
{badge}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface ActionButtonProps {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
function ActionButton({ icon, label, href, external, compact }: ActionButtonProps) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopener noreferrer' : undefined}
|
||||
className={`
|
||||
inline-flex items-center gap-1.5
|
||||
border border-pmr-nhsblue text-pmr-nhsblue
|
||||
hover:bg-pmr-nhsblue hover:text-white
|
||||
transition-colors duration-100
|
||||
rounded
|
||||
${compact ? 'px-2 py-1 text-xs' : 'px-3 py-1.5 text-sm'}
|
||||
font-inter font-medium
|
||||
`}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user