Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 110 additions & 22 deletions apps/web/src/pages/browse/[title].astro
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function getStaticPaths() {
const { titleNum, entries } = Astro.props;
const titleName = TITLE_NAMES[titleNum] ?? `Title ${titleNum}`;

// Group entries by chapter
// Group entries by chapter, sort sections within each chapter
const byChapter = new Map<number, typeof entries>();
for (const entry of entries) {
const ch = entry.data.chapter;
Expand All @@ -36,15 +36,26 @@ for (const entry of entries) {
byChapter.get(ch)!.push(entry);
}

// Sort chapters numerically
// Sort chapters numerically, sort sections within each
const sortedChapters = [...byChapter.entries()].sort((a, b) => a[0] - b[0]);
for (const [, chapterEntries] of sortedChapters) {
chapterEntries.sort((a, b) => {
const aNum = parseFloat(a.data.usc_section);
const bNum = parseFloat(b.data.usc_section);
if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;
return a.data.usc_section.localeCompare(b.data.usc_section, undefined, { numeric: true });
});
}

const base = import.meta.env.BASE_URL;

// Determine which chapters to auto-expand (small titles expand all)
const autoExpand = sortedChapters.length <= 10;
---

<BaseLayout
title={`Title ${titleNum} — ${titleName}`}
description={`Chapters in Title ${titleNum} of the United States Code: ${titleName}`}
description={`Browse all ${entries.length} sections in Title ${titleNum} of the United States Code: ${titleName}`}
>
<Breadcrumbs items={[
{ label: 'Home', href: base },
Expand All @@ -55,31 +66,108 @@ const base = import.meta.env.BASE_URL;
<h1 class="text-navy dark:text-amber">
Title {titleNum} — {titleName}
</h1>
<p>
<p class="mb-1">
{entries.length} section{entries.length !== 1 ? 's' : ''} across {sortedChapters.length} chapter{sortedChapters.length !== 1 ? 's' : ''}.
Select a chapter to view its sections.
</p>
<p class="not-prose text-xs text-gray-400 dark:text-gray-500 font-sans">
Click a chapter to expand its sections, or click a section to view its full text and change history.
</p>

<!-- Expand/Collapse all toggle -->
{sortedChapters.length > 5 && (
<div class="not-prose mt-4 mb-2 flex gap-2 font-sans text-xs">
<button
id="expand-all"
class="rounded border border-gray-300 px-2 py-1 text-gray-600 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800"
onclick="document.querySelectorAll('#title-toc details').forEach(d => d.open = true)"
>Expand all</button>
<button
id="collapse-all"
class="rounded border border-gray-300 px-2 py-1 text-gray-600 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800"
onclick="document.querySelectorAll('#title-toc details').forEach(d => d.open = false)"
>Collapse all</button>
</div>
)}

<div class="not-prose mt-6">
<ul class="divide-y divide-gray-200 rounded-lg border border-gray-200 dark:divide-gray-800 dark:border-gray-800">
{sortedChapters.map(([chapterNum, chapterEntries]) => (
<li>
<a
href={`${base}browse/title-${titleNum}/chapter-${chapterNum}/`}
class="flex items-baseline gap-3 px-4 py-3 font-sans text-sm transition-colors hover:bg-gray-50 dark:hover:bg-gray-900"
>
<span class="w-10 shrink-0 text-right font-mono text-xs text-slate dark:text-gray-500">
<!-- Expandable TOC -->
<div id="title-toc" class="not-prose mt-4 space-y-1">
{sortedChapters.map(([chapterNum, chapterEntries]) => {
const activeCount = chapterEntries.filter(e => {
const s = e.data.status ?? 'active';
return s === 'active';
}).length;
const inactiveCount = chapterEntries.length - activeCount;

return (
<details
class="group rounded border border-gray-200 dark:border-gray-800"
open={autoExpand}
>
<summary class="flex cursor-pointer items-center gap-3 px-4 py-2.5 font-sans text-sm transition-colors hover:bg-gray-50 dark:hover:bg-gray-900 select-none">
<svg class="h-3 w-3 shrink-0 text-gray-400 transition-transform group-open:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
<span class="font-mono text-xs text-slate dark:text-gray-500 w-10 shrink-0 text-right">
Ch. {chapterNum}
</span>
<span class="text-navy dark:text-gray-200">
<a
href={`${base}browse/title-${titleNum}/chapter-${chapterNum}/`}
class="font-medium text-navy hover:underline dark:text-gray-200"
onclick="event.stopPropagation()"
>
Chapter {chapterNum}
</a>
<span class="ml-auto flex items-center gap-2 text-xs text-gray-400">
{inactiveCount > 0 && (
<span class="text-gray-300 dark:text-gray-600">{inactiveCount} inactive</span>
)}
<span>{activeCount} section{activeCount !== 1 ? 's' : ''}</span>
</span>
<span class="ml-auto text-xs text-gray-400">
{chapterEntries.length} section{chapterEntries.length !== 1 ? 's' : ''}
</span>
</a>
</li>
))}
</ul>
</summary>

<ul class="divide-y divide-gray-100 border-t border-gray-200 dark:divide-gray-800 dark:border-gray-800">
{chapterEntries.map((entry) => {
const sTitle = entry.data.title;
const status = entry.data.status ?? 'active';
const isInactive = status !== 'active' || sTitle.includes('Repealed') || sTitle.includes('Reserved') || sTitle.includes('Omitted') || sTitle.includes('Transferred') || sTitle.includes('Renumbered');
const statusLabel = status !== 'active' ? status.charAt(0).toUpperCase() + status.slice(1) : (
sTitle.includes('Repealed') ? 'Repealed' :
sTitle.includes('Reserved') ? 'Reserved' :
sTitle.includes('Omitted') ? 'Omitted' :
sTitle.includes('Renumbered') ? 'Renumbered' :
sTitle.includes('Transferred') ? 'Transferred' : null
);

return (
<li>
<a
href={`${base}statute/${entry.id}/`}
class:list={[
'flex items-baseline gap-3 px-4 py-1.5 pl-12 text-xs transition-colors hover:bg-gray-50 dark:hover:bg-gray-900',
isInactive && 'opacity-50',
]}
>
<span class="w-14 shrink-0 text-right font-mono text-[11px] text-slate dark:text-gray-500">
&sect; {entry.data.usc_section}
</span>
<span class:list={[
'truncate text-gray-700 dark:text-gray-300',
isInactive && 'italic',
]}>
{sTitle.replace(/^Section \S+ - /, '')}
</span>
{statusLabel && (
<span class="ml-auto shrink-0 rounded bg-gray-100 px-1 py-0.5 text-[9px] font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-500">
{statusLabel}
</span>
)}
</a>
</li>
);
})}
</ul>
</details>
);
})}
</div>
</BaseLayout>
Loading