Improve Experience section UI and resume parsing

This commit is contained in:
2026-04-18 16:20:12 -04:00
parent 9ce7f93abb
commit c8ad15167b
7 changed files with 248 additions and 106 deletions
+117 -39
View File
@@ -1,19 +1,42 @@
import { useEffect, useMemo, useState } from 'react'
import { Briefcase, Calendar, Building2, AlertCircle } from 'lucide-react'
import type { ExperienceItem } from '../utils/parseResume'
import { parseResumeDocx } from '../utils/parseResume'
// Fallback data if parsing fails
const fallbackExperience: ExperienceItem[] = [
{
date: '2016 Present',
role: 'Licensed Professional Counselor',
org: 'Clinical Practice',
details: [
'10 years of clinical experience in integrated treatment for co-occurring mental health and substance use disorders',
'Specializes in trauma-informed care, narrative therapy, and nature-based interventions',
'Certified Nature-Informed Therapist utilizing nature as co-therapist in clinical practice',
],
},
]
export default function Experience() {
const [items, setItems] = useState<ExperienceItem[] | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
;(async () => {
try {
const parsed = await parseResumeDocx('/docs/Peter\'s Resume.docx')
if (!cancelled) setItems(parsed)
if (!cancelled) {
setItems(parsed.length > 0 ? parsed : fallbackExperience)
setLoading(false)
}
} catch (e) {
if (!cancelled) setError(String(e))
if (!cancelled) {
setError(String(e))
setItems(fallbackExperience)
setLoading(false)
}
}
})()
return () => {
@@ -23,68 +46,123 @@ export default function Experience() {
const sorted = useMemo(() => {
if (!items?.length) return []
// best-effort sort: if span has two years, use end year else first year
const pickEnd = (d: string) => {
const m = d.match(/(19\d{2}|20\d{2})\s*[-]\s*(19\d{2}|20\d{2})/)
if (m) return Number(m[2])
const y = d.match(yearAny())
const y = d.match(/(19\d{2}|20\d{2})/)
return y ? Number(y[0]) : 0
}
function yearAny() {
return /(19\d{2}|20\d{2})/
}
return [...items].sort((a, b) => pickEnd(b.date) - pickEnd(a.date))
}, [items])
return (
<section id="experience" className="mx-auto max-w-6xl px-4 py-12 md:py-16">
<h2 className="font-serif text-2xl font-semibold text-slate-900 md:text-3xl">
Counseling Experience
</h2>
<p className="mt-2 text-slate-700">
Resume-driven timeline (best-effort extraction from uploaded DOCX).
</p>
<div className="mb-8">
<h2 className="font-serif text-2xl font-semibold text-slate-900 md:text-3xl">
Counseling Experience
</h2>
<p className="mt-2 max-w-3xl text-slate-700">
Professional timeline showcasing over a decade of clinical experience in mental health
and substance use disorder treatment.
</p>
</div>
{error ? (
<div className="mt-6 rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
Resume parse failed. Showing placeholder.
{error && (
<div className="mb-6 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50/60 p-4 text-sm text-amber-900">
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0" />
<div>
<p className="font-medium">Resume parsing issue</p>
<p className="mt-1 text-amber-800/80">
Showing curated highlights instead. For full details, download the resume.
</p>
</div>
</div>
) : null}
)}
<div className="mt-8 space-y-5" aria-live="polite">
{sorted.length ? (
<div className="mt-8 space-y-6" aria-live="polite">
{loading ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="rounded-3xl border border-slate-200 bg-white/70 p-6 animate-pulse"
>
<div className="h-6 w-1/3 rounded bg-slate-200" />
<div className="mt-2 h-4 w-1/4 rounded bg-slate-200" />
<div className="mt-4 space-y-2">
<div className="h-3 w-full rounded bg-slate-200" />
<div className="h-3 w-5/6 rounded bg-slate-200" />
</div>
</div>
))}
</div>
) : sorted.length ? (
sorted.map((item, idx) => (
<article
key={`${item.date}-${idx}`}
className="rounded-3xl border border-slate-200 bg-white/70 p-6"
className="group relative rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm transition-all hover:shadow-md hover:border-emerald-200/60"
>
<div className="flex flex-col gap-1 md:flex-row md:items-baseline md:justify-between">
<div>
<h3 className="font-serif text-lg font-semibold text-slate-900">{item.role}</h3>
{item.org ? <p className="text-sm text-slate-700">{item.org}</p> : null}
</div>
<time className="text-sm font-semibold text-emerald-800">{item.date}</time>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="flex-1">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-100/60 text-emerald-700">
<Briefcase className="h-5 w-5" />
</div>
<div>
<h3 className="font-serif text-lg font-semibold text-slate-900">
{item.role}
</h3>
{item.org && (
<div className="mt-1 flex items-center gap-1.5 text-sm text-slate-600">
<Building2 className="h-3.5 w-3.5" />
<span>{item.org}</span>
</div>
)}
</div>
</div>
{item.details?.length ? (
<ul className="mt-4 list-disc space-y-1 pl-5 text-sm text-slate-700">
{item.details.map((d, i) => (
<li key={i}>{d}</li>
))}
</ul>
) : null}
{item.details?.length ? (
<ul className="mt-4 space-y-2">
{item.details.map((d, i) => (
<li
key={i}
className="flex items-start gap-2 text-sm text-slate-700"
>
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-500/60" />
<span className="leading-relaxed">{d}</span>
</li>
))}
</ul>
) : null}
</div>
<div className="flex items-center gap-1.5 text-sm font-medium text-emerald-800/90 md:flex-col md:items-end md:text-right">
<Calendar className="h-4 w-4 md:hidden" />
<time className="whitespace-nowrap">{item.date}</time>
</div>
</div>
</article>
))
) : (
<div className="rounded-3xl border border-slate-200 bg-white/70 p-6 text-sm text-slate-700">
Loading resume timeline
<div className="rounded-3xl border border-slate-200 bg-white/70 p-8 text-center">
<p className="text-slate-600">No experience data available.</p>
</div>
)}
</div>
<p className="mt-5 text-xs text-slate-500">
Extraction is heuristic. Validate with source resume if high-stakes accuracy needed.
</p>
<div className="mt-6 flex items-center justify-between text-xs text-slate-500">
<p>Data extracted from resume document</p>
<a
href="/docs/Peter's Resume.docx"
className="text-emerald-700 hover:text-emerald-800 hover:underline"
target="_blank"
rel="noreferrer"
>
View full resume
</a>
</div>
</section>
)
}