open-navigator / web_app /src /pages /Explore.tsx
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
Raw
History Blame Contribute Delete
22.5 kB
import { Link, useLocation } from 'react-router-dom'
import { useLayoutEffect, Fragment, useState, type ReactNode } from 'react'
import { ChevronRightIcon } from '@heroicons/react/24/outline'
import {
BUILD_PHASE,
EXPLORE_BUILD_ID,
EXPLORE_CAUSES_ID,
EXPLORE_FIND_HELP_ID,
EXPLORE_PLAN_ID,
EXPLORE_PRIMARY_NAV,
EXPLORE_SECTION_SCROLL_MARGIN_CLASS,
EXPLORE_TRACK_DECISIONS_ID,
FIND_HELP_SECTION,
LEGACY_EXPLORE_HASH_REDIRECT,
PLAN_DEFINE_SUCCESS_BY_BRANCH,
PLAN_IDENTIFY_ALLIES_BY_BRANCH,
PLAN_PATH_BRANCH_OPTIONS,
TRACK_DECISIONS_SECTION,
explorePlanSubsectionId,
type ExploreCard,
type PlanPathBranchId,
} from '../data/exploreActionPhases'
import { CAUSES } from '../data/causes'
import { flyoutIcons } from '../data/homeQuickNavFlyouts'
const CARD_CTA = 'Take the next step →'
const PLAN_BRANCH_TAG_CLASS: Record<PlanPathBranchId, string> = {
personal: 'rounded-full bg-sky-100 px-3 py-0.5 text-xs font-semibold text-sky-900 ring-1 ring-sky-200',
macro: 'rounded-full bg-amber-50 px-3 py-0.5 text-xs font-semibold text-amber-900 ring-1 ring-amber-200',
professional:
'rounded-full bg-violet-50 px-3 py-0.5 text-xs font-semibold text-violet-900 ring-1 ring-violet-200',
}
const PLAN_SUP_TAG_CLASS: Record<'allies' | 'success', string> = {
allies: 'rounded-full bg-sky-100 px-3 py-0.5 text-xs font-semibold text-sky-900 ring-1 ring-sky-200',
success: 'rounded-full bg-sky-100 px-3 py-0.5 text-xs font-semibold text-sky-900 ring-1 ring-sky-200',
}
function scrollToExploreTarget(targetId: string) {
document.getElementById(targetId)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
function ActionCard({ option, cta = CARD_CTA }: { option: ExploreCard; cta?: string }): ReactNode {
const Icon = option.icon
const isExternal = option.path.startsWith('http')
const inner = (
<div className="group bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-gray-200">
<div className="p-6">
<div
className="w-14 h-14 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300"
style={{ backgroundColor: `${option.color}15` }}
>
<div style={{ color: option.color }}>
<Icon className="h-7 w-7" />
</div>
</div>
<div className="mb-3">
<h3 className="text-xl font-bold text-gray-900 mb-1 group-hover:text-[#354F52] transition-colors">{option.title}</h3>
{option.stats ? (
<p className="text-sm font-medium" style={{ color: option.color }}>
{option.stats}
</p>
) : null}
</div>
<p className="text-gray-600 text-sm leading-relaxed">{option.description}</p>
<div className="mt-4 flex items-center text-sm font-medium" style={{ color: option.color }}>
<span className="group-hover:translate-x-1 transition-transform duration-300">{cta}</span>
</div>
</div>
<div
className="h-1 w-full transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left"
style={{ backgroundColor: option.color }}
/>
</div>
)
if (isExternal) {
return (
<a href={option.path} target="_blank" rel="noopener noreferrer">
{inner}
</a>
)
}
return <Link to={option.path}>{inner}</Link>
}
const CAUSE_ACCENT = ['#354F52', '#52796F', '#84A98C', '#2f5d62', '#52796F', '#354F52'] as const
function CauseEntryCard({ cause, color }: { cause: (typeof CAUSES)[number]; color: string }) {
const icon = flyoutIcons[cause.iconKey]()
const isExternal = cause.to.startsWith('http')
const inner = (
<div className="group flex h-full flex-col rounded-xl border border-gray-100 bg-white p-5 shadow-md transition-all duration-300 hover:border-gray-200 hover:shadow-xl">
<div
className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg transition-transform group-hover:scale-105"
style={{ backgroundColor: `${color}18` }}
>
<span style={{ color }}>{icon}</span>
</div>
<h3 className="mb-1 text-lg font-bold text-gray-900 group-hover:text-[#354F52]">{cause.label}</h3>
<p className="mb-3 flex-1 text-sm leading-relaxed text-gray-600">{cause.desc}</p>
<span
className="mt-auto w-fit rounded-full px-2 py-0.5 text-[10px] font-bold"
style={{
background: cause.hot ? '#fff4d6' : `${color}15`,
color: cause.hot ? '#a06000' : color,
}}
>
{cause.tag}
{cause.hot ? ' 🔥' : ''}
</span>
<div className="mt-3 text-sm font-medium" style={{ color }}>
<span className="group-hover:translate-x-0.5 inline-block transition-transform">{CARD_CTA}</span>
</div>
</div>
)
if (isExternal) {
return (
<a href={cause.to} target="_blank" rel="noopener noreferrer" className="block h-full">
{inner}
</a>
)
}
return (
<Link to={cause.to} className="block h-full">
{inner}
</Link>
)
}
export default function Explore() {
const location = useLocation()
const [planBranch, setPlanBranch] = useState<PlanPathBranchId | null>(null)
useLayoutEffect(() => {
const raw = location.hash.replace(/^#/, '')
if (!raw) {
window.scrollTo({ top: 0, behavior: 'auto' })
return
}
const targetId = LEGACY_EXPLORE_HASH_REDIRECT[raw] ?? raw
requestAnimationFrame(() => {
document.getElementById(targetId)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}, [location.pathname, location.hash])
return (
<div className="min-h-screen bg-slate-300">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6 sm:pt-8 pb-2">
<div className="rounded-2xl border border-slate-400/40 bg-white shadow-[0_4px_28px_-10px_rgba(15,23,42,0.18)]">
<div className="px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center max-w-3xl mx-auto">
<p className="text-xs font-semibold uppercase tracking-widest text-teal-800/90 mb-2">Take action</p>
<h1 className="text-4xl font-bold text-gray-900 mb-3">From information to impact</h1>
<p className="text-lg text-gray-600">
Start by choosing a cause, make a plan (learn the record, decide who to work with, then show up), find help when
someone needs direct support, track the decisions that matter, and build on open data.
</p>
</div>
<div
className="mt-8 flex flex-col gap-3 md:flex-row md:flex-wrap md:items-stretch md:justify-center max-w-6xl mx-auto"
aria-label="Explore: choose a cause, make a plan, find help, track decisions, build with data"
>
{EXPLORE_PRIMARY_NAV.map((step, i) => {
const Icon = step.Icon
const next = EXPLORE_PRIMARY_NAV[i + 1]
return (
<Fragment key={step.key}>
<button
type="button"
onClick={() => scrollToExploreTarget(step.targetId)}
className="flex flex-1 min-w-[10rem] cursor-pointer items-start gap-3 rounded-xl border border-gray-200 bg-slate-50/90 px-4 py-3 text-left shadow-sm transition-colors hover:border-teal-700/40 hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-700"
>
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-teal-800 text-white">
<Icon className="h-5 w-5" aria-hidden />
</span>
<div>
<div className="flex items-center gap-2">
<span className="font-semibold text-gray-900">{step.title}</span>
</div>
<p className="mt-1 text-xs leading-snug text-gray-600">{step.blurb}</p>
</div>
</button>
{next ? (
<button
type="button"
className="hidden md:flex shrink-0 items-center self-center rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-teal-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-700"
aria-label={`Jump to ${next.title}`}
onClick={() => scrollToExploreTarget(next.targetId)}
>
<ChevronRightIcon className="h-6 w-6" aria-hidden />
</button>
) : null}
</Fragment>
)
})}
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-16">
<section
id={EXPLORE_CAUSES_ID}
className={EXPLORE_SECTION_SCROLL_MARGIN_CLASS}
aria-labelledby="explore-causes-heading"
>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Start</p>
<h2 id="explore-causes-heading" className="text-2xl font-bold text-gray-900">
Choose a cause
</h2>
<p className="mt-1 max-w-3xl text-gray-600">
Pick where you are starting — each card sends you to search, services, or a formal route that fits.
</p>
</div>
<span className="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-900 ring-1 ring-emerald-200">
Start here if you are new
</span>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{CAUSES.map((cause, i) => (
<CauseEntryCard key={cause.id} cause={cause} color={CAUSE_ACCENT[i % CAUSE_ACCENT.length]!} />
))}
</div>
</section>
<section
id={EXPLORE_PLAN_ID}
className={`${EXPLORE_SECTION_SCROLL_MARGIN_CLASS} space-y-16`}
aria-labelledby="explore-plan-heading"
>
<div className="flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Plan</p>
<h2 id="explore-plan-heading" className="text-2xl font-bold text-gray-900">
Make a plan
</h2>
<p className="mt-1 max-w-3xl text-gray-600">
Choose <strong>Personal Path</strong>, <strong>Macro Path</strong>, or <strong>Professional Path</strong>{' '}
(one at a time). Your Identify Allies and Define Success steps update from that choice.
</p>
</div>
</div>
<section
id={explorePlanSubsectionId('path')}
className={EXPLORE_SECTION_SCROLL_MARGIN_CLASS}
aria-labelledby="plan-path-heading"
>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Plan · Step 1 of 4</p>
<h3 id="plan-path-heading" className="text-2xl font-bold text-gray-900">
Choose your path
</h3>
<p className="mt-1 max-w-3xl text-gray-600">
Select one branch below. You can switch anytime; only the allies and success tools for that branch are
shown.
</p>
</div>
</div>
<div
className="grid grid-cols-1 gap-4 md:grid-cols-3"
role="radiogroup"
aria-label="Personal path, macro path, or professional path"
>
{PLAN_PATH_BRANCH_OPTIONS.map((opt) => {
const selected = planBranch === opt.branch
return (
<button
key={opt.branch}
type="button"
role="radio"
aria-checked={selected}
onClick={() => setPlanBranch(opt.branch)}
className={`rounded-xl border-2 p-5 text-left shadow-sm transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-700 ${
selected
? 'border-teal-700 bg-teal-50/60 ring-1 ring-teal-800/15'
: 'border-gray-200 bg-white hover:border-teal-300 hover:bg-gray-50/80'
}`}
>
<h4 className="text-xl font-bold text-gray-900">{opt.title}</h4>
<p className="mt-2 text-sm leading-relaxed text-gray-600">{opt.subtitle}</p>
<p className="mt-3">
<span className={PLAN_BRANCH_TAG_CLASS[opt.branch]}>{opt.tag}</span>
</p>
</button>
)
})}
</div>
{planBranch ? (
<>
{(() => {
const opt = PLAN_PATH_BRANCH_OPTIONS.find((o) => o.branch === planBranch)
if (!opt) return null
return (
<>
<div className="mb-6 mt-12 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Plan · Step 2 of 4</p>
<h3 className="text-2xl font-bold text-gray-900">{opt.title}</h3>
<p className="mt-1 max-w-3xl text-gray-600">{opt.subtitle}</p>
<p className="mt-2">
<span className={PLAN_BRANCH_TAG_CLASS[opt.branch]}>{opt.tag}</span>
</p>
</div>
</div>
<div className={opt.gridClass}>
{opt.cards.map((option) => (
<ActionCard key={`path-${opt.branch}-${option.title}`} option={option} />
))}
</div>
</>
)
})()}
</>
) : (
<p
className="mt-8 rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-6 text-center text-sm text-gray-600"
role="status"
>
Choose <strong>Personal Path</strong>, <strong>Macro Path</strong>, or{' '}
<strong>Professional Path</strong> to see your path resources and the Identify Allies and Define Success
steps.
</p>
)}
</section>
{planBranch ? (
<>
<section
id={explorePlanSubsectionId('allies')}
className={EXPLORE_SECTION_SCROLL_MARGIN_CLASS}
aria-labelledby="plan-allies-heading"
>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Plan · Step 3 of 4</p>
<h3 id="plan-allies-heading" className="text-2xl font-bold text-gray-900">
Identify Allies
</h3>
<p className="mt-1 max-w-3xl text-gray-600">
{PLAN_IDENTIFY_ALLIES_BY_BRANCH[planBranch].subtitle}
</p>
<p className="mt-2">
<span className={PLAN_SUP_TAG_CLASS.allies}>Strategy</span>
</p>
</div>
</div>
<div className={PLAN_IDENTIFY_ALLIES_BY_BRANCH[planBranch].gridClass}>
{PLAN_IDENTIFY_ALLIES_BY_BRANCH[planBranch].cards.map((option) => (
<ActionCard key={`allies-${planBranch}-${option.title}`} option={option} />
))}
</div>
</section>
<section
id={explorePlanSubsectionId('success')}
className={EXPLORE_SECTION_SCROLL_MARGIN_CLASS}
aria-labelledby="plan-success-heading"
>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Plan · Step 4 of 4</p>
<h3 id="plan-success-heading" className="text-2xl font-bold text-gray-900">
Define Success
</h3>
<p className="mt-1 max-w-3xl text-gray-600">
{PLAN_DEFINE_SUCCESS_BY_BRANCH[planBranch].subtitle}
</p>
<p className="mt-2">
<span className={PLAN_SUP_TAG_CLASS.success}>Outcomes</span>
</p>
</div>
</div>
<div className={PLAN_DEFINE_SUCCESS_BY_BRANCH[planBranch].gridClass}>
{PLAN_DEFINE_SUCCESS_BY_BRANCH[planBranch].cards.map((option) => (
<ActionCard key={`success-${planBranch}-${option.title}`} option={option} />
))}
</div>
</section>
</>
) : null}
</section>
<section
id={EXPLORE_FIND_HELP_ID}
className={EXPLORE_SECTION_SCROLL_MARGIN_CLASS}
aria-labelledby="explore-find-help-heading"
>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Find</p>
<h2 id="explore-find-help-heading" className="text-2xl font-bold text-gray-900">
{FIND_HELP_SECTION.title}
</h2>
<p className="mt-1 max-w-3xl text-gray-600">{FIND_HELP_SECTION.subtitle}</p>
</div>
</div>
<div className={FIND_HELP_SECTION.gridClass}>
{FIND_HELP_SECTION.cards.map((option) => (
<ActionCard key={`find-help-${option.title}`} option={option} />
))}
</div>
</section>
<section
id={EXPLORE_TRACK_DECISIONS_ID}
className={EXPLORE_SECTION_SCROLL_MARGIN_CLASS}
aria-labelledby="explore-track-heading"
>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Track</p>
<h2 id="explore-track-heading" className="text-2xl font-bold text-gray-900">
{TRACK_DECISIONS_SECTION.title}
</h2>
<p className="mt-1 max-w-3xl text-gray-600">{TRACK_DECISIONS_SECTION.subtitle}</p>
</div>
</div>
<div className={TRACK_DECISIONS_SECTION.gridClass}>
{TRACK_DECISIONS_SECTION.cards.map((option) => (
<ActionCard key={`track-${option.title}`} option={option} />
))}
</div>
</section>
<section
id={EXPLORE_BUILD_ID}
className={EXPLORE_SECTION_SCROLL_MARGIN_CLASS}
aria-labelledby="explore-build-heading"
>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-gray-200 pb-4">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-teal-800">Build</p>
<h2 id="explore-build-heading" className="text-2xl font-bold text-gray-900">
{BUILD_PHASE.title}
</h2>
<p className="mt-1 max-w-3xl text-gray-600">{BUILD_PHASE.subtitle}</p>
</div>
</div>
<div className={BUILD_PHASE.gridClass}>
{BUILD_PHASE.cards.map((option) => (
<ActionCard key={`build-${option.title}`} option={option} />
))}
</div>
</section>
<div className="text-center">
<div className="bg-white rounded-xl shadow-md p-8 max-w-2xl mx-auto border border-gray-100">
<h2 className="text-2xl font-bold text-gray-900 mb-3">Need the raw files?</h2>
<p className="text-gray-600 mb-6">
Bulk tables, APIs, and documentation for teams building their own workflows on top of the same data.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="https://huggingface.co/datasets/CommunityOne/open-navigator-data"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg text-white font-semibold hover:shadow-lg transition-all"
style={{ backgroundColor: '#354F52' }}
>
View on HuggingFace
</a>
<a
href={
import.meta.env.PROD
? 'https://www.communityone.com/docs/data-sources/data-model-erd'
: 'http://localhost:3000/docs/data-sources/data-model-erd'
}
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all border-2"
style={{ borderColor: '#354F52', color: '#354F52' }}
>
Data model diagram
</a>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12 text-center">
<Link to="/" className="inline-flex items-center text-gray-600 hover:text-gray-900 font-medium transition-colors">
← Back to Home
</Link>
</div>
</div>
)
}