Add nature-informed counseling portfolio site
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { ExperienceItem } from '../utils/parseResume'
|
||||
import { parseResumeDocx } from '../utils/parseResume'
|
||||
|
||||
export default function Experience() {
|
||||
const [items, setItems] = useState<ExperienceItem[] | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
const parsed = await parseResumeDocx('/docs/Peter\'s Resume.docx')
|
||||
if (!cancelled) setItems(parsed)
|
||||
} catch (e) {
|
||||
if (!cancelled) setError(String(e))
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
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())
|
||||
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>
|
||||
|
||||
{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.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-8 space-y-5" aria-live="polite">
|
||||
{sorted.length ? (
|
||||
sorted.map((item, idx) => (
|
||||
<article
|
||||
key={`${item.date}-${idx}`}
|
||||
className="rounded-3xl border border-slate-200 bg-white/70 p-6"
|
||||
>
|
||||
<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>
|
||||
|
||||
{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}
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-3xl border border-slate-200 bg-white/70 p-6 text-sm text-slate-700">
|
||||
Loading resume timeline…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mt-5 text-xs text-slate-500">
|
||||
Extraction is heuristic. Validate with source resume if high-stakes accuracy needed.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user