Add nature-informed counseling portfolio site

This commit is contained in:
2026-04-18 15:23:20 -04:00
commit 6a28cbdd58
59 changed files with 33409 additions and 0 deletions
+90
View File
@@ -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>
)
}