import ExternalLink from 'lucide-solid/icons/external-link'; import type { JSX } from 'solid-js'; import { createMemo, createSignal, createUniqueId, For, Show } from 'solid-js'; import { statusLabel } from '@/lib/filterOpportunities'; import { formatDate, formatDateTime, formatValue } from '@/lib/format'; import type { ReleaseValue } from '@/lib/ocdsRelease'; import { parseReleaseData, summariseRelease } from '@/lib/ocdsRelease'; import type { NoticeSite } from '@/lib/opportunities'; import type { OpportunityRelease } from '@/types/Opportunities'; import { JsonView } from './JsonView'; interface Props { releases: OpportunityRelease[]; /** The site the releases are notices on, so each can link to its notice; undefined if none. */ site: NoticeSite | undefined; } const tabClass = 'shrink-0 border-b-2 border-transparent px-3 py-2 text-sm whitespace-nowrap text-muted hover:text-ink aria-selected:border-accent aria-selected:font-semibold aria-selected:text-ink'; const sectionHeading = 'mb-1.5 text-sm font-semibold'; const summaryList = 'grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm'; const value = (money: ReleaseValue | undefined): string => money ? formatValue(money.amount, money.currency) : '—'; const tagsOf = (release: OpportunityRelease): string => (release.release_tags ?? []).map((tag) => tag.tag).join(', '); interface FieldProps { term: string; children: JSX.Element; } /** A term and its description in a summary list. */ function Field(props: FieldProps): JSX.Element { return ( <>
{props.term}
{props.children}
); } interface ReleasePanelProps { release: OpportunityRelease; site: NoticeSite | undefined; } /** One release: its notice, a summary of its tender, awards and contracts, and its JSON. */ function ReleasePanel(props: ReleasePanelProps): JSX.Element { const parsed = createMemo(() => parseReleaseData(props.release.data)); const summary = createMemo(() => summariseRelease(parsed())); // The JSON is only laid out once it's opened: a release can be 100KB or more. const [isJsonOpen, setIsJsonOpen] = createSignal(false); const notice = () => { const site = props.site; const url = site?.urlFor(props.release); return site && url ? { name: site.name, url } : undefined; }; const json = () => { const data = parsed(); return data === undefined ? props.release.data : JSON.stringify(data, null, 2); }; return (
{props.release.release_id} {formatDateTime(props.release.release_date)} {tagsOf(props.release) || '—'}
{(notice) => ( Open notice on {notice().name} )}
{(tender) => (

Tender

{tender().title || '—'} {tender().status ? statusLabel(tender().status) : '—'} {value(tender().value)} {formatDate(tender().deadline)} {tender().lotCount || '—'}
)}
0}>

Awards

    {(award) => (
  • {award.title || award.id || '—'} {award.status ? statusLabel(award.status) : '—'} {award.suppliers.join(', ') || '—'} {value(award.value)} {formatDate(award.date)}
  • )}
0}>

Contracts

    {(contract) => (
  • {contract.title || contract.id || '—'} {contract.status ? statusLabel(contract.status) : '—'} {value(contract.value)} {formatDate(contract.dateSigned)} {formatDate(contract.start)} to {formatDate(contract.end)}
  • )}
setIsJsonOpen(e.currentTarget.open)}> Release JSON
); } /** * An opportunity's releases as tabs, newest first, each with a summary and * its JSON. The tabs follow the ARIA tabs pattern: the arrow keys, Home and * End move between them, and Tab moves into the open one. */ export function ReleaseTabs(props: Props): JSX.Element { const id = createUniqueId(); const releases = createMemo(() => [...props.releases].sort( (first, second) => Date.parse(second.release_date) - Date.parse(first.release_date), ), ); const [selected, setSelected] = createSignal(0); const tabs: HTMLButtonElement[] = []; const select = (index: number): void => { const count = releases().length; const next = (index + count) % count; setSelected(next); tabs[next]?.focus(); }; const onKeyDown = (event: KeyboardEvent): void => { const moves: Record = { ArrowRight: selected() + 1, ArrowLeft: selected() - 1, Home: 0, End: releases().length - 1, }; const index = moves[event.key]; if (index === undefined) return; event.preventDefault(); select(index); }; return ( 0} fallback={

This opportunity has no releases.

} >
{(release, index) => ( )}
{(release) => }
); }