Tasks 5-6: Build Sidebar with PersonHeader, Tags, and Alerts
- Created src/components/Sidebar.tsx: - PersonHeader section with 52px avatar, name, title, status badge with pulse animation - Details grid: GPhC No. (monospace), Education, Location, Phone (link), Email (link), Registered - Tags section with colored pill badges (teal/amber/green variants) - Alerts/Highlights section with severity-based styling (alert/amber) - Section title component with divider line - Custom scrollbar styling (4px, transparent track, border-colored thumb) - Added animations to src/index.css: - @keyframes pulse for status badge dot (opacity 1→0.4→1, 2s infinite) - .pmr-scrollbar custom scrollbar styles Data sources: patient.ts, tags.ts, alerts.ts Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
import { AlertTriangle, AlertCircle } from 'lucide-react'
|
||||
import { patient } from '@/data/patient'
|
||||
import { tags } from '@/data/tags'
|
||||
import { alerts } from '@/data/alerts'
|
||||
import type { Tag, Alert } from '@/types/pmr'
|
||||
|
||||
interface SectionTitleProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function SectionTitle({ children }: SectionTitleProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
color: 'var(--text-tertiary)',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
<span>{children}</span>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
height: '1px',
|
||||
background: 'var(--border-light)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TagPillProps {
|
||||
tag: Tag
|
||||
}
|
||||
|
||||
function TagPill({ tag }: TagPillProps) {
|
||||
const styles: Record<Tag['colorVariant'], React.CSSProperties> = {
|
||||
teal: {
|
||||
background: 'var(--accent-light)',
|
||||
color: 'var(--accent)',
|
||||
border: '1px solid var(--accent-border)',
|
||||
},
|
||||
amber: {
|
||||
background: 'var(--amber-light)',
|
||||
color: 'var(--amber)',
|
||||
border: '1px solid var(--amber-border)',
|
||||
},
|
||||
green: {
|
||||
background: 'var(--success-light)',
|
||||
color: 'var(--success)',
|
||||
border: '1px solid var(--success-border)',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
fontSize: '10.5px',
|
||||
fontWeight: 500,
|
||||
padding: '3px 8px',
|
||||
borderRadius: '4px',
|
||||
lineHeight: 1.3,
|
||||
...styles[tag.colorVariant],
|
||||
}}
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface AlertFlagProps {
|
||||
alert: Alert
|
||||
}
|
||||
|
||||
function AlertFlag({ alert }: AlertFlagProps) {
|
||||
const Icon = alert.icon === 'AlertTriangle' ? AlertTriangle : AlertCircle
|
||||
|
||||
const styles: Record<Alert['severity'], React.CSSProperties> = {
|
||||
alert: {
|
||||
background: 'var(--alert-light)',
|
||||
color: 'var(--alert)',
|
||||
border: '1px solid var(--alert-border)',
|
||||
},
|
||||
amber: {
|
||||
background: 'var(--amber-light)',
|
||||
color: 'var(--amber)',
|
||||
border: '1px solid var(--amber-border)',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
letterSpacing: '0.02em',
|
||||
...styles[alert.severity],
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon size={14} strokeWidth={2.5} />
|
||||
</div>
|
||||
<span>{alert.message}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside
|
||||
style={{
|
||||
width: 'var(--sidebar-width)',
|
||||
minWidth: 'var(--sidebar-width)',
|
||||
background: 'var(--sidebar-bg)',
|
||||
borderRight: '1px solid var(--border)',
|
||||
overflowY: 'auto',
|
||||
padding: '20px 16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
}}
|
||||
className="pmr-scrollbar"
|
||||
>
|
||||
{/* PersonHeader Section */}
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '2px solid var(--accent)',
|
||||
paddingBottom: '16px',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
style={{
|
||||
width: '52px',
|
||||
height: '52px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, var(--accent), #0A8080)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#FFFFFF',
|
||||
fontSize: '18px',
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 8px rgba(13,110,110,0.25)',
|
||||
marginBottom: '12px',
|
||||
}}
|
||||
>
|
||||
AC
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: 700,
|
||||
color: 'var(--text-primary)',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
CHARLWOOD, Andrew
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11.5px',
|
||||
fontFamily: 'Geist Mono, monospace',
|
||||
fontWeight: 400,
|
||||
color: 'var(--text-secondary)',
|
||||
marginTop: '2px',
|
||||
}}
|
||||
>
|
||||
Pharmacy Data Technologist
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
marginTop: '8px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
color: 'var(--success)',
|
||||
background: 'var(--success-light)',
|
||||
border: '1px solid var(--success-border)',
|
||||
padding: '3px 9px',
|
||||
borderRadius: '20px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--success)',
|
||||
animation: 'pulse 2s infinite',
|
||||
}}
|
||||
/>
|
||||
<span>{patient.badge}</span>
|
||||
</div>
|
||||
|
||||
{/* Details grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gap: '6px',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
{/* GPhC No. */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '11.5px',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>
|
||||
GPhC No.
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--text-primary)',
|
||||
fontWeight: 500,
|
||||
fontFamily: 'Geist Mono, monospace',
|
||||
fontSize: '11px',
|
||||
letterSpacing: '0.12em',
|
||||
}}
|
||||
>
|
||||
{patient.nhsNumber.replace(/\s/g, '')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Education */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '11.5px',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>
|
||||
Education
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--text-primary)',
|
||||
fontWeight: 500,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{patient.qualification}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '11.5px',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>
|
||||
Location
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--text-primary)',
|
||||
fontWeight: 500,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{patient.address}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '11.5px',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>
|
||||
Phone
|
||||
</span>
|
||||
<a
|
||||
href={`tel:${patient.phone}`}
|
||||
style={{
|
||||
color: 'var(--accent)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.textDecoration = 'underline')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.textDecoration = 'none')
|
||||
}
|
||||
>
|
||||
{patient.phone.replace(/(\d{5})(\d{6})/, '$1 $2')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '11.5px',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>
|
||||
Email
|
||||
</span>
|
||||
<a
|
||||
href={`mailto:${patient.email}`}
|
||||
style={{
|
||||
color: 'var(--accent)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.textDecoration = 'underline')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.textDecoration = 'none')
|
||||
}
|
||||
>
|
||||
{patient.email}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Registered */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '11.5px',
|
||||
padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>
|
||||
Registered
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--text-primary)',
|
||||
fontWeight: 500,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{patient.registrationYear}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags Section */}
|
||||
<div style={{ padding: '14px 0 6px' }}>
|
||||
<SectionTitle>Tags</SectionTitle>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '5px',
|
||||
}}
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<TagPill key={tag.label} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alerts / Highlights Section */}
|
||||
<div style={{ padding: '14px 0 6px' }}>
|
||||
<SectionTitle>Alerts / Highlights</SectionTitle>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
{alerts.map((alert, index) => (
|
||||
<AlertFlag key={index} alert={alert} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -232,3 +232,36 @@ body {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Pulse animation for status badge dot */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar for sidebar */
|
||||
.pmr-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
|
||||
.pmr-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.pmr-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pmr-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pmr-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user