From 2804e5e549d61068e1cf0ff6b81ab6fa0727c2e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 14:45:27 +0000 Subject: [PATCH 1/2] Add USCode display to hillstack frontend - Create tRPC router (uscode.ts) with endpoints for releases, titles, levels, lineage, and section content - Build release list page showing all US Code release points as cards - Build interactive viewer page with lazy-loading tree sidebar and hierarchical section content display using MUI X Tree View - Add @mui/x-tree-view dependency for tree navigation component https://claude.ai/code/session_016PccEf3pFp9KkvEyVjfxa6 --- hillstack/package.json | 1 + hillstack/pnpm-lock.yaml | 107 +++- .../uscode/[releaseId]/[[...path]]/page.tsx | 470 ++++++++++++++++++ hillstack/src/app/congress/uscode/page.tsx | 73 ++- hillstack/src/server/api/root.ts | 2 + hillstack/src/server/api/routers/uscode.ts | 146 ++++++ 6 files changed, 780 insertions(+), 19 deletions(-) create mode 100644 hillstack/src/app/congress/uscode/[releaseId]/[[...path]]/page.tsx create mode 100644 hillstack/src/server/api/routers/uscode.ts diff --git a/hillstack/package.json b/hillstack/package.json index 721a7e15..627b3ed1 100644 --- a/hillstack/package.json +++ b/hillstack/package.json @@ -28,6 +28,7 @@ "@mui/material": "^7.3.6", "@mui/x-charts": "^8.23.0", "@mui/x-data-grid": "^8.22.1", + "@mui/x-tree-view": "^8.27.2", "@prisma/adapter-pg": "^7.1.0", "@prisma/client": "^7.1.0", "@tanstack/react-query": "^5.69.0", diff --git a/hillstack/pnpm-lock.yaml b/hillstack/pnpm-lock.yaml index 4153ea9c..b47adf9b 100644 --- a/hillstack/pnpm-lock.yaml +++ b/hillstack/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@mui/x-data-grid': specifier: ^8.22.1 version: 8.22.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/x-tree-view': + specifier: ^8.27.2 + version: 8.27.2(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@prisma/adapter-pg': specifier: ^7.1.0 version: 7.1.0 @@ -180,6 +183,10 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -192,6 +199,16 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@base-ui/utils@0.2.5': + resolution: {integrity: sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@biomejs/biome@2.3.8': resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==} engines: {node: '>=14.21.3'} @@ -328,6 +345,9 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@hono/node-server@1.19.6': resolution: {integrity: sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==} engines: {node: '>=18.14.1'} @@ -586,14 +606,6 @@ packages: '@types/react': optional: true - '@mui/types@7.4.8': - resolution: {integrity: sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==} - peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@mui/types@7.4.9': resolution: {integrity: sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==} peerDependencies: @@ -672,6 +684,28 @@ packages: peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 + '@mui/x-internals@8.26.0': + resolution: {integrity: sha512-B9OZau5IQUvIxwpJZhoFJKqRpmWf5r0yMmSXjQuqb5WuqM755EuzWJOenY48denGoENzMLT8hQpA0hRTeU2IPA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@mui/x-tree-view@8.27.2': + resolution: {integrity: sha512-gceKjUEqKHBVt5BV0Yscx2NKgbI9z6IgEx3BHToeAupNCIZ7kAlnZUtk+FyIIcN+Vr6CFz5J0zxjnPgfHjbj2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0 + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/x-virtualizer@0.2.13': resolution: {integrity: sha512-HrAxZ3vjzCqJZFRFK2l0RdnzZCW/jNv9G4oyoh4E6mqYejOFJiJhEUhlgnrT209fneXh+j8VXOMuybj09wig9w==} engines: {node: '>=14.0.0'} @@ -1846,6 +1880,8 @@ snapshots: '@babel/runtime@7.28.4': {} + '@babel/runtime@7.28.6': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -1869,6 +1905,17 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@base-ui/utils@0.2.5(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.6 + '@floating-ui/utils': 0.2.10 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + '@biomejs/biome@2.3.8': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.3.8 @@ -2017,6 +2064,8 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} + '@floating-ui/utils@0.2.10': {} + '@hono/node-server@1.19.6(hono@4.10.6)': dependencies: hono: 4.10.6 @@ -2231,12 +2280,6 @@ snapshots: '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) '@types/react': 19.2.7 - '@mui/types@7.4.8(@types/react@19.2.7)': - dependencies: - '@babel/runtime': 7.28.4 - optionalDependencies: - '@types/react': 19.2.7 - '@mui/types@7.4.9(@types/react@19.2.7)': dependencies: '@babel/runtime': 7.28.4 @@ -2246,7 +2289,7 @@ snapshots: '@mui/utils@7.3.5(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/types': 7.4.8(@types/react@19.2.7) + '@mui/types': 7.4.9(@types/react@19.2.7) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 @@ -2341,7 +2384,7 @@ snapshots: '@mui/x-internals@8.22.0(@types/react@19.2.7)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.0) react: 19.2.0 reselect: 5.1.1 use-sync-external-store: 1.6.0(react@19.2.0) @@ -2358,10 +2401,40 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@mui/x-internals@8.26.0(@types/react@19.2.7)(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.0) + react: 19.2.0 + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + + '@mui/x-tree-view@8.27.2(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@mui/material@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@base-ui/utils': 0.2.5(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.6(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.0) + '@mui/x-internals': 8.26.0(@types/react@19.2.7)(react@19.2.0) + '@types/react-transition-group': 4.4.12(@types/react@19.2.7) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + '@mui/x-virtualizer@0.2.13(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.7)(react@19.2.0) + '@mui/utils': 7.3.6(@types/react@19.2.7)(react@19.2.0) '@mui/x-internals': 8.22.0(@types/react@19.2.7)(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) diff --git a/hillstack/src/app/congress/uscode/[releaseId]/[[...path]]/page.tsx b/hillstack/src/app/congress/uscode/[releaseId]/[[...path]]/page.tsx new file mode 100644 index 00000000..c46d5120 --- /dev/null +++ b/hillstack/src/app/congress/uscode/[releaseId]/[[...path]]/page.tsx @@ -0,0 +1,470 @@ +'use client'; + +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { Box, CircularProgress, Typography } from '@mui/material'; +import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; +import { TreeItem } from '@mui/x-tree-view/TreeItem'; +import { useParams, useRouter } from 'next/navigation'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { api } from '~/trpc/react'; + +interface SectionNode { + usc_section_id: number; + usc_ident: string | null; + number: string | null; + section_display: string | null; + heading: string | null; + content_type: string | null; + usc_chapter_id: number | null; + parent_id: number | null; +} + +interface ContentItem { + usc_content_id: number; + usc_ident: string | null; + parent_id: number | null; + order_number: number | null; + section_display: string | null; + heading: string | null; + content_str: string | null; + content_type: string | null; + number: string | null; + usc_section_id: number | null; +} + +interface ContentTreeNode extends ContentItem { + children: ContentTreeNode[]; +} + +function buildContentTree(items: ContentItem[]): ContentTreeNode[] { + const map = new Map(); + const roots: ContentTreeNode[] = []; + + for (const item of items) { + map.set(item.usc_content_id, { ...item, children: [] }); + } + + for (const item of items) { + const node = map.get(item.usc_content_id); + if (!node) continue; + const parent = item.parent_id ? map.get(item.parent_id) : undefined; + if (parent) { + parent.children.push(node); + } else { + roots.push(node); + } + } + + return roots; +} + +function stripCitations(str: string | null): string { + if (!str) return ''; + return str + .replace(/]*>(.*?)<\/usccite>/gs, '$1') + .replace(/\\n/g, ' '); +} + +function ContentNode({ + node, + depth, +}: { + node: ContentTreeNode; + depth: number; +}) { + const hasHeading = node.heading != null && node.heading.trim() !== ''; + + return ( + 0 ? 2 : 0, + borderLeft: depth > 0 ? '1px dashed' : 'none', + borderColor: 'divider', + pl: depth > 0 ? 1.5 : 0, + py: 0.25, + }} + > + + {hasHeading ? ( + + + {node.section_display} {node.heading} + + + ) : ( + + {node.section_display}{' '} + + )} + + {stripCitations(node.content_str)} + + + {node.children + .sort((a, b) => (a.order_number ?? 0) - (b.order_number ?? 0)) + .map((child) => ( + + ))} + + ); +} + +function USCContentView({ + chapterId, + sectionNumber, +}: { + chapterId: number; + sectionNumber: string; +}) { + const { data, isLoading } = api.uscode.sectionContent.useQuery({ + chapterId, + sectionNumber, + }); + + if (isLoading) { + return ( + + + + ); + } + + if (!data || data.length === 0) { + return ( + + No content found for this section. + + ); + } + + const tree = buildContentTree(data); + + return ( + + {tree.map((node) => ( + + ))} + + ); +} + +function SidebarTreeNode({ + section, + releaseId, + onSelectSection, + loadedChildren, + onExpand, +}: { + section: SectionNode; + releaseId: string; + onSelectSection: (shortTitle: string, sectionNumber: string) => void; + loadedChildren: Map; + onExpand: (chapterId: number, parentId: number) => void; +}) { + const isLeaf = section.content_type === 'section'; + const nodeId = `section-${section.usc_section_id}`; + const children = loadedChildren.get(String(section.usc_section_id)) ?? []; + const prettyDisplay = + section.content_type === 'section' + ? (section.section_display ?? '').replace(/SS/g, '\u00A7') + : (section.section_display ?? ''); + + return ( + { + if (isLeaf && section.number) { + onSelectSection( + String(section.usc_chapter_id), + section.number, + ); + } else if ( + !isLeaf && + children.length === 0 && + section.usc_chapter_id + ) { + onExpand(section.usc_chapter_id, section.usc_section_id); + } + }} + > + {!isLeaf && + children.map((child) => ( + + ))} + {!isLeaf && children.length === 0 && ( + + )} + + ); +} + +export default function USCodeViewer() { + const params = useParams(); + const router = useRouter(); + const releaseId = params.releaseId as string; + const pathParts = (params.path as string[] | undefined) ?? []; + const urlTitle = pathParts[0] ?? null; + const urlSection = pathParts[1] ?? null; + + const [selectedChapterId, setSelectedChapterId] = useState( + null, + ); + const [selectedSection, setSelectedSection] = useState( + urlSection, + ); + const [loadedChildren, setLoadedChildren] = useState< + Map + >(new Map()); + const [expandedItems, setExpandedItems] = useState([]); + + const { data: titles, isLoading: titlesLoading } = + api.uscode.titles.useQuery({ + releaseId: Number(releaseId), + }); + + const titleMap = useMemo(() => { + const map = new Map(); + if (titles) { + for (const t of titles) { + if (t.short_title) { + map.set(t.short_title, t.usc_chapter_id); + } + } + } + return map; + }, [titles]); + + useEffect(() => { + if (urlTitle && titleMap.has(urlTitle)) { + const chapId = titleMap.get(urlTitle); + if (chapId == null) return; + setSelectedChapterId(chapId); + if (urlSection) { + setSelectedSection(urlSection); + } + } + }, [urlTitle, urlSection, titleMap]); + + const handleLoadLevels = useCallback( + async (chapterId: number, parentId: number | null) => { + const key = + parentId != null ? String(parentId) : `chapter-${chapterId}`; + if (loadedChildren.has(key)) return; + + try { + const result = await fetch( + `/api/trpc/uscode.levels?input=${encodeURIComponent( + JSON.stringify({ json: { chapterId, parentId } }), + )}`, + ); + const json = await result.json(); + const sections: SectionNode[] = json?.result?.data?.json ?? []; + setLoadedChildren((prev) => { + const next = new Map(prev); + next.set(key, sections); + return next; + }); + } catch { + // silently fail + } + }, + [loadedChildren], + ); + + const handleSelectSection = useCallback( + (chapterIdStr: string, sectionNumber: string) => { + const chapterId = Number(chapterIdStr); + setSelectedChapterId(chapterId); + setSelectedSection(sectionNumber); + + const title = titles?.find((t) => t.usc_chapter_id === chapterId); + if (title?.short_title) { + router.push( + `/congress/uscode/${releaseId}/${title.short_title}/${sectionNumber}`, + ); + } + }, + [titles, releaseId, router], + ); + + const handleTitleExpand = useCallback( + (chapterId: number) => { + const key = `chapter-${chapterId}`; + if (!loadedChildren.has(key)) { + handleLoadLevels(chapterId, null); + } + }, + [loadedChildren, handleLoadLevels], + ); + + const handleItemExpansionToggle = useCallback( + (_event: React.SyntheticEvent | null, itemIds: string[]) => { + setExpandedItems(itemIds); + + for (const itemId of itemIds) { + if (itemId.startsWith('title-')) { + const chapterId = Number(itemId.replace('title-', '')); + handleTitleExpand(chapterId); + } else if (itemId.startsWith('section-')) { + const sectionId = Number(itemId.replace('section-', '')); + const key = String(sectionId); + if (!loadedChildren.has(key)) { + const allSections = Array.from( + loadedChildren.values(), + ).flat(); + const section = allSections.find( + (s) => s.usc_section_id === sectionId, + ); + if (section?.usc_chapter_id) { + handleLoadLevels(section.usc_chapter_id, sectionId); + } + } + } + } + }, + [handleTitleExpand, loadedChildren, handleLoadLevels], + ); + + if (titlesLoading) { + return ( + + + + ); + } + + return ( + + + + Titles + + + {titles?.map((title) => { + const titleNodeId = `title-${title.usc_chapter_id}`; + const children = + loadedChildren.get( + `chapter-${title.usc_chapter_id}`, + ) ?? []; + + return ( + + {children.map((section) => ( + + handleLoadLevels(chapId, parentId) + } + onSelectSection={handleSelectSection} + releaseId={releaseId} + section={section} + /> + ))} + {children.length === 0 && ( + + )} + + ); + })} + + + + + {selectedChapterId && selectedSection ? ( + + ) : ( + + + Select a section from the sidebar to view its + content. + + + )} + + + ); +} diff --git a/hillstack/src/app/congress/uscode/page.tsx b/hillstack/src/app/congress/uscode/page.tsx index 1a94dd4d..386e4bf4 100644 --- a/hillstack/src/app/congress/uscode/page.tsx +++ b/hillstack/src/app/congress/uscode/page.tsx @@ -1,3 +1,72 @@ -'use client'; +import { + Box, + Card, + CardActionArea, + CardContent, + Typography, +} from '@mui/material'; +import Link from 'next/link'; +import { api } from '~/trpc/server'; -export default function USCodeSearch() {} +export default async function USCodePage() { + const releases = await api.uscode.releases(); + + return ( + + + United States Code + + + Release points of the US Code, typically aligned around the + passage of major legislation. + + + {releases.map((release) => ( + + + + + {release.short_title} + + {release.long_title && ( + + {release.long_title} + + )} + + Effective:{' '} + {release.effective_date + ? new Date( + release.effective_date, + ).toLocaleDateString() + : 'N/A'} + + + + + ))} + + + ); +} diff --git a/hillstack/src/server/api/root.ts b/hillstack/src/server/api/root.ts index c2ba5ca0..b18c1523 100644 --- a/hillstack/src/server/api/root.ts +++ b/hillstack/src/server/api/root.ts @@ -1,5 +1,6 @@ import { billRouter } from '~/server/api/routers/bill'; import { statsRouter } from '~/server/api/routers/stats'; +import { uscodeRouter } from '~/server/api/routers/uscode'; import { userRouter } from '~/server/api/routers/user'; import { createCallerFactory, createTRPCRouter } from '~/server/api/trpc'; import { committeeRouter } from './routers/committee'; @@ -15,6 +16,7 @@ export const appRouter = createTRPCRouter({ legislator: legislatorRouter, committee: committeeRouter, stats: statsRouter, + uscode: uscodeRouter, user: userRouter, }); diff --git a/hillstack/src/server/api/routers/uscode.ts b/hillstack/src/server/api/routers/uscode.ts new file mode 100644 index 00000000..9b4b3e6c --- /dev/null +++ b/hillstack/src/server/api/routers/uscode.ts @@ -0,0 +1,146 @@ +import { z } from 'zod'; +import { createTRPCRouter, publicProcedure } from '~/server/api/trpc'; + +export const uscodeRouter = createTRPCRouter({ + releases: publicProcedure.query(async ({ ctx }) => { + const releases = await ctx.db.usc_release.findMany({ + select: { + usc_release_id: true, + short_title: true, + effective_date: true, + long_title: true, + }, + orderBy: { effective_date: 'desc' }, + }); + return releases; + }), + + titles: publicProcedure + .input(z.object({ releaseId: z.number() })) + .query(async ({ input, ctx }) => { + const titles = await ctx.db.usc_chapter.findMany({ + where: { usc_release_id: input.releaseId }, + select: { + usc_chapter_id: true, + short_title: true, + long_title: true, + usc_release_id: true, + }, + orderBy: { usc_chapter_id: 'asc' }, + }); + return titles; + }), + + levels: publicProcedure + .input( + z.object({ + chapterId: z.number(), + parentId: z.number().nullable(), + }), + ) + .query(async ({ input, ctx }) => { + const sections = await ctx.db.usc_section.findMany({ + where: { + usc_chapter_id: input.chapterId, + parent_id: input.parentId, + }, + select: { + usc_section_id: true, + usc_ident: true, + number: true, + section_display: true, + heading: true, + content_type: true, + usc_chapter_id: true, + parent_id: true, + }, + orderBy: { usc_section_id: 'asc' }, + }); + return sections; + }), + + lineage: publicProcedure + .input( + z.object({ + chapterId: z.number(), + sectionNumber: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const section = await ctx.db.usc_section.findFirst({ + where: { + usc_chapter_id: input.chapterId, + number: input.sectionNumber, + }, + }); + + if (!section) return []; + + const lineage = [section]; + let current = section; + let maxDepth = 20; + + while (current.parent_id && maxDepth > 0) { + maxDepth--; + const parent = await ctx.db.usc_section.findFirst({ + where: { + usc_section_id: current.parent_id, + usc_chapter_id: input.chapterId, + }, + }); + if (!parent) break; + lineage.push(parent); + current = parent; + if (!current.parent_id) break; + } + + return lineage.map((s) => ({ + usc_section_id: s.usc_section_id, + usc_ident: s.usc_ident, + number: s.number, + section_display: s.section_display, + heading: s.heading, + content_type: s.content_type, + usc_chapter_id: s.usc_chapter_id, + parent_id: s.parent_id, + })); + }), + + sectionContent: publicProcedure + .input( + z.object({ + chapterId: z.number(), + sectionNumber: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const section = await ctx.db.usc_section.findFirst({ + where: { + usc_chapter_id: input.chapterId, + number: input.sectionNumber, + content_type: 'section', + }, + }); + + if (!section) return null; + + const content = await ctx.db.usc_content.findMany({ + where: { usc_section_id: section.usc_section_id }, + select: { + usc_content_id: true, + usc_ident: true, + parent_id: true, + order_number: true, + section_display: true, + heading: true, + content_str: true, + content_type: true, + number: true, + usc_section_id: true, + }, + orderBy: { usc_content_id: 'asc' }, + }); + + return content; + }), +}); From 29c2f86a1dcc01136129908d1498136e0193ac93 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 14:46:28 +0000 Subject: [PATCH 2/2] Apply Biome formatting fixes to existing files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-formatted by pnpm check:write — whitespace and line-wrapping changes only, no logic changes. https://claude.ai/code/session_016PccEf3pFp9KkvEyVjfxa6 --- hillstack/src/app/_home/dashboard.tsx | 2 +- .../src/app/_home/widgets/interestFeed.tsx | 18 ++++--- .../congress/bills/[billId]/interestBadge.tsx | 4 +- .../app/congress/bills/[billId]/layout.tsx | 2 +- .../congress/bills/[billId]/overview/page.tsx | 6 ++- hillstack/src/app/user/interests/page.tsx | 50 +++++++++++-------- hillstack/src/server/api/routers/committee.ts | 11 +++- hillstack/src/server/api/routers/user.ts | 45 ++++++++--------- 8 files changed, 77 insertions(+), 61 deletions(-) diff --git a/hillstack/src/app/_home/dashboard.tsx b/hillstack/src/app/_home/dashboard.tsx index 96cd2208..d3854137 100644 --- a/hillstack/src/app/_home/dashboard.tsx +++ b/hillstack/src/app/_home/dashboard.tsx @@ -9,11 +9,11 @@ import SellIcon from '@mui/icons-material/Sell'; import { Card, Grid, Toolbar } from '@mui/material'; import Box from '@mui/material/Box'; import type React from 'react'; +import { InterestFeed } from '~/app/_home/widgets/interestFeed'; import { LegislationCalendar } from '~/app/_home/widgets/legislationCalendar'; import { LegislationFollowed } from '~/app/_home/widgets/legislationFollowed'; import { LegislationTags } from '~/app/_home/widgets/legislationTags'; import { LegislatorFollowed } from '~/app/_home/widgets/legislatorFollowed'; -import { InterestFeed } from '~/app/_home/widgets/interestFeed'; function DashboardWidget({ title, diff --git a/hillstack/src/app/_home/widgets/interestFeed.tsx b/hillstack/src/app/_home/widgets/interestFeed.tsx index 60303f36..4a12e4fc 100644 --- a/hillstack/src/app/_home/widgets/interestFeed.tsx +++ b/hillstack/src/app/_home/widgets/interestFeed.tsx @@ -1,6 +1,13 @@ 'use client'; -import { Box, Button, List, ListItem, ListItemButton, Typography } from '@mui/material'; +import { + Box, + Button, + List, + ListItem, + ListItemButton, + Typography, +} from '@mui/material'; import Link from 'next/link'; import { useSession } from 'next-auth/react'; import type { RouterOutputs } from '~/trpc/react'; @@ -71,8 +78,8 @@ export function InterestFeed() { textAlign='center' variant='body2' > - No bills found yet. Set up your interests to track - relevant legislation. + No bills found yet. Set up your interests to track relevant + legislation.