174 lines
6.7 KiB
TypeScript
174 lines
6.7 KiB
TypeScript
/* ═══════════════════════════════════════════════════════════════
|
|
PUBLIC HREFS
|
|
|
|
One place that turns a record into a URL. The API deliberately
|
|
never sends paths — it sends `{ kind, id }` and, for an
|
|
organization, the `org_kind` that decides which of the three
|
|
routes a slug belongs to. Deciding that in each component is how
|
|
/regions/x and /chapters/x end up both existing for the same
|
|
record, and how the history timeline ended up emitting /event/:id
|
|
while the router only knew /retreats/:id.
|
|
|
|
timelineRefs.ts now delegates here rather than keeping its own
|
|
table, so there is one answer to "where does an event live" and
|
|
changing it is changing EVENT_BASE below.
|
|
|
|
navConfig.js stays the source of truth for the *nav*: these are
|
|
record routes, which never appear in it.
|
|
|
|
── On missing ids ──
|
|
Every builder takes an id the caller believed it had. When it
|
|
doesn't, the old behaviour was to interpolate the string
|
|
"undefined" into a path, render a link to it, mount a page and
|
|
fetch /api/organizations/undefined — four steps between the
|
|
mistake and any sign of it, none of which name the component
|
|
that made it.
|
|
|
|
Now the id is checked here. In dev that's a console.error with a
|
|
stack trace pointing at the caller; in production the path still
|
|
comes out, because a broken link beats a crashed page, and
|
|
useResource refuses to fetch it.
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
export type OrgKind = 'national' | 'region' | 'chapter' | 'partner'
|
|
export type RefKind = 'event' | 'organization' | 'team' | 'award' | 'person'
|
|
|
|
/* A region, a chapter and a partner read as different things to a
|
|
visitor even though they are one table, so they get one route
|
|
each. 'national' is NGU itself — one record, no listing to sit
|
|
under, so it falls through to the generic path. */
|
|
const ORG_BASE: Record<string, string> = {
|
|
region: '/regions',
|
|
chapter: '/chapters',
|
|
partner: '/partners',
|
|
national: '/organizations',
|
|
}
|
|
|
|
/** Canonical base for an event page.
|
|
*
|
|
* /events/:id, not /retreats/:id. The listing page is called
|
|
* Retreats because that's what NGU calls the gatherings it hosts,
|
|
* but the records are `events`, the API route is /api/events, and
|
|
* plenty of them — partner events, conferences — aren't retreats
|
|
* at all. Naming the record route after one page's editorial
|
|
* framing would have been wrong the first time a non-retreat got
|
|
* its own page.
|
|
*
|
|
* Nothing redirects from /retreats/:id, because nothing ever
|
|
* linked there. */
|
|
export const EVENT_BASE = '/events'
|
|
|
|
const DEV = Boolean((import.meta as any)?.env?.DEV)
|
|
|
|
/* Router params arrive as strings, so an id that has already been
|
|
through a template literal shows up as the literal word. Those
|
|
are as broken as a genuine null. */
|
|
const BAD = new Set(['', 'undefined', 'null', 'NaN'])
|
|
|
|
export const isBadId = (id: unknown): boolean =>
|
|
id == null || BAD.has(String(id))
|
|
|
|
function checkId(id: unknown, what: string): string {
|
|
if (!isBadId(id)) return String(id)
|
|
|
|
if (DEV) {
|
|
// console.error rather than warn: this is always a bug, and the
|
|
// stack is the whole point — it names the component that passed
|
|
// nothing.
|
|
console.error(
|
|
`hrefs: ${what}() was given ${JSON.stringify(id)}. ` +
|
|
`The link it returns will 404. Caller:`,
|
|
new Error('hrefs: missing id').stack,
|
|
)
|
|
}
|
|
|
|
return 'undefined'
|
|
}
|
|
|
|
/**
|
|
* The API path for one record, or null when the id isn't usable.
|
|
*
|
|
* The mirror image of the builders below: they make the URL a
|
|
* visitor sees, this makes the URL the client fetches, and both
|
|
* have to agree about what counts as an id.
|
|
*
|
|
* It lives here rather than next to the hook that calls it because
|
|
* it is a path, and paths are this file's job — and because a
|
|
* component that interpolates a missing route param produces the
|
|
* literal string "undefined", which the server cannot tell apart
|
|
* from a slug somebody genuinely typed. It answers 404 either way,
|
|
* and the log fills with GET /api/organizations/undefined with
|
|
* nothing to say where it came from.
|
|
*
|
|
* Returning null costs a round trip and turns a mystery 404 into
|
|
* the not-found page, which is what a visitor should see anyway.
|
|
*/
|
|
export function detailPath(base: string, id?: string | null): string | null {
|
|
return isBadId(id) ? null : `${base}/${encodeURIComponent(String(id))}`
|
|
}
|
|
|
|
export const eventHref = (id?: string | null) =>
|
|
`${EVENT_BASE}/${checkId(id, 'eventHref')}`
|
|
|
|
export const teamHref = (id?: string | null) => `/teams/${checkId(id, 'teamHref')}`
|
|
|
|
export const awardHref = (id?: string | null) => `/awards/${checkId(id, 'awardHref')}`
|
|
|
|
export const personHref = (id?: string | null) => `/people/${checkId(id, 'personHref')}`
|
|
|
|
export function orgHref(id?: string | null, kind?: string | null): string {
|
|
return `${ORG_BASE[kind ?? ''] ?? '/organizations'}/${checkId(id, 'orgHref')}`
|
|
}
|
|
|
|
/* For anything holding a polymorphic reference — timeline entries,
|
|
content blocks — where the kind arrives as data rather than being
|
|
known at the call site.
|
|
|
|
Returns null for a kind with no page AND for a reference with no
|
|
id, so the caller renders plain text instead of a dead link.
|
|
This is the one the timeline wants: an entry whose ref didn't
|
|
resolve should read as text, not as a link to nowhere. */
|
|
export function refHref(
|
|
kind: string | null | undefined,
|
|
id: string | null | undefined,
|
|
orgKind?: string | null,
|
|
): string | null {
|
|
if (!kind || isBadId(id)) return null
|
|
switch (kind) {
|
|
case 'event':
|
|
return eventHref(id)
|
|
case 'organization':
|
|
return orgHref(id, orgKind)
|
|
case 'team':
|
|
return teamHref(id)
|
|
case 'award':
|
|
return awardHref(id)
|
|
case 'person':
|
|
return personHref(id)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
/* What to call the kind in a breadcrumb or a back link. */
|
|
export const ORG_KIND_LABEL: Record<string, string> = {
|
|
national: 'Next Generation of Unity',
|
|
region: 'Region',
|
|
chapter: 'Chapter',
|
|
partner: 'Partner organization',
|
|
}
|
|
|
|
/* Where "back" goes from a record page. A chapter belongs to
|
|
/community, a retreat to /retreats. */
|
|
export function orgListHref(kind?: string | null): { to: string; label: string } {
|
|
switch (kind) {
|
|
case 'region':
|
|
return { to: '/community#local', label: 'All regions' }
|
|
case 'chapter':
|
|
return { to: '/community#local', label: 'All chapters' }
|
|
case 'partner':
|
|
return { to: '/community#partners', label: 'All partner organizations' }
|
|
default:
|
|
return { to: '/community', label: 'Community' }
|
|
}
|
|
}
|