Skip to content

Commit f1909cd

Browse files
committed
feat: Render project README content
- Add sanitized Markdown rendering with GFM support - Replace project scenes with README previews
1 parent ee1f4ca commit f1909cd

21 files changed

Lines changed: 972 additions & 485 deletions

bun.lock

Lines changed: 160 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@
4848
"radix-ui": "^1.6.7",
4949
"react": "^19.2.4",
5050
"react-dom": "^19.2.4",
51+
"react-markdown": "^10.1.0",
5152
"react-resizable-panels": "^4.5.2",
53+
"rehype-raw": "^7.0.0",
54+
"rehype-sanitize": "^6.0.0",
55+
"remark-gfm": "^4.0.1",
5256
"shiki": "^3.21.0",
5357
"sonner": "^2.0.7",
5458
"tailwind-merge": "^3.4.0",

src/components/Header.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ import { GITHUB_OWNER } from '@/lib/github'
88

99
export default function Header() {
1010
return (
11-
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur-md supports-[backdrop-filter]:bg-background/85">
12-
<div className="mx-auto flex h-16 max-w-7xl items-center px-4 sm:px-6 lg:h-[4.5rem] lg:px-8">
11+
<header className="sticky top-0 z-50 bg-muted/20 backdrop-blur-md">
12+
<div className="mx-auto flex h-14 max-w-7xl items-center px-4 sm:px-6 lg:px-8">
1313
<Link
1414
to="/"
15-
className="group flex shrink-0 items-baseline gap-2 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring"
15+
className="group flex shrink-0 items-center gap-1.5 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring"
1616
>
17-
<span className="font-display text-lg font-semibold tracking-tight">
18-
hapwi
17+
<span className="font-display text-lg font-semibold tracking-[-0.025em]">
18+
hapwi&apos;s
1919
</span>
20-
<span className="hidden font-mono text-xs uppercase tracking-[0.16em] text-muted-foreground transition-colors group-hover:text-foreground sm:inline">
20+
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-muted-foreground transition-colors group-hover:text-foreground">
2121
library
2222
</span>
2323
</Link>
@@ -27,7 +27,7 @@ export default function Header() {
2727
<Button
2828
asChild
2929
variant="ghost"
30-
size="icon-lg"
30+
size="icon"
3131
className="hidden sm:inline-flex"
3232
>
3333
<a

src/components/bottom-nav.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ export function BottomNav() {
2525
}
2626

2727
return (
28-
<div className="fixed inset-x-0 bottom-0 z-40 overflow-hidden rounded-t-[1.375rem] border-t bg-background pb-[env(safe-area-inset-bottom,0px)] shadow-[0_-8px_24px_-20px_rgba(0,0,0,0.7)]">
28+
<div className="fixed inset-x-0 bottom-0 z-40 overflow-hidden rounded-t-3xl border-t bg-background pb-[env(safe-area-inset-bottom,0px)]">
2929
<nav
3030
aria-label="Primary"
31-
className="grid min-h-[5.25rem] w-full grid-cols-3 px-2 sm:min-h-[5.5rem] sm:px-4"
31+
className="grid min-h-[3.75rem] w-full grid-cols-3 sm:min-h-16"
3232
>
3333
{navItems.map(({ path, label, icon: Icon }) => {
3434
const active = isActive(path)
@@ -39,23 +39,15 @@ export function BottomNav() {
3939
to={path}
4040
aria-current={active ? 'page' : undefined}
4141
className={cn(
42-
'group relative flex min-w-0 flex-col items-center justify-center gap-1 px-2 py-2 text-[0.8125rem] font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
42+
'group relative flex min-h-[3.75rem] min-w-0 flex-col items-center justify-center gap-1 px-0 py-3 text-[0.6875rem] leading-none text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring sm:min-h-16',
4343
active && 'text-primary',
4444
)}
4545
>
46-
<span
47-
className={cn(
48-
'grid size-11 place-items-center rounded-full transition-colors',
49-
active &&
50-
'bg-primary text-primary-foreground shadow-sm shadow-primary/25',
51-
!active &&
52-
'group-hover:bg-accent group-hover:text-accent-foreground',
53-
)}
54-
>
46+
<span className="relative grid size-6 shrink-0 place-items-center">
5547
<HugeiconsIcon
5648
icon={Icon}
5749
strokeWidth={2}
58-
className="size-5.5"
50+
className="size-6"
5951
aria-hidden="true"
6052
/>
6153
</span>

src/components/catalog-row.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,23 @@ export const ProjectLink = forwardRef<
3535
{
3636
project: CatalogProject
3737
children: ReactNode
38+
source?: boolean
3839
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>
39-
>(function ProjectLink({ project, className, children, ...props }, ref) {
40+
>(function ProjectLink(
41+
{ project, source, className, children, ...props },
42+
ref,
43+
) {
4044
if (project.to === '/repos/$repo' && project.repo) {
4145
return (
4246
<Link
4347
ref={ref}
4448
to="/repos/$repo"
4549
params={{ repo: project.repo }}
46-
search={{ file: undefined, path: undefined }}
50+
search={{
51+
file: undefined,
52+
path: undefined,
53+
source: source ? true : undefined,
54+
}}
4755
className={className}
4856
{...props}
4957
>
@@ -111,13 +119,19 @@ export const ProjectLink = forwardRef<
111119
)
112120
})
113121

114-
export function CatalogRow({ project }: { project: CatalogProject }) {
122+
export function CatalogRow({
123+
project,
124+
source,
125+
}: {
126+
project: CatalogProject
127+
source?: boolean
128+
}) {
115129
const isExternal = !project.to
116130

117131
return (
118132
<div className="flex flex-col gap-2">
119133
<Item asChild size="sm">
120-
<ProjectLink project={project}>
134+
<ProjectLink project={project} source={source}>
121135
<ItemContent>
122136
<ItemTitle>
123137
{project.name}

src/components/github-repo-browser.tsx

Lines changed: 129 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Link } from '@tanstack/react-router'
33
import {
44
AlertCircle,
55
ArrowLeft,
6+
BookOpen,
67
Copy,
78
Download,
89
ExternalLink,
@@ -14,9 +15,11 @@ import { toast } from 'sonner'
1415

1516
import { CodeFileViewer } from '@/components/code-file-viewer'
1617
import { FileIcon } from '@/components/file-icon'
18+
import { ProjectReadme } from '@/components/project-readme'
1719
import { RepoBreadcrumb } from '@/components/repo-breadcrumb'
1820
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
1921
import { Button } from '@/components/ui/button'
22+
import { Skeleton } from '@/components/ui/skeleton'
2023
import {
2124
Tooltip,
2225
TooltipContent,
@@ -45,12 +48,14 @@ type GitHubRepoBrowserProps = {
4548
repo: string
4649
file?: string
4750
path?: string
51+
source?: boolean
4852
}
4953

5054
export function GitHubRepoBrowser({
5155
repo,
5256
file,
5357
path,
58+
source,
5459
}: GitHubRepoBrowserProps) {
5560
const owner = GITHUB_OWNER
5661
const curated = findCuratedProject(repo)
@@ -121,6 +126,15 @@ export function GitHubRepoBrowser({
121126
() => buildRepoListing(files, currentPath),
122127
[files, currentPath],
123128
)
129+
const readmeFile = useMemo(
130+
() =>
131+
files.find(
132+
(candidate) =>
133+
!candidate.path.includes('/') &&
134+
/^readme(?:\.(?:md|mdx|markdown))?$/i.test(candidate.name),
135+
) ?? null,
136+
[files],
137+
)
124138

125139
const rawUrl = useMemo(() => {
126140
if (!activeFile) return null
@@ -270,7 +284,11 @@ export function GitHubRepoBrowser({
270284
label: repo,
271285
href: '/repos/$repo',
272286
params: { repo },
273-
search: { file: undefined, path: undefined },
287+
search: {
288+
file: undefined,
289+
path: undefined,
290+
source: source ? true : undefined,
291+
},
274292
},
275293
]
276294

@@ -284,7 +302,7 @@ export function GitHubRepoBrowser({
284302
label: part,
285303
href: '/repos/$repo',
286304
params: { repo },
287-
search: { file: undefined, path: runningPath },
305+
search: { file: undefined, path: runningPath, source: true },
288306
})
289307
}
290308

@@ -297,15 +315,107 @@ export function GitHubRepoBrowser({
297315
}
298316

299317
return segments
300-
}, [currentPath, activeFile, repo])
318+
}, [currentPath, activeFile, repo, source])
319+
320+
if (!source && !selectedPath) {
321+
return (
322+
<div className="flex min-h-0 flex-1 flex-col">
323+
<main className="mx-auto w-full max-w-5xl flex-1 px-4 py-3 sm:px-6 sm:py-4 lg:px-8">
324+
<div className="flex flex-col gap-4">
325+
<header className="grid gap-3 border-b border-border/70 pb-3 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
326+
<div className="flex min-w-0 items-start gap-2">
327+
<Button
328+
asChild
329+
variant="ghost"
330+
size="icon-sm"
331+
className="mt-0.5 text-muted-foreground"
332+
>
333+
<Link to="/" aria-label="Back to work">
334+
<ArrowLeft />
335+
</Link>
336+
</Button>
337+
<div className="min-w-0">
338+
<h1 className="font-display text-2xl font-semibold tracking-tight text-balance">
339+
{title}
340+
</h1>
341+
<p className="mt-1 max-w-3xl text-sm leading-relaxed text-muted-foreground">
342+
{description}
343+
</p>
344+
</div>
345+
</div>
346+
347+
<div className="flex shrink-0">
348+
<Button asChild size="sm">
349+
<a href={githubRepoUrl} target="_blank" rel="noreferrer">
350+
<ExternalLink data-icon="inline-start" />
351+
View source
352+
</a>
353+
</Button>
354+
</div>
355+
</header>
356+
357+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6">
358+
{treeError ? (
359+
<Alert>
360+
<AlertTitle>Project details unavailable</AlertTitle>
361+
<AlertDescription>{treeError}</AlertDescription>
362+
</Alert>
363+
) : isLoadingTree ? (
364+
<div
365+
className="flex flex-col gap-4 py-4"
366+
aria-label="Loading project details"
367+
>
368+
<Skeleton className="h-8 w-2/3" />
369+
<Skeleton className="h-4 w-full" />
370+
<Skeleton className="h-4 w-5/6" />
371+
<Skeleton className="h-24 w-full" />
372+
</div>
373+
) : readmeFile ? (
374+
<ProjectReadme
375+
owner={owner}
376+
repo={repo}
377+
branch={branch}
378+
path={readmeFile.path}
379+
title={title}
380+
/>
381+
) : (
382+
<Alert>
383+
<AlertTitle>No project details found</AlertTitle>
384+
<AlertDescription>
385+
Use View source to open this project on GitHub.
386+
</AlertDescription>
387+
</Alert>
388+
)}
389+
</section>
390+
</div>
391+
</main>
392+
</div>
393+
)
394+
}
301395

302396
if (!selectedPath) {
303397
return (
304398
<div className="flex min-h-0 flex-1 flex-col">
305399
<main className="mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6 sm:py-8 lg:px-8 lg:py-10">
306400
<div className="flex flex-col gap-8">
307401
<header className="flex flex-col gap-4">
308-
<RepoBreadcrumb segments={breadcrumbSegments} />
402+
<div className="flex flex-wrap items-center justify-between gap-3">
403+
<RepoBreadcrumb segments={breadcrumbSegments} />
404+
<Button asChild variant="outline" size="sm">
405+
<Link
406+
to="/repos/$repo"
407+
params={{ repo }}
408+
search={{
409+
file: undefined,
410+
path: undefined,
411+
source: undefined,
412+
}}
413+
>
414+
<BookOpen data-icon="inline-start" />
415+
Project overview
416+
</Link>
417+
</Button>
418+
</div>
309419
<div className="flex flex-col gap-2">
310420
<div className="flex flex-wrap items-center gap-3">
311421
<h1 className="font-display text-2xl font-semibold tracking-tight sm:text-3xl">
@@ -349,7 +459,11 @@ export function GitHubRepoBrowser({
349459
key={folder.path}
350460
to="/repos/$repo"
351461
params={{ repo }}
352-
search={{ file: undefined, path: folder.path }}
462+
search={{
463+
file: undefined,
464+
path: folder.path,
465+
source: true,
466+
}}
353467
className="catalog-row group"
354468
>
355469
<span className="catalog-caret" aria-hidden />
@@ -369,7 +483,11 @@ export function GitHubRepoBrowser({
369483
key={item.path}
370484
to="/repos/$repo"
371485
params={{ repo }}
372-
search={{ file: item.path, path: undefined }}
486+
search={{
487+
file: item.path,
488+
path: undefined,
489+
source: true,
490+
}}
373491
className="catalog-row group"
374492
>
375493
<span className="catalog-caret" aria-hidden />
@@ -429,7 +547,11 @@ export function GitHubRepoBrowser({
429547
<Link
430548
to="/repos/$repo"
431549
params={{ repo }}
432-
search={{ file: undefined, path: currentPath || undefined }}
550+
search={{
551+
file: undefined,
552+
path: currentPath || undefined,
553+
source: true,
554+
}}
433555
>
434556
<ArrowLeft />
435557
<span className="hidden sm:inline">Back</span>

0 commit comments

Comments
 (0)