Jiseoup/showmycodePublic
EN|KO
  • Code
  • Commits
  • Pull Requests
← Back to list
Merged

feat: add skeleton loading UI for all route segments

#14
JiseoupJiseoup · May 20, 2026feat/loading → main
feat
OverviewCommitsFiles changed
Changed files

+225 -5 · 8

@@ -0,0 +1,47 @@
+export default function CommitDetailLoading() {
+ return (
+ <main className="mx-auto w-full max-w-7xl flex-1 space-y-5 overflow-auto px-6 py-6">
+ {/* Back link skeleton. */}
+ <div className="bg-muted h-3 w-24 animate-pulse rounded" />
+
+ {/* Commit header skeleton. */}
+ <div className="border-border space-y-3 rounded-lg border p-4">
+ <div className="space-y-2">
+ <div className="bg-muted h-5 w-3/4 animate-pulse rounded" />
+ <div className="bg-muted h-4 w-1/2 animate-pulse rounded" />
+ </div>
+ <div className="border-border flex items-center gap-2 border-t pt-2">
+ <div className="bg-muted h-5 w-5 animate-pulse rounded-full" />
+ <div className="bg-muted h-4 w-40 animate-pulse rounded" />
+ <div className="bg-muted ml-auto h-5 w-16 animate-pulse rounded" />
+ </div>
+ </div>
+
+ {/* Files changed skeleton. */}
+ <div>
+ <div className="mb-3 flex items-center gap-2">
+ <div className="bg-muted h-4 w-32 animate-pulse rounded" />
+ <div className="bg-muted h-4 w-16 animate-pulse rounded" />
+ </div>
+ <div className="space-y-3">
+ {Array.from({ length: 3 }).map((_, i) => (
+ <div key={i} className="border-border rounded border">
+ <div className="border-border border-b px-3 py-2">
+ <div className="bg-muted h-4 w-48 animate-pulse rounded" />
+ </div>
+ <div className="space-y-1 p-3">
+ {Array.from({ length: 4 }).map((_, j) => (
+ <div
+ key={j}
+ className="bg-muted h-4 animate-pulse rounded"
+ style={{ width: `${40 + ((j * 31) % 50)}%` }}
+ />
+ ))}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ </main>
+ );
+}
@@ -0,0 +1,28 @@
+export default function CommitsLoading() {
+ return (
+ <main className="mx-auto w-full max-w-4xl flex-1 overflow-auto px-3 py-4 md:px-6 md:py-6">
+ {/* Title + branch selector skeleton. */}
+ <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
+ <div className="bg-muted h-6 w-40 animate-pulse rounded" />
+ <div className="bg-muted h-8 w-32 animate-pulse rounded" />
+ </div>
+
+ {/* Commit list skeletons. */}
+ <ul className="space-y-px">
+ {Array.from({ length: 6 }).map((_, i) => (
+ <li key={i} className="border-border flex items-start gap-3 border-b py-3 last:border-0">
+ <div className="bg-muted mt-0.5 h-8 w-8 shrink-0 animate-pulse rounded-full" />
+ <div className="min-w-0 flex-1 space-y-2">
+ <div
+ className="bg-muted h-4 animate-pulse rounded"
+ style={{ width: `${50 + ((i * 23) % 40)}%` }}
+ />
+ <div className="bg-muted h-3 w-36 animate-pulse rounded" />
+ </div>
+ <div className="bg-muted h-6 w-16 shrink-0 animate-pulse rounded" />
+ </li>
+ ))}
+ </ul>
+ </main>
+ );
+}
@@ -0,0 +1,43 @@
+export default function PullDetailLoading() {
+ return (
+ <main className="mx-auto w-full max-w-4xl flex-1 space-y-5 overflow-auto px-6 py-6">
+ {/* Back link skeleton. */}
+ <div className="bg-muted h-3 w-24 animate-pulse rounded" />
+
+ {/* PR header skeleton. */}
+ <div className="space-y-2">
+ <div className="flex items-center gap-2">
+ <div className="bg-muted h-5 w-16 animate-pulse rounded-full" />
+ <div className="bg-muted h-5 w-3/5 animate-pulse rounded" />
+ </div>
+ <div className="flex items-center gap-2">
+ <div className="bg-muted h-5 w-5 animate-pulse rounded-full" />
+ <div className="bg-muted h-4 w-40 animate-pulse rounded" />
+ <div className="bg-muted h-4 w-32 animate-pulse rounded" />
+ </div>
+ </div>
+
+ {/* Tab navigation skeleton. */}
+ <div className="border-border flex gap-1 border-b">
+ {Array.from({ length: 3 }).map((_, i) => (
+ <div key={i} className="px-3 py-2">
+ <div className="bg-muted h-4 w-16 animate-pulse rounded" />
+ </div>
+ ))}
+ </div>
+
+ {/* Content skeleton. */}
+ <div className="border-border rounded-lg border p-4">
+ <div className="space-y-3">
+ {Array.from({ length: 5 }).map((_, i) => (
+ <div
+ key={i}
+ className="bg-muted h-4 animate-pulse rounded"
+ style={{ width: `${40 + ((i * 41) % 50)}%` }}
+ />
+ ))}
+ </div>
+ </div>
+ </main>
+ );
+}
@@ -0,0 +1,30 @@
+export default function PullsLoading() {
+ return (
+ <main className="mx-auto w-full max-w-4xl flex-1 overflow-auto px-3 py-4 md:px-6 md:py-6">
+ {/* Title skeleton. */}
+ <div className="mb-4">
+ <div className="bg-muted h-6 w-48 animate-pulse rounded" />
+ </div>
+
+ {/* PR list skeletons. */}
+ <ul className="space-y-px">
+ {Array.from({ length: 5 }).map((_, i) => (
+ <li key={i} className="border-border flex items-start gap-3 border-b py-4 last:border-0">
+ <div className="bg-muted mt-0.5 h-8 w-8 shrink-0 animate-pulse rounded-full" />
+ <div className="min-w-0 flex-1 space-y-2">
+ <div className="flex items-center gap-2">
+ <div className="bg-muted h-5 w-16 animate-pulse rounded-full" />
+ <div
+ className="bg-muted h-4 animate-pulse rounded"
+ style={{ width: `${40 + ((i * 29) % 40)}%` }}
+ />
+ </div>
+ <div className="bg-muted h-3 w-40 animate-pulse rounded" />
+ <div className="bg-muted h-3 w-32 animate-pulse rounded" />
+ </div>
+ </li>
+ ))}
+ </ul>
+ </main>
+ );
+}
@@ -0,0 +1,36 @@
+export default function CodeLoading() {
+ return (
+ <div className="flex flex-1 overflow-hidden">
+ {/* Sidebar skeleton (hidden on mobile). */}
+ <aside className="border-border hidden w-64 shrink-0 border-r md:block">
+ <div className="space-y-2 p-3">
+ {Array.from({ length: 8 }).map((_, i) => (
+ <div key={i} className="flex items-center gap-2">
+ <div className="bg-muted h-4 w-4 animate-pulse rounded" />
+ <div
+ className="bg-muted h-4 animate-pulse rounded"
+ style={{ width: `${60 + ((i * 37) % 40)}%` }}
+ />
+ </div>
+ ))}
+ </div>
+ </aside>
+
+ {/* Code area skeleton. */}
+ <div className="flex-1">
+ <div className="border-border bg-muted/40 border-b px-4 py-2">
+ <div className="bg-muted h-4 w-48 animate-pulse rounded" />
+ </div>
+ <div className="space-y-2 p-4">
+ {Array.from({ length: 12 }).map((_, i) => (
+ <div
+ key={i}
+ className="bg-muted h-4 animate-pulse rounded"
+ style={{ width: `${30 + ((i * 47) % 60)}%` }}
+ />
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
@@ -0,0 +1,39 @@
+export default function HomeLoading() {
+ return (
+ <div className="bg-background min-h-screen">
+ {/* Header skeleton. */}
+ <header className="border-border flex items-center justify-between gap-2 border-b px-3 py-3 md:gap-4 md:px-6">
+ <div className="flex items-center gap-2">
+ <div className="bg-muted h-5 w-5 animate-pulse rounded" />
+ <div className="bg-muted h-5 w-24 animate-pulse rounded" />
+ </div>
+ <div className="flex items-center gap-3">
+ <div className="bg-muted h-8 w-16 animate-pulse rounded" />
+ <div className="bg-muted h-8 w-8 animate-pulse rounded" />
+ </div>
+ </header>
+
+ {/* Repo card skeletons. */}
+ <main className="mx-auto max-w-3xl px-3 py-6 md:px-6 md:py-10">
+ <ul className="space-y-3">
+ {Array.from({ length: 3 }).map((_, i) => (
+ <li key={i} className="border-border rounded-lg border p-5">
+ <div className="flex items-start justify-between gap-4">
+ <div className="min-w-0 flex-1 space-y-3">
+ <div className="bg-muted h-5 w-40 animate-pulse rounded" />
+ <div className="bg-muted h-4 w-64 animate-pulse rounded" />
+ <div className="flex gap-4">
+ <div className="bg-muted h-3 w-20 animate-pulse rounded" />
+ <div className="bg-muted h-3 w-12 animate-pulse rounded" />
+ <div className="bg-muted h-3 w-28 animate-pulse rounded" />
+ </div>
+ </div>
+ <div className="bg-muted h-5 w-14 animate-pulse rounded-full" />
+ </div>
+ </li>
+ ))}
+ </ul>
+ </main>
+ </div>
+ );
+}
@@ -18,11 +18,7 @@ export default async function HomePage({ params }: { params: Promise<{ lang: str
<div className="bg-background min-h-screen">
<header className="border-border flex items-center justify-between gap-2 border-b px-3 py-3 md:gap-4 md:px-6">
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
- <svg
- viewBox="0 0 16 16"
- className="h-5 w-5 shrink-0 fill-current"
- aria-hidden
- >
+ <svg viewBox="0 0 16 16" className="h-5 w-5 shrink-0 fill-current" aria-hidden>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
<span className="truncate text-base font-semibold">{SITE_NAME}</span>
@@ -95,6 +95,7 @@ The API route (`app/api/github/[...path]/route.ts`) validates that requested rep
- **Syntax highlighting** — `CodeViewer.tsx` uses Shiki with `github-light`/`github-dark` themes, switching based on the `dark` class on `<html>`. Line numbers are rendered via CSS counters on `.code-viewer code .line::before` in `globals.css`.
- **Markdown rendering** — `MarkdownBody.tsx` uses `react-markdown` + `remark-gfm` with custom Tailwind-styled components (no `@tailwindcss/typography`). Use this component for any user-generated Markdown content.
- **Diff view** — `FilesChanged.tsx` renders GitHub-style diffs with per-file and global fold/unfold, plus a changed-files tree sidebar for navigation. Accepts `GhPullFile[]` and a dict slice; reused across PR detail and commit detail pages.
+- **Loading UI** — every data-fetching route has a co-located `loading.tsx` with `animate-pulse` skeletons matching the page layout to minimize CLS.
- **Pagination** — implemented via `?page=N` searchParams on server components; `hasNext` is inferred from `results.length === perPage` (GitHub API does not return total count).
- **Styling** — Tailwind CSS v4 with class-based dark mode; CSS custom properties for theming; `lib/utils.ts` exports `cn()` (clsx + tailwind-merge). Use `px-3 md:px-6` (not bare `px-6`) for page-level horizontal padding.
- **shadcn/ui** — configured in `components.json` (zinc base color, `@/` aliases, no `tailwind.config.ts` — Tailwind v4 config lives in `globals.css`). When adding a new shadcn component, also add the required CSS variables (`--popover`, `--popover-foreground`, `--input`, `--ring`, etc.) to both `:root` and `.dark` in `app/globals.css`, and map them in the `@theme inline` block. The shadcn CLI may not auto-inject these variables, so verify manually.