Refactor to pull all text enteries into single location

This commit is contained in:
2026-02-17 01:10:31 +00:00
parent 6605966fab
commit 83b327d58e
36 changed files with 954 additions and 1443 deletions
+21 -14
View File
@@ -10,6 +10,7 @@ import {
import { CardHeader } from './Card'
import { skills } from '@/data/skills'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import { getSkillsUICopy } from '@/lib/profile-content'
import type { SkillMedication, SkillCategory } from '@/types/pmr'
const iconMap: Record<string, LucideIcon> = {
@@ -21,19 +22,14 @@ const iconMap: Record<string, LucideIcon> = {
const SKILLS_PER_CATEGORY = 4
const categoryConfig: { id: SkillCategory; label: string }[] = [
{ id: 'Technical', label: 'Technical' },
{ id: 'Domain', label: 'Healthcare Domain' },
{ id: 'Leadership', label: 'Strategic & Leadership' },
]
interface SkillRowProps {
skill: SkillMedication
yearsSuffix: string
onClick: () => void
onHighlight?: (id: string | null) => void
}
function SkillRow({ skill, onClick, onHighlight }: SkillRowProps) {
function SkillRow({ skill, yearsSuffix, onClick, onHighlight }: SkillRowProps) {
const IconComponent = iconMap[skill.icon]
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -106,7 +102,7 @@ function SkillRow({ skill, onClick, onHighlight }: SkillRowProps) {
fontFamily: '"Geist Mono", monospace',
}}
>
{skill.frequency} · {skill.yearsOfExperience} yrs
{skill.frequency} · {skill.yearsOfExperience} {yearsSuffix}
</div>
</div>
<div
@@ -135,6 +131,9 @@ interface CategorySectionProps {
label: string
categoryId: SkillCategory
skills: SkillMedication[]
itemCountSuffix: string
yearsSuffix: string
viewAllLabel: string
onSkillClick: (skill: SkillMedication) => void
onViewAll: (category: SkillCategory) => void
isFirst: boolean
@@ -145,6 +144,9 @@ function CategorySection({
label,
categoryId,
skills: categorySkills,
itemCountSuffix,
yearsSuffix,
viewAllLabel,
onSkillClick,
onViewAll,
isFirst,
@@ -190,7 +192,7 @@ function CategorySection({
whiteSpace: 'nowrap',
}}
>
{categorySkills.length} items
{categorySkills.length} {itemCountSuffix}
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
@@ -198,6 +200,7 @@ function CategorySection({
<SkillRow
key={skill.id}
skill={skill}
yearsSuffix={yearsSuffix}
onClick={() => onSkillClick(skill)}
onHighlight={onNodeHighlight}
/>
@@ -228,9 +231,9 @@ function CategorySection({
onMouseLeave={(e) => {
e.currentTarget.style.color = 'var(--accent)'
}}
aria-label={`View all ${categorySkills.length} ${label} skills`}
aria-label={`${viewAllLabel} ${categorySkills.length} ${label} skills`}
>
View all ({categorySkills.length})
{viewAllLabel} ({categorySkills.length})
<ChevronRight size={12} />
</button>
)}
@@ -244,8 +247,9 @@ interface RepeatMedicationsSubsectionProps {
export function RepeatMedicationsSubsection({ onNodeHighlight }: RepeatMedicationsSubsectionProps) {
const { openPanel } = useDetailPanel()
const skillsCopy = getSkillsUICopy()
const groupedSkills = categoryConfig.map(({ id, label }) => ({
const groupedSkills = skillsCopy.categories.map(({ id, label }) => ({
id,
label,
skills: skills
@@ -265,8 +269,8 @@ export function RepeatMedicationsSubsection({ onNodeHighlight }: RepeatMedicatio
<div>
<CardHeader
dotColor="amber"
title="REPEAT MEDICATIONS"
rightText="Active prescriptions"
title={skillsCopy.sectionTitle}
rightText={skillsCopy.rightText}
/>
<div className="medications-grid">
{groupedSkills.map((group) => (
@@ -275,6 +279,9 @@ export function RepeatMedicationsSubsection({ onNodeHighlight }: RepeatMedicatio
label={group.label}
categoryId={group.id}
skills={group.skills}
itemCountSuffix={skillsCopy.itemCountSuffix}
yearsSuffix={skillsCopy.yearsSuffix}
viewAllLabel={skillsCopy.viewAllLabel}
onSkillClick={handleSkillClick}
onViewAll={handleViewAll}
isFirst
+17 -15
View File
@@ -17,6 +17,7 @@ import cvmisLogo from '../../cvmis-logo.svg'
import { patient } from '@/data/patient'
import { tags } from '@/data/tags'
import { alerts } from '@/data/alerts'
import { getSidebarCopy } from '@/lib/profile-content'
import type { Tag, Alert } from '@/types/pmr'
interface SidebarProps {
@@ -163,6 +164,7 @@ function AlertFlag({ alert }: AlertFlagProps) {
}
export default function Sidebar({ activeSection, onNavigate, onSearchClick }: SidebarProps) {
const sidebarCopy = getSidebarCopy()
const [isDesktop, setIsDesktop] = useState(() => window.matchMedia('(min-width: 1024px)').matches)
const [isMobileExpanded, setIsMobileExpanded] = useState(false)
@@ -257,7 +259,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
cursor: 'pointer',
}}
>
{isExpanded && <span style={{ fontSize: '12px', fontWeight: 600 }}>Menu</span>}
{isExpanded && <span style={{ fontSize: '12px', fontWeight: 600 }}>{sidebarCopy.menuLabel}</span>}
{isExpanded ? <X size={17} strokeWidth={2.4} /> : <Menu size={18} strokeWidth={2.4} />}
</button>
)}
@@ -287,7 +289,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
type="button"
onClick={onSearchClick}
className="sidebar-control"
aria-label="Search. Press Control plus K"
aria-label={sidebarCopy.searchAriaLabel}
style={{
width: '100%',
minHeight: '44px',
@@ -305,7 +307,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
>
<Search size={16} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }} aria-hidden="true" />
<span style={{ flex: 1, textAlign: 'left', fontSize: '13px' }}>
Search
{sidebarCopy.searchLabel}
</span>
<kbd
style={{
@@ -318,7 +320,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
lineHeight: 1,
}}
>
Ctrl+K
{sidebarCopy.searchShortcut}
</kbd>
</button>
@@ -326,7 +328,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
<SectionTitle>Patient Data</SectionTitle>
<SectionTitle>{sidebarCopy.sectionTitle}</SectionTitle>
<div
style={{
@@ -367,7 +369,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
marginTop: '2px',
}}
>
Pharmacy Data Technologist
{sidebarCopy.roleTitle}
</div>
<div
@@ -387,7 +389,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
padding: '4px 0',
}}
>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>GPhC No.</span>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>{sidebarCopy.gphcLabel}</span>
<span
style={{
color: 'var(--text-primary)',
@@ -410,7 +412,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
padding: '4px 0',
}}
>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>Education</span>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>{sidebarCopy.educationLabel}</span>
<span style={{ color: 'var(--text-primary)', fontWeight: 500, textAlign: 'right' }}>
{patient.qualification}
</span>
@@ -425,7 +427,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
padding: '4px 0',
}}
>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>Location</span>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>{sidebarCopy.locationLabel}</span>
<span style={{ color: 'var(--text-primary)', fontWeight: 500, textAlign: 'right' }}>
{patient.address}
</span>
@@ -440,7 +442,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
padding: '4px 0',
}}
>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>Phone</span>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>{sidebarCopy.phoneLabel}</span>
<a
href={`tel:${patient.phone}`}
style={{
@@ -465,7 +467,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
padding: '4px 0',
}}
>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>Email</span>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>{sidebarCopy.emailLabel}</span>
<a
href={`mailto:${patient.email}`}
style={{
@@ -490,7 +492,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
padding: '4px 0',
}}
>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>Registered</span>
<span style={{ color: 'var(--text-tertiary)', fontWeight: 400 }}>{sidebarCopy.registeredLabel}</span>
<span style={{ color: 'var(--text-primary)', fontWeight: 500, textAlign: 'right' }}>
{patient.registrationYear}
</span>
@@ -500,7 +502,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
)}
<section>
{isExpanded && <SectionTitle>Navigation</SectionTitle>}
{isExpanded && <SectionTitle>{sidebarCopy.navigationTitle}</SectionTitle>}
<nav aria-label="Sidebar navigation" style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{navSections.map((section) => {
const isActive = activeSection === section.id
@@ -546,7 +548,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
{isExpanded && (
<>
<section style={{ paddingTop: '8px' }}>
<SectionTitle>Tags</SectionTitle>
<SectionTitle>{sidebarCopy.tagsTitle}</SectionTitle>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '5px' }}>
{tags.map((tag) => (
<TagPill key={tag.label} tag={tag} />
@@ -555,7 +557,7 @@ export default function Sidebar({ activeSection, onNavigate, onSearchClick }: Si
</section>
<section style={{ padding: '8px 0 4px' }}>
<SectionTitle>Alerts / Highlights</SectionTitle>
<SectionTitle>{sidebarCopy.alertsTitle}</SectionTitle>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{alerts.map((alert, index) => (
<AlertFlag key={index} alert={alert} />
@@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion'
import { ChevronRight } from 'lucide-react'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import { timelineEntities, timelineConsultations } from '@/data/timeline'
import { getExperienceEducationUICopy } from '@/lib/profile-content'
import type { TimelineEntity } from '@/types/pmr'
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
@@ -33,8 +34,9 @@ function TimelineInterventionItem({
onViewFull,
onHighlight,
}: TimelineInterventionItemProps) {
const experienceEducationCopy = getExperienceEducationUICopy()
const isEducation = entity.kind === 'education'
const interventionLabel = isEducation ? 'Education' : 'Employment'
const interventionLabel = isEducation ? experienceEducationCopy.educationLabel : experienceEducationCopy.employmentLabel
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -284,7 +286,7 @@ function TimelineInterventionItem({
e.currentTarget.style.opacity = '1'
}}
>
View full record
{experienceEducationCopy.viewFullRecordLabel}
<ChevronRight size={12} />
</button>
</div>
+5 -5
View File
@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronRight } from 'lucide-react'
import { CardHeader } from './Card'
import { consultations } from '@/data/consultations'
import { timelineConsultations } from '@/data/timeline'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
@@ -15,7 +15,7 @@ function hexToRgba(hex: string, opacity: number): string {
}
interface RoleItemProps {
consultation: typeof consultations[0]
consultation: typeof timelineConsultations[0]
isExpanded: boolean
isHighlightedFromGraph: boolean
onToggle: () => void
@@ -279,7 +279,7 @@ export function WorkExperienceSubsection({ onNodeHighlight, highlightedRoleId }:
}, [])
const handleViewFull = useCallback(
(consultation: typeof consultations[0]) => {
(consultation: typeof timelineConsultations[0]) => {
openPanel({ type: 'career-role', consultation })
},
[openPanel],
@@ -287,9 +287,9 @@ export function WorkExperienceSubsection({ onNodeHighlight, highlightedRoleId }:
return (
<div>
<CardHeader dotColor="teal" title="WORK EXPERIENCE" rightText={`${consultations.length} roles`} />
<CardHeader dotColor="teal" title="WORK EXPERIENCE" rightText={`${timelineConsultations.length} roles`} />
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{consultations.map((c) => (
{timelineConsultations.map((c) => (
<RoleItem
key={c.id}
consultation={c}
+18 -20
View File
@@ -9,6 +9,7 @@ import {
} from 'lucide-react'
import { skills } from '@/data/skills'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import { getSkillsUICopy } from '@/lib/profile-content'
import type { SkillMedication, SkillCategory } from '@/types/pmr'
const iconMap: Record<string, LucideIcon> = {
@@ -18,12 +19,6 @@ const iconMap: Record<string, LucideIcon> = {
MessageSquare, UserPlus, RefreshCw, Calculator, Presentation,
}
const categoryConfig: { id: SkillCategory; label: string }[] = [
{ id: 'Technical', label: 'Technical' },
{ id: 'Domain', label: 'Healthcare Domain' },
{ id: 'Leadership', label: 'Strategic & Leadership' },
]
interface SkillsAllDetailProps {
category?: SkillCategory
}
@@ -31,6 +26,7 @@ interface SkillsAllDetailProps {
export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
const { openPanel } = useDetailPanel()
const categoryRefs = useRef<Record<string, HTMLDivElement | null>>({})
const skillsCopy = getSkillsUICopy()
// Scroll to highlighted category on mount
useEffect(() => {
@@ -39,7 +35,7 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
}
}, [category])
const groupedSkills = categoryConfig.map(({ id, label }) => ({
const groupedSkills = skillsCopy.categories.map(({ id, label }) => ({
id,
label,
skills: skills
@@ -91,17 +87,17 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
background: 'var(--border-light)',
}}
/>
<span
style={{
fontSize: '10px',
color: 'var(--text-tertiary)',
fontFamily: '"Geist Mono", monospace',
whiteSpace: 'nowrap',
}}
>
{group.skills.length} items
</span>
</div>
<span
style={{
fontSize: '10px',
color: 'var(--text-tertiary)',
fontFamily: '"Geist Mono", monospace',
whiteSpace: 'nowrap',
}}
>
{group.skills.length} {skillsCopy.itemCountSuffix}
</span>
</div>
{/* Skill rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
@@ -109,6 +105,7 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
<SkillRow
key={skill.id}
skill={skill}
yearsSuffix={skillsCopy.yearsSuffix}
onClick={() => handleSkillClick(skill)}
/>
))}
@@ -122,10 +119,11 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
interface SkillRowProps {
skill: SkillMedication
yearsSuffix: string
onClick: () => void
}
function SkillRow({ skill, onClick }: SkillRowProps) {
function SkillRow({ skill, yearsSuffix, onClick }: SkillRowProps) {
const IconComponent = iconMap[skill.icon]
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -198,7 +196,7 @@ function SkillRow({ skill, onClick }: SkillRowProps) {
fontFamily: '"Geist Mono", monospace',
}}
>
{skill.frequency} · {skill.yearsOfExperience} yrs
{skill.frequency} · {skill.yearsOfExperience} {yearsSuffix}
</div>
</div>
+11 -13
View File
@@ -5,6 +5,7 @@ import { ParentSection } from '../ParentSection'
import { kpis } from '@/data/kpis'
import type { KPI } from '@/types/pmr'
import { useDetailPanel } from '@/contexts/DetailPanelContext'
import { getLatestResultsCopy, getProfileSectionTitle, getProfileSummaryText } from '@/lib/profile-content'
const colorMap: Record<KPI['colorVariant'], string> = {
green: '#059669',
@@ -18,6 +19,7 @@ interface MetricCardProps {
function MetricCard({ kpi }: MetricCardProps) {
const { openPanel } = useDetailPanel()
const latestResultsCopy = getLatestResultsCopy()
const handleClick = () => {
openPanel({ type: 'kpi', kpi })
@@ -102,7 +104,7 @@ function MetricCard({ kpi }: MetricCardProps) {
fontFamily: 'var(--font-geist-mono)',
}}
>
Click to view evidence
{latestResultsCopy.evidenceCta}
<ChevronRight size={12} />
</div>
</button>
@@ -110,6 +112,10 @@ function MetricCard({ kpi }: MetricCardProps) {
}
export function PatientSummaryTile() {
const summaryText = getProfileSummaryText()
const latestResultsCopy = getLatestResultsCopy()
const sectionTitle = getProfileSectionTitle()
const profileTextStyles: React.CSSProperties = {
fontSize: '15px',
lineHeight: '1.65',
@@ -123,22 +129,14 @@ export function PatientSummaryTile() {
}
return (
<ParentSection title="Patient Summary" tileId="patient-summary">
<ParentSection title={sectionTitle} tileId="patient-summary">
{/* Profile text */}
<div style={profileTextStyles}>
<strong>Healthcare leader</strong> combining clinical pharmacy expertise with proficiency in{' '}
<strong>Python, SQL, and data analytics</strong>, self-taught over the past decade through a drive to find root causes in data and build the most efficient solutions to complex problems. Currently{' '}
<strong>leading population health analytics for NHS Norfolk & Waveney ICB</strong>, serving a population of 1.2 million. Experienced in working with messy, real-world prescribing data at scale to deliver actionable insightsfrom{' '}
<strong>financial scenario modelling</strong> and <strong>pharmaceutical rebate negotiation</strong> to{' '}
<strong>algorithm design</strong> and <strong>population-level pathway development</strong>. Proven track record of identifying and prioritising efficiency programmes worth{' '}
<strong>£14.6M+</strong> through automated, data-driven analysis. Skilled at translating complex clinical, financial, and analytical requirements into clear recommendations for{' '}
<strong>executive stakeholders</strong>.
</div>
<div style={profileTextStyles}>{summaryText}</div>
{/* Latest Results subsection */}
<div style={{ marginTop: '28px' }}>
<div className="latest-results-header">
<CardHeader dotColor="teal" title="LATEST RESULTS (CLICK TO VIEW FULL REFERENCE RANGE)" rightText="Updated May 2025" />
<CardHeader dotColor="teal" title={latestResultsCopy.title} rightText={latestResultsCopy.rightText} />
<p
style={{
margin: 0,
@@ -147,7 +145,7 @@ export function PatientSummaryTile() {
fontFamily: 'var(--font-geist-mono)',
}}
>
Select a metric to inspect methodology, impact, and outcomes.
{latestResultsCopy.helperText}
</p>
</div>
<div className="latest-results-grid" style={kpiGridStyles}>