/* ═══════════════════════════════════════════════════════════════ ORGANIZATION DETAIL /regions/:id /chapters/:id /partners/:id /organizations/:id One component behind four routes, because organizations are one table. The kind decides which extras render, not which file runs. The URL kind is decoration — the slug is what identifies the record — so a request for /chapters/great-lakes when that slug is a region redirects to the canonical path rather than rendering a correct page at a wrong address. Leadership arrives flat with team_id and team_name on each row, so grouping costs nothing. `teams` is fetched alongside anyway: a team with no current public members would otherwise be invisible here instead of listed, and its page unreachable. ═══════════════════════════════════════════════════════════════ */ import { Link, Navigate, useLocation, useParams } from 'react-router-dom' import PageShell from '../components/PageShell.tsx' import PageState from '../components/PageState.tsx' import ContentBlocks from '../components/ContentBlocks.tsx' import PeopleTiles from '../components/PeopleTiles.tsx' import EventListCards from './sections/EventList-Cards.tsx' import { useOrganization, type Leader, type OrganizationRecord, } from '../lib/useContent.ts' import { orgHref, orgListHref, teamHref } from '../lib/hrefs.ts' import { orgLogo, personPhoto } from '../lib/media.ts' const TEAL = '#138ba0' const BODY = '#4a6b72' export default function OrganizationDetail() { const { id } = useParams() const { pathname } = useLocation() const { data: org, loading, error, notFound, reload } = useOrganization(id) if (!org) { return ( ) } // /chapters/x when x is a region: same record, wrong address. // // useLocation, not window.location: the latter is outside the // router's awareness — always "/" under HashRouter, and free to // be stale mid-navigation, either of which turns this into a // redirect loop rather than a one-shot correction. // // Guarded on org.id because a record with no id would redirect to // /chapters/undefined, which is a worse page than the one we're // already on. All four organization routes must be registered or // this redirects somewhere nothing matches. const canonical = org.id ? orgHref(org.id, org.kind) : null if (canonical && decodeURIComponent(pathname) !== canonical) { return } const accent = org.color || TEAL const back = orgListHref(org.kind) const sections: any[] = [ { id: 'about', title: 'About', accent, background: '#ffffff', content: (
{org.description.map((paragraph, index) => (

{paragraph}

))}
), }, ] const teamBlocks = groupLeadership(org) if (teamBlocks.length > 0) { sections.push({ id: 'leadership', title: 'Who runs it', accent, background: '#eef9fb', content: (
{teamBlocks.map((block) => (
{block.teamId ? (

{block.label}

) : ( block.label && (

{block.label}

) )} {block.tagline && (

{block.tagline}

)}
{block.people.length > 0 ? ( ({ id: leader.person_id, name: leader.display_name, title: leader.title, pronouns: leader.pronouns, public_email: leader.public_email, photo: personPhoto(leader.photo), is_owner: leader.is_owner, }))} /> ) : (

No members listed yet.

)}
))}
), }) } if (org.kind === 'region' && (org.details.chapters?.length ?? 0) > 0) { sections.push({ id: 'chapters', title: 'Chapters', blurb: org.details.map_note ?? undefined, accent, background: '#ffffff', content: (
{(org.details.chapters ?? []).map((chapter) => ( {chapter.name} {chapter.location_label && ( {chapter.location_label} )} ))}
), }) } if (org.awards.length > 0) { sections.push({ id: 'awards', title: 'Awards', blurb: `Given by ${org.short_name || org.name}.`, accent, background: '#eef9fb', content: (
{org.awards.map((award) => ( {award.name} {award.description && ( {award.description} )} {award.recipient_count === 0 ? 'No recipients yet' : `${award.recipient_count} recipient${award.recipient_count === 1 ? '' : 's'}`} ))}
), }) } // EventList-Cards fetches and renders this itself — its own // docstring offers `host` for exactly this case, and a card there // already knows about gradients, logos, past-event collapsing and // the carousel. A second grid here was the same component written // worse. // // `org.events` is still what decides whether the section exists at // all: an organization that has never hosted anything shouldn't // get a heading followed by "events coming soon". if (org.events.length > 0) { sections.push({ id: 'events', title: 'Gatherings', accent, background: '#ffffff', content: ( ), }) } return } /* ── Pieces ──────────────────────────────────────────────────── */ function Facts({ org, accent, back, }: { org: OrganizationRecord accent: string back: { to: string; label: string } }) { const where = org.location_label || [org.locality, org.state_code].filter(Boolean).join(', ') return (
{where && {where}} {org.kind === 'region' && org.details.scope && ( {org.details.scope} )} {org.kind === 'chapter' && org.details.region_id && ( Part of{' '} {org.details.region_name} )} {org.kind === 'chapter' && org.details.meets && ( Meets {org.details.meets} )} {org.kind === 'chapter' && org.details.started && ( Since {org.details.started} )} {org.is_online && Online} {back.label}
) } function Contact({ org, accent }: { org: OrganizationRecord; accent: string }) { const hasAny = org.links.length > 0 || org.socials.length > 0 || org.website || org.email if (!hasAny) return null return (
{org.links.map((link) => ( {link.label} ))} {[org.website, org.email, ...org.socials] .filter((link): link is NonNullable => Boolean(link)) .map((link) => ( {link.label} ))}
) } function Logo({ file, name, accent, }: { file?: string | null name: string accent: string }) { const src = orgLogo(file) if (!src) { return ( ) } return } /* ── Leadership → team blocks ────────────────────────────────── */ type TeamBlock = { key: string teamId: string | null label: string | null tagline?: string | null accent?: string | null people: Leader[] } /* Order comes from `teams` (the admin's sort_order), not from the order people happen to appear in. Three cases to cover: affiliations with no team at all, teams with nobody in them, and people filed under a team that is no longer published. */ function groupLeadership(org: OrganizationRecord): TeamBlock[] { const byTeam = new Map() const loose: Leader[] = [] for (const leader of org.leadership) { if (!leader.team_id) { loose.push(leader) continue } const list = byTeam.get(leader.team_id) if (list) list.push(leader) else byTeam.set(leader.team_id, [leader]) } const blocks: TeamBlock[] = [] // People who hold a role in the organization without sitting on a // team. Usually the leads. No heading — they are the page. if (loose.length > 0) { blocks.push({ key: 'loose', teamId: null, label: null, people: loose }) } for (const team of org.teams) { blocks.push({ key: team.id, teamId: team.id, label: team.name, tagline: team.tagline, accent: team.color, people: byTeam.get(team.id) ?? [], }) byTeam.delete(team.id) } // Whatever is left is filed under an unpublished team. Its page // isn't reachable, so the heading is plain text — but the people // are real and shouldn't silently vanish from the org. for (const [teamId, people] of byTeam) { blocks.push({ key: teamId, teamId: null, label: people[0]?.team_name ?? null, people, }) } return blocks }