Jiseoup/showmycodePublic
EN|KO
  • 코드
  • 커밋
  • 풀 리퀘스트
← 목록으로

Merge pull request #8 from Jiseoup/chore/coding-conventions

chore: introduce coding conventions and reformat codebase

JiseoupJISUB LIM · 2026년 5월 4일7db77b7

변경된 파일39개+1463 -354

변경된 파일

+1463 -354 · 39개

@@ -16,7 +16,10 @@ Closes #
## Checklist
-- [ ] `npm run build` passes
+- [ ] `npm run format:check` passes
- [ ] `npm run lint` passes
-- [ ] New/updated strings added to both `dictionaries/ko.json` and `dictionaries/en.json` (if applicable)
+- [ ] `npm run typecheck` passes
+- [ ] `npm run build` passes
+- [ ] PR title follows Conventional Commits (`feat:`, `fix:`, `chore:`, `refactor:`, `docs:`, `test:`, `i18n:`)
+- [ ] New/updated strings added to both `locales/ko.json` and `locales/en.json` (if applicable)
- [ ] Tests added for new functionality (if applicable)
@@ -21,24 +21,22 @@ export default async function CommitDetailPage({ params }: Props) {
const body = bodyLines.join("\n").trim();
return (
- <main className="flex-1 overflow-auto max-w-4xl mx-auto w-full px-6 py-6 space-y-5">
+ <main className="mx-auto w-full max-w-4xl flex-1 space-y-5 overflow-auto px-6 py-6">
<Link
href={`/${lang}/repository/${owner}/${repo}/commits`}
- className="text-xs text-muted-foreground hover:text-foreground transition-colors"
+ className="text-muted-foreground hover:text-foreground text-xs transition-colors"
>
← {dict.commits.backToList}
</Link>
{/* Commit header. */}
- <div className="border border-border rounded-lg p-4 space-y-3">
+ <div className="border-border space-y-3 rounded-lg border p-4">
<div>
- <p className="font-semibold text-base leading-snug">{title}</p>
- {body && (
- <p className="text-sm text-muted-foreground mt-2 whitespace-pre-wrap">{body}</p>
- )}
+ <p className="text-base leading-snug font-semibold">{title}</p>
+ {body && <p className="text-muted-foreground mt-2 text-sm whitespace-pre-wrap">{body}</p>}
</div>
- <div className="flex items-center gap-2 pt-2 border-t border-border flex-wrap">
+ <div className="border-border flex flex-wrap items-center gap-2 border-t pt-2">
{commit.author?.avatar_url ? (
<Image
src={commit.author.avatar_url}
@@ -48,27 +46,27 @@ export default async function CommitDetailPage({ params }: Props) {
className="rounded-full"
/>
) : (
- <div className="w-5 h-5 rounded-full bg-muted" />
+ <div className="bg-muted h-5 w-5 rounded-full" />
)}
- <span className="text-sm text-muted-foreground">
+ <span className="text-muted-foreground text-sm">
{commit.commit.author.name} · {formatDate(commit.commit.author.date, lang)}
</span>
- <code className="ml-auto text-xs font-mono text-muted-foreground bg-muted px-2 py-1 rounded">
+ <code className="text-muted-foreground bg-muted ml-auto rounded px-2 py-1 font-mono text-xs">
{commit.sha.slice(0, 7)}
</code>
</div>
</div>
{/* Files changed. */}
<div>
- <h2 className="text-sm font-semibold mb-3">
+ <h2 className="mb-3 text-sm font-semibold">
{dict.commits.filesChanged}
- <span className="ml-2 font-normal text-muted-foreground">
- {commit.files.length}{dict.commits.countSuffix}
+ <span className="text-muted-foreground ml-2 font-normal">
+ {commit.files.length}
+ {dict.commits.countSuffix}
</span>
- <span className="ml-2 text-xs font-mono font-normal">
- <span className="text-green-600">+{commit.stats.additions}</span>
- {" "}
+ <span className="ml-2 font-mono text-xs font-normal">
+ <span className="text-green-600">+{commit.stats.additions}</span>{" "}
<span className="text-red-500">-{commit.stats.deletions}</span>
</span>
</h2>
@@ -36,11 +36,11 @@ export default async function CommitsPage({ params, searchParams }: Props) {
`/${lang}/repository/${owner}/${repo}/commits?page=${p}&branch=${encodeURIComponent(branch)}`;
return (
- <main className="flex-1 overflow-auto max-w-4xl mx-auto w-full px-3 md:px-6 py-4 md:py-6">
- <div className="flex items-center justify-between mb-4 flex-wrap gap-2">
+ <main className="mx-auto w-full max-w-4xl flex-1 overflow-auto px-3 py-4 md:px-6 md:py-6">
+ <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<h2 className="text-lg font-semibold">
{dict.commits.title}
- <span className="ml-2 text-sm font-normal text-muted-foreground">
+ <span className="text-muted-foreground ml-2 text-sm font-normal">
{dict.commits.page} {page}
</span>
</h2>
@@ -55,36 +55,36 @@ export default async function CommitsPage({ params, searchParams }: Props) {
const body = bodyLines.join("\n").trim();
return (
- <li key={c.sha} className="border-b border-border last:border-0">
+ <li key={c.sha} className="border-border border-b last:border-0">
<Link
href={`/${lang}/repository/${owner}/${repo}/commits/${c.sha}`}
- className="flex items-start gap-3 py-3 hover:bg-muted/50 transition-colors rounded px-1 -mx-1"
+ className="hover:bg-muted/50 -mx-1 flex items-start gap-3 rounded px-1 py-3 transition-colors"
>
{c.author?.avatar_url ? (
<Image
src={c.author.avatar_url}
alt={c.author.login}
width={32}
height={32}
- className="rounded-full shrink-0 mt-0.5"
+ className="mt-0.5 shrink-0 rounded-full"
/>
) : (
- <div className="w-8 h-8 rounded-full bg-muted shrink-0 mt-0.5" />
+ <div className="bg-muted mt-0.5 h-8 w-8 shrink-0 rounded-full" />
)}
<div className="min-w-0 flex-1">
- <p className="font-medium text-sm leading-snug">{title}</p>
+ <p className="text-sm leading-snug font-medium">{title}</p>
{body && (
- <p className="text-xs text-muted-foreground mt-1 whitespace-pre-line line-clamp-3">
+ <p className="text-muted-foreground mt-1 line-clamp-3 text-xs whitespace-pre-line">
{body}
</p>
)}
- <p className="text-xs text-muted-foreground mt-1">
+ <p className="text-muted-foreground mt-1 text-xs">
{c.commit.author.name} · {formatDate(c.commit.author.date, lang)}
</p>
</div>
- <code className="text-xs font-mono text-muted-foreground bg-muted px-2 py-1 rounded shrink-0">
+ <code className="text-muted-foreground bg-muted shrink-0 rounded px-2 py-1 font-mono text-xs">
{c.sha.slice(0, 7)}
</code>
</Link>
@@ -94,24 +94,24 @@ export default async function CommitsPage({ params, searchParams }: Props) {
</ul>
{(hasPrev || hasNext) && (
- <div className="flex items-center justify-between mt-6 gap-2 flex-wrap">
+ <div className="mt-6 flex flex-wrap items-center justify-between gap-2">
{hasPrev ? (
<Link
href={pageUrl(page - 1)}
- className="px-3 py-1.5 text-sm border border-border rounded hover:bg-muted transition-colors"
+ className="border-border hover:bg-muted rounded border px-3 py-1.5 text-sm transition-colors"
>
← {dict.commits.prev}
</Link>
) : (
<div />
)}
- <span className="text-sm text-muted-foreground">
+ <span className="text-muted-foreground text-sm">
{dict.commits.page} {page}
</span>
{hasNext ? (
<Link
href={pageUrl(page + 1)}
- className="px-3 py-1.5 text-sm border border-border rounded hover:bg-muted transition-colors"
+ className="border-border hover:bg-muted rounded border px-3 py-1.5 text-sm transition-colors"
>
{dict.commits.next} →
</Link>
@@ -13,40 +13,40 @@ type Props = {
searchParams: Promise<{ tab?: string }>;
};
-function PRBadge({ merged, state, dict }: {
+function PRBadge({
+ merged,
+ state,
+ dict,
+}: {
merged: boolean;
state: string;
dict: { merged: string; open: string; closed: string };
}) {
if (merged)
return (
- <span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300 font-medium">
+ <span className="rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900/40 dark:text-purple-300">
{dict.merged}
</span>
);
if (state === "open")
return (
- <span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300 font-medium">
+ <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/40 dark:text-green-300">
{dict.open}
</span>
);
return (
- <span className="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300 font-medium">
+ <span className="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/40 dark:text-red-300">
{dict.closed}
</span>
);
}
-
export default async function PullDetailPage({ params, searchParams }: Props) {
const { lang, owner, repo, number } = await params;
const { tab: tabParam } = await searchParams;
const prNumber = parseInt(number, 10);
- const tab: Tab =
- tabParam === "commits" ? "commits"
- : tabParam === "files" ? "files"
- : "overview";
+ const tab: Tab = tabParam === "commits" ? "commits" : tabParam === "files" ? "files" : "overview";
const [pr, dict] = await Promise.all([
getPull(owner, repo, prNumber),
@@ -60,56 +60,56 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
]);
const baseUrl = `/${lang}/repository/${owner}/${repo}/pulls/${prNumber}`;
- const tabUrl = (t: Tab) => t === "overview" ? baseUrl : `${baseUrl}?tab=${t}`;
+ const tabUrl = (t: Tab) => (t === "overview" ? baseUrl : `${baseUrl}?tab=${t}`);
const tabs: { key: Tab; label: string }[] = [
{ key: "overview", label: dict.pulls.tabOverview },
- { key: "commits", label: dict.pulls.tabCommits },
- { key: "files", label: dict.pulls.tabFiles },
+ { key: "commits", label: dict.pulls.tabCommits },
+ { key: "files", label: dict.pulls.tabFiles },
];
return (
- <main className="flex-1 overflow-auto max-w-4xl mx-auto w-full px-6 py-6 space-y-5">
+ <main className="mx-auto w-full max-w-4xl flex-1 space-y-5 overflow-auto px-6 py-6">
{/* Back link. */}
<Link
href={`/${lang}/repository/${owner}/${repo}/pulls`}
- className="text-xs text-muted-foreground hover:text-foreground transition-colors"
+ className="text-muted-foreground hover:text-foreground text-xs transition-colors"
>
← {dict.pulls.backToList}
</Link>
{/* PR header. */}
<div>
- <div className="flex items-start gap-2 flex-wrap">
+ <div className="flex flex-wrap items-start gap-2">
<PRBadge merged={!!pr.merged_at} state={pr.state} dict={dict.pulls} />
- <h1 className="text-lg font-semibold leading-snug">{pr.title}</h1>
+ <h1 className="text-lg leading-snug font-semibold">{pr.title}</h1>
<span className="text-muted-foreground font-normal">#{pr.number}</span>
</div>
- <div className="flex items-center gap-2 mt-2 flex-wrap">
+ <div className="mt-2 flex flex-wrap items-center gap-2">
<Image
src={pr.user.avatar_url}
alt={pr.user.login}
width={20}
height={20}
className="rounded-full"
/>
- <span className="text-sm text-muted-foreground">
+ <span className="text-muted-foreground text-sm">
{pr.user.login} · {formatDate(pr.created_at, lang)}
</span>
- <span className="text-xs font-mono text-muted-foreground">
+ <span className="text-muted-foreground font-mono text-xs">
{pr.head.ref} → {pr.base.ref}
</span>
</div>
{pr.labels.length > 0 && (
- <div className="flex items-center gap-1.5 mt-2 flex-wrap">
+ <div className="mt-2 flex flex-wrap items-center gap-1.5">
{pr.labels.map((l) => {
const safeColor = /^[0-9a-fA-F]{6}$/.test(l.color) ? l.color : "8b949e";
return (
<span
key={l.name}
- className="text-xs px-1.5 py-0.5 rounded"
+ className="rounded px-1.5 py-0.5 text-xs"
style={{
background: `#${safeColor}33`,
color: `#${safeColor}`,
@@ -125,16 +125,16 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
</div>
{/* Tab navigation. */}
- <div className="flex gap-1 border-b border-border">
+ <div className="border-border flex gap-1 border-b">
{tabs.map(({ key, label }) => (
<Link
key={key}
href={tabUrl(key)}
className={[
- "px-3 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
+ "-mb-px border-b-2 px-3 py-2 text-sm font-medium transition-colors",
tab === key
? "border-foreground text-foreground"
- : "border-transparent text-muted-foreground hover:text-foreground",
+ : "text-muted-foreground hover:text-foreground border-transparent",
].join(" ")}
>
{label}
@@ -144,11 +144,11 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
{/* Overview. */}
{tab === "overview" && (
- <div className="border border-border rounded-lg p-4 text-foreground">
+ <div className="border-border text-foreground rounded-lg border p-4">
{pr.body?.trim() ? (
<MarkdownBody>{pr.body}</MarkdownBody>
) : (
- <span className="text-sm text-muted-foreground italic">{dict.pulls.noBody}</span>
+ <span className="text-muted-foreground text-sm italic">{dict.pulls.noBody}</span>
)}
</div>
)}
@@ -160,34 +160,34 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
const [title, ...bodyLines] = c.commit.message.split("\n");
const body = bodyLines.join("\n").trim();
return (
- <li key={c.sha} className="border-b border-border last:border-0">
+ <li key={c.sha} className="border-border border-b last:border-0">
<Link
href={`/${lang}/repository/${owner}/${repo}/commits/${c.sha}`}
- className="flex items-start gap-3 py-3 hover:bg-muted/50 transition-colors rounded px-1 -mx-1"
+ className="hover:bg-muted/50 -mx-1 flex items-start gap-3 rounded px-1 py-3 transition-colors"
>
{c.author?.avatar_url ? (
<Image
src={c.author.avatar_url}
alt={c.author.login}
width={32}
height={32}
- className="rounded-full shrink-0 mt-0.5"
+ className="mt-0.5 shrink-0 rounded-full"
/>
) : (
- <div className="w-8 h-8 rounded-full bg-muted shrink-0 mt-0.5" />
+ <div className="bg-muted mt-0.5 h-8 w-8 shrink-0 rounded-full" />
)}
<div className="min-w-0 flex-1">
- <p className="font-medium text-sm leading-snug">{title}</p>
+ <p className="text-sm leading-snug font-medium">{title}</p>
{body && (
- <p className="text-xs text-muted-foreground mt-1 whitespace-pre-line line-clamp-3">
+ <p className="text-muted-foreground mt-1 line-clamp-3 text-xs whitespace-pre-line">
{body}
</p>
)}
- <p className="text-xs text-muted-foreground mt-1">
+ <p className="text-muted-foreground mt-1 text-xs">
{c.commit.author.name} · {formatDate(c.commit.author.date, lang)}
</p>
</div>
- <code className="text-xs font-mono text-muted-foreground bg-muted px-2 py-1 rounded shrink-0">
+ <code className="text-muted-foreground bg-muted shrink-0 rounded px-2 py-1 font-mono text-xs">
{c.sha.slice(0, 7)}
</code>
</Link>
@@ -198,9 +198,7 @@ export default async function PullDetailPage({ params, searchParams }: Props) {
)}
{/* Files changed */}
- {tab === "files" && files && (
- <FilesChanged files={files} dict={dict.pulls} />
- )}
+ {tab === "files" && files && <FilesChanged files={files} dict={dict.pulls} />}
</main>
);
}
@@ -11,25 +11,29 @@ type Props = {
searchParams: Promise<{ page?: string }>;
};
-function PRBadge({ merged, state, dict }: {
+function PRBadge({
+ merged,
+ state,
+ dict,
+}: {
merged: boolean;
state: string;
dict: { merged: string; open: string; closed: string };
}) {
if (merged)
return (
- <span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300 font-medium shrink-0">
+ <span className="shrink-0 rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900/40 dark:text-purple-300">
{dict.merged}
</span>
);
if (state === "open")
return (
- <span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300 font-medium shrink-0">
+ <span className="shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/40 dark:text-green-300">
{dict.open}
</span>
);
return (
- <span className="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300 font-medium shrink-0">
+ <span className="shrink-0 rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/40 dark:text-red-300">
{dict.closed}
</span>
);
@@ -48,14 +52,13 @@ export default async function PullsPage({ params, searchParams }: Props) {
const hasPrev = page > 1;
const hasNext = pulls.length === PER_PAGE;
- const pageUrl = (p: number) =>
- `/${lang}/repository/${owner}/${repo}/pulls?page=${p}`;
+ const pageUrl = (p: number) => `/${lang}/repository/${owner}/${repo}/pulls?page=${p}`;
return (
- <main className="flex-1 overflow-auto max-w-4xl mx-auto w-full px-3 md:px-6 py-4 md:py-6">
- <h2 className="text-lg font-semibold mb-4">
+ <main className="mx-auto w-full max-w-4xl flex-1 overflow-auto px-3 py-4 md:px-6 md:py-6">
+ <h2 className="mb-4 text-lg font-semibold">
{dict.pulls.title}
- <span className="ml-2 text-sm font-normal text-muted-foreground">
+ <span className="text-muted-foreground ml-2 text-sm font-normal">
{dict.pulls.page} {page}
</span>
</h2>
@@ -65,46 +68,44 @@ export default async function PullsPage({ params, searchParams }: Props) {
) : (
<ul className="space-y-px">
{pulls.map((pr) => (
- <li key={pr.number} className="border-b border-border last:border-0">
+ <li key={pr.number} className="border-border border-b last:border-0">
<Link
href={`/${lang}/repository/${owner}/${repo}/pulls/${pr.number}`}
- className="flex items-start gap-3 py-4 hover:bg-muted/50 transition-colors rounded px-1 -mx-1"
+ className="hover:bg-muted/50 -mx-1 flex items-start gap-3 rounded px-1 py-4 transition-colors"
>
<Image
src={pr.user.avatar_url}
alt={pr.user.login}
width={32}
height={32}
- className="rounded-full shrink-0 mt-0.5"
+ className="mt-0.5 shrink-0 rounded-full"
/>
<div className="min-w-0 flex-1">
- <div className="flex items-center gap-2 flex-wrap">
+ <div className="flex flex-wrap items-center gap-2">
<PRBadge merged={!!pr.merged_at} state={pr.state} dict={dict.pulls} />
- <p className="font-medium text-sm">{pr.title}</p>
- <span className="text-xs text-muted-foreground">#{pr.number}</span>
+ <p className="text-sm font-medium">{pr.title}</p>
+ <span className="text-muted-foreground text-xs">#{pr.number}</span>
</div>
- <p className="text-xs text-muted-foreground mt-1 font-mono truncate">
+ <p className="text-muted-foreground mt-1 truncate font-mono text-xs">
{pr.head.ref} → {pr.base.ref}
</p>
{pr.body && (
- <p className="text-xs text-muted-foreground mt-1.5 line-clamp-2">
- {pr.body}
- </p>
+ <p className="text-muted-foreground mt-1.5 line-clamp-2 text-xs">{pr.body}</p>
)}
- <div className="flex items-center gap-2 mt-2 flex-wrap">
- <span className="text-xs text-muted-foreground">
+ <div className="mt-2 flex flex-wrap items-center gap-2">
+ <span className="text-muted-foreground text-xs">
{pr.user.login} · {formatDate(pr.created_at, lang)}
</span>
{pr.labels.map((l) => {
const safeColor = /^[0-9a-fA-F]{6}$/.test(l.color) ? l.color : "8b949e";
return (
<span
key={l.name}
- className="text-xs px-1.5 py-0.5 rounded"
+ className="rounded px-1.5 py-0.5 text-xs"
style={{
background: `#${safeColor}33`,
color: `#${safeColor}`,
@@ -124,24 +125,24 @@ export default async function PullsPage({ params, searchParams }: Props) {
)}
{(hasPrev || hasNext) && (
- <div className="flex items-center justify-between mt-6 gap-2 flex-wrap">
+ <div className="mt-6 flex flex-wrap items-center justify-between gap-2">
{hasPrev ? (
<Link
href={pageUrl(page - 1)}
- className="px-3 py-1.5 text-sm border border-border rounded hover:bg-muted transition-colors"
+ className="border-border hover:bg-muted rounded border px-3 py-1.5 text-sm transition-colors"
>
← {dict.pulls.prev}
</Link>
) : (
<div />
)}
- <span className="text-sm text-muted-foreground">
+ <span className="text-muted-foreground text-sm">
{dict.pulls.page} {page}
</span>
{hasNext ? (
<Link
href={pageUrl(page + 1)}
- className="px-3 py-1.5 text-sm border border-border rounded hover:bg-muted transition-colors"
+ className="border-border hover:bg-muted rounded border px-3 py-1.5 text-sm transition-colors"
>
{dict.pulls.next} →
</Link>
@@ -14,12 +14,12 @@ export default function RepoError({
const t = messages[lang as keyof typeof messages] ?? messages.ko;
return (
- <div className="flex-1 flex flex-col items-center justify-center gap-4 text-center px-6">
+ <div className="flex flex-1 flex-col items-center justify-center gap-4 px-6 text-center">
<p className="text-sm font-medium">{t.error.title}</p>
- <p className="text-xs text-muted-foreground">{error.message}</p>
+ <p className="text-muted-foreground text-xs">{error.message}</p>
<button
onClick={unstable_retry}
- className="text-xs px-3 py-1.5 rounded border border-border hover:bg-muted transition-colors"
+ className="border-border hover:bg-muted rounded border px-3 py-1.5 text-xs transition-colors"
>
{t.error.retry}
</button>
@@ -15,15 +15,10 @@ export default async function RepoLayout({ children, params }: Props) {
const locale = lang as Locale;
// Guard: reject any repo not in the allowlist before fetching or rendering anything.
- const allowed = getAllowedRepos().some(
- (r) => r.owner === owner && r.repo === repo
- );
+ const allowed = getAllowedRepos().some((r) => r.owner === owner && r.repo === repo);
if (!allowed) notFound();
- const [repoData, dict] = await Promise.all([
- getRepo(owner, repo),
- getDictionary(locale),
- ]);
+ const [repoData, dict] = await Promise.all([getRepo(owner, repo), getDictionary(locale)]);
const tabs = [
{ label: dict.nav.code, href: `/${locale}/repository/${owner}/${repo}` },
@@ -32,21 +27,27 @@ export default async function RepoLayout({ children, params }: Props) {
];
return (
- <div className="min-h-screen bg-background flex flex-col">
- <header className="border-b border-border px-3 md:px-6 py-3 flex items-center justify-between gap-2 md:gap-4">
- <div className="flex items-center gap-2 min-w-0 overflow-hidden">
- <Link href={`/${locale}`} className="text-muted-foreground hover:text-foreground transition-colors shrink-0">
- <svg viewBox="0 0 16 16" className="w-5 h-5 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"/>
+ <div className="bg-background flex min-h-screen flex-col">
+ <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">
+ <Link
+ href={`/${locale}`}
+ className="text-muted-foreground hover:text-foreground shrink-0 transition-colors"
+ >
+ <svg viewBox="0 0 16 16" className="h-5 w-5 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>
</Link>
<span className="text-muted-foreground">/</span>
- <Link href={`/${locale}`} className="text-muted-foreground hover:text-foreground text-sm transition-colors">
+ <Link
+ href={`/${locale}`}
+ className="text-muted-foreground hover:text-foreground text-sm transition-colors"
+ >
{owner}
</Link>
<span className="text-muted-foreground">/</span>
- <span className="font-semibold text-sm truncate">{repo}</span>
- <span className="shrink-0 text-xs border border-border rounded-full px-2 py-0.5 text-muted-foreground">
+ <span className="truncate text-sm font-semibold">{repo}</span>
+ <span className="border-border text-muted-foreground shrink-0 rounded-full border px-2 py-0.5 text-xs">
{repoData.private ? dict.repo.private : dict.repo.public}
</span>
</div>
@@ -56,13 +57,13 @@ export default async function RepoLayout({ children, params }: Props) {
</div>
</header>
- <nav className="border-b border-border px-3 md:px-6">
+ <nav className="border-border border-b px-3 md:px-6">
<ul className="flex gap-1">
{tabs.map((tab) => (
<li key={tab.href}>
<Link
href={tab.href}
- className="inline-block px-3 py-2.5 text-sm text-muted-foreground hover:text-foreground border-b-2 border-transparent hover:border-foreground/30 transition-colors"
+ className="text-muted-foreground hover:text-foreground hover:border-foreground/30 inline-block border-b-2 border-transparent px-3 py-2.5 text-sm transition-colors"
>
{tab.label}
</Link>
@@ -71,9 +72,7 @@ export default async function RepoLayout({ children, params }: Props) {
</ul>
</nav>
- <div className="flex-1 flex overflow-hidden">
- {children}
- </div>
+ <div className="flex flex-1 overflow-hidden">{children}</div>
</div>
);
}
@@ -14,10 +14,7 @@ export default async function CodePage({ params, searchParams }: Props) {
const { path: selectedPath, branch: branchParam } = await searchParams;
const dict = await getDictionary(lang as Locale);
- const [repoData, branches] = await Promise.all([
- getRepo(owner, repo),
- getBranches(owner, repo),
- ]);
+ const [repoData, branches] = await Promise.all([getRepo(owner, repo), getBranches(owner, repo)]);
const branchNames = branches.map((b) => b.name);
const branch = branchNames.find((n) => n === branchParam) ?? repoData.default_branch;
@@ -39,13 +36,13 @@ export default async function CodePage({ params, searchParams }: Props) {
>
{selectedPath ? (
<div>
- <div className="px-4 py-2 border-b border-border bg-muted/40 text-sm font-mono text-muted-foreground">
+ <div className="border-border bg-muted/40 text-muted-foreground border-b px-4 py-2 font-mono text-sm">
{selectedPath}
</div>
<CodeViewer owner={owner} repo={repo} path={selectedPath} branch={branch} />
</div>
) : (
- <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
+ <div className="text-muted-foreground flex h-full items-center justify-center text-sm">
{dict.code.selectFile}
</div>
)}
@@ -4,11 +4,7 @@ import { ThemeToggle } from "@/components/ThemeToggle";
import { LangSwitcher } from "@/components/LangSwitcher";
import { getDictionary, type Locale } from "@/lib/i18n.server";
-export default async function HomePage({
- params,
-}: {
- params: Promise<{ lang: string }>;
-}) {
+export default async function HomePage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const locale = lang as Locale;
const [repos] = await Promise.all([
@@ -17,21 +13,21 @@ export default async function HomePage({
const dict = await getDictionary(locale);
return (
- <div className="min-h-screen bg-background">
- <header className="border-b border-border px-3 md:px-6 py-4 flex items-center justify-between">
+ <div className="bg-background min-h-screen">
+ <header className="border-border flex items-center justify-between border-b px-3 py-4 md:px-6">
<div className="flex items-center gap-2">
- <svg viewBox="0 0 16 16" className="w-6 h-6 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 viewBox="0 0 16 16" className="h-6 w-6 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="font-semibold text-lg">showmycode</span>
+ <span className="text-lg font-semibold">showmycode</span>
</div>
<div className="flex items-center gap-3">
<LangSwitcher currentLang={locale} />
<ThemeToggle />
</div>
</header>
- <main className="max-w-3xl mx-auto px-3 md:px-6 py-6 md:py-10">
+ <main className="mx-auto max-w-3xl px-3 py-6 md:px-6 md:py-10">
<ul className="space-y-3">
{repos.map((repo) => (
<li key={repo.full_name}>
@@ -2,14 +2,11 @@ import { NextRequest, NextResponse } from "next/server";
import { getAllowedRepos } from "@/lib/github";
const BASE = "https://api.github.com";
-const PAT = process.env.GITHUB_PAT!;
+const PAT = process.env.GITHUB_PAT!;
// This route proxies all GitHub API requests so the PAT never reaches the client.
// It validates every request against the GITHUB_REPOS allowlist before forwarding.
-export async function GET(
- req: NextRequest,
- { params }: { params: Promise<{ path: string[] }> }
-) {
+export async function GET(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params;
const ghPath = path.join("/");
@@ -35,11 +35,9 @@ export default function UnauthorizedPage() {
};
return (
- <div className="flex flex-col items-center justify-center min-h-screen gap-4">
+ <div className="flex min-h-screen flex-col items-center justify-center gap-4">
<p className="text-base font-semibold">Access restricted</p>
- <p className="text-sm text-muted-foreground">
- Enter your access token to continue.
- </p>
+ <p className="text-muted-foreground text-sm">Enter your access token to continue.</p>
<form onSubmit={handleSubmit} className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2">
<input
@@ -48,21 +46,18 @@ export default function UnauthorizedPage() {
placeholder="Access token"
autoFocus
onChange={() => setError(false)}
- className="text-sm px-3 py-1.5 w-56 rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-border"
+ className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-border w-56 rounded border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none"
/>
<button
type="submit"
disabled={submitting}
- className="text-sm px-3 py-1.5 rounded border border-border bg-muted hover:bg-accent transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
+ className="border-border bg-muted hover:bg-accent cursor-pointer rounded border px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
>
Enter
</button>
</div>
{/* Reserve space so the layout doesn't shift when the message toggles. */}
- <p
- aria-live="polite"
- className={`text-sm text-red-500 ${error ? "visible" : "invisible"}`}
- >
+ <p aria-live="polite" className={`text-sm text-red-500 ${error ? "visible" : "invisible"}`}>
Invalid token. Please try again.
</p>
</form>
@@ -55,8 +55,12 @@
}
@layer base {
- * { @apply border-border; }
- body { @apply bg-background text-foreground; }
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
}
/* Shiki line numbers. */
@@ -14,17 +14,19 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<html lang="ko" suppressHydrationWarning>
<head>
{/* Prevent dark mode flash on page load. */}
- <script dangerouslySetInnerHTML={{
- __html: `
+ <script
+ dangerouslySetInnerHTML={{
+ __html: `
try {
const saved = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (saved === "dark" || (!saved && prefersDark)) {
document.documentElement.classList.add("dark");
}
} catch {}
- `
- }} />
+ `,
+ }}
+ />
</head>
<body className={inter.className}>{children}</body>
</html>
@@ -1,16 +1,16 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as SelectPrimitive from "@radix-ui/react-select"
-import { Check, ChevronDown, ChevronUp } from "lucide-react"
+import * as React from "react";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { Check, ChevronDown, ChevronUp } from "lucide-react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
-const Select = SelectPrimitive.Root
+const Select = SelectPrimitive.Root;
-const SelectGroup = SelectPrimitive.Group
+const SelectGroup = SelectPrimitive.Group;
-const SelectValue = SelectPrimitive.Value
+const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -19,8 +19,8 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
- "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
- className
+ "border-input bg-background ring-offset-background data-[placeholder]:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+ className,
)}
{...props}
>
@@ -29,43 +29,36 @@ const SelectTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
-))
-SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+));
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
- className={cn(
- "flex cursor-default items-center justify-center py-1",
- className
- )}
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
-))
-SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+));
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
- className={cn(
- "flex cursor-default items-center justify-center py-1",
- className
- )}
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
-))
-SelectScrollDownButton.displayName =
- SelectPrimitive.ScrollDownButton.displayName
+));
+SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
@@ -75,10 +68,10 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
- "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] origin-[--radix-select-content-transform-origin] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
- className
+ className,
)}
position={position}
{...props}
@@ -88,28 +81,28 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
-))
-SelectContent.displayName = SelectPrimitive.Content.displayName
+));
+SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
- className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
+ className={cn("py-1.5 pr-2 pl-8 text-sm font-semibold", className)}
{...props}
/>
-))
-SelectLabel.displayName = SelectPrimitive.Label.displayName
+));
+SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
@@ -118,8 +111,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ "focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className,
)}
{...props}
>
@@ -131,20 +124,20 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
-))
-SelectItem.displayName = SelectPrimitive.Item.displayName
+));
+SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
+ className={cn("bg-muted -mx-1 my-1 h-px", className)}
{...props}
/>
-))
-SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+));
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
@@ -157,4 +150,4 @@ export {
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
-}
+};
@@ -29,7 +29,7 @@ export function BranchSelector({ branches, current }: Props) {
return (
<Select value={current} onValueChange={handleChange}>
- <SelectTrigger className="h-7 text-xs w-full focus:ring-0 focus:ring-offset-0">
+ <SelectTrigger className="h-7 w-full text-xs focus:ring-0 focus:ring-offset-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -16,9 +16,10 @@ export async function CodeViewer({
const file = await getContents(owner, repo, path, branch);
// Decode base64 content.
- const raw = file.encoding === "base64"
- ? Buffer.from(file.content.replace(/\n/g, ""), "base64").toString("utf-8")
- : file.content;
+ const raw =
+ file.encoding === "base64"
+ ? Buffer.from(file.content.replace(/\n/g, ""), "base64").toString("utf-8")
+ : file.content;
const lang = getLanguage(path);
@@ -29,7 +30,7 @@ export async function CodeViewer({
return (
<div
- className="code-viewer text-sm overflow-auto [&>pre]:p-5 [&>pre]:min-h-full"
+ className="code-viewer overflow-auto text-sm [&>pre]:min-h-full [&>pre]:p-5"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
@@ -14,28 +14,41 @@ type Dict = {
unfoldAll: string;
};
-function FileStatusBadge({ status, dict }: {
+function FileStatusBadge({
+ status,
+ dict,
+}: {
status: GhPullFile["status"];
dict: Pick<Dict, "added" | "removed" | "modified" | "renamed">;
}) {
const map: Record<string, { label: string; cls: string }> = {
- added: { label: dict.added, cls: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300" },
- removed: { label: dict.removed, cls: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300" },
- modified: { label: dict.modified, cls: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300" },
- renamed: { label: dict.renamed, cls: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300" },
+ added: {
+ label: dict.added,
+ cls: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
+ },
+ removed: {
+ label: dict.removed,
+ cls: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
+ },
+ modified: {
+ label: dict.modified,
+ cls: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300",
+ },
+ renamed: {
+ label: dict.renamed,
+ cls: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
+ },
};
const { label, cls } = map[status] ?? map.modified;
return (
- <span className={`text-xs px-1.5 py-0.5 rounded font-medium shrink-0 ${cls}`}>
- {label}
- </span>
+ <span className={`shrink-0 rounded px-1.5 py-0.5 text-xs font-medium ${cls}`}>{label}</span>
);
}
function DiffView({ patch }: { patch: string }) {
const lines = patch.split("\n");
return (
- <div className="overflow-x-auto text-xs font-mono leading-5">
+ <div className="overflow-x-auto font-mono text-xs leading-5">
{lines.map((line, i) => {
let cls: string;
if (line.startsWith("@@"))
@@ -44,8 +57,7 @@ function DiffView({ patch }: { patch: string }) {
cls = "bg-green-50 text-green-800 dark:bg-green-950/40 dark:text-green-300";
else if (line.startsWith("-"))
cls = "bg-red-50 text-red-800 dark:bg-red-950/40 dark:text-red-300";
- else
- cls = "text-muted-foreground";
+ else cls = "text-muted-foreground";
return (
<div key={i} className={`px-4 whitespace-pre ${cls}`}>
{line || " "}
@@ -69,22 +81,21 @@ export function FilesChanged({ files, dict }: { files: GhPullFile[]; dict: Dict
}
};
- const toggle = (sha: string) =>
- setFolded((prev) => ({ ...prev, [sha]: !prev[sha] }));
+ const toggle = (sha: string) => setFolded((prev) => ({ ...prev, [sha]: !prev[sha] }));
return (
<div>
- <div className="flex items-center justify-between mb-3">
- <p className="text-xs text-muted-foreground">
- <span className="text-green-600">+{files.reduce((s, f) => s + f.additions, 0)}</span>
- {" "}
+ <div className="mb-3 flex items-center justify-between">
+ <p className="text-muted-foreground text-xs">
+ <span className="text-green-600">+{files.reduce((s, f) => s + f.additions, 0)}</span>{" "}
<span className="text-red-500">-{files.reduce((s, f) => s + f.deletions, 0)}</span>
{" · "}
- {files.length}{dict.countSuffix}
+ {files.length}
+ {dict.countSuffix}
</p>
<button
onClick={toggleAll}
- className="text-xs text-muted-foreground hover:text-foreground transition-colors"
+ className="text-muted-foreground hover:text-foreground text-xs transition-colors"
>
{allFolded ? dict.unfoldAll : dict.foldAll}
</button>
@@ -94,36 +105,36 @@ export function FilesChanged({ files, dict }: { files: GhPullFile[]; dict: Dict
{files.map((file) => {
const isFolded = !!folded[file.sha];
return (
- <div key={file.sha} className="border border-border rounded-lg overflow-hidden">
+ <div key={file.sha} className="border-border overflow-hidden rounded-lg border">
<button
onClick={() => toggle(file.sha)}
- className="w-full flex items-center gap-2 px-4 py-2 bg-muted/50 border-b border-border flex-wrap text-left hover:bg-muted transition-colors"
+ className="bg-muted/50 border-border hover:bg-muted flex w-full flex-wrap items-center gap-2 border-b px-4 py-2 text-left transition-colors"
>
- <span className={`text-muted-foreground transition-transform shrink-0 ${isFolded ? "-rotate-90" : ""}`}>
+ <span
+ className={`text-muted-foreground shrink-0 transition-transform ${isFolded ? "-rotate-90" : ""}`}
+ >
▾
</span>
<FileStatusBadge status={file.status} dict={dict} />
- <span className="text-xs font-mono font-medium min-w-0 truncate flex-1">
+ <span className="min-w-0 flex-1 truncate font-mono text-xs font-medium">
{file.status === "renamed" && file.previous_filename
? `${file.previous_filename} → ${file.filename}`
: file.filename}
</span>
- <span className="text-xs font-mono text-muted-foreground shrink-0">
- <span className="text-green-600">+{file.additions}</span>
- {" "}
+ <span className="text-muted-foreground shrink-0 font-mono text-xs">
+ <span className="text-green-600">+{file.additions}</span>{" "}
<span className="text-red-500">-{file.deletions}</span>
</span>
</button>
- {!isFolded && (
- file.patch ? (
+ {!isFolded &&
+ (file.patch ? (
<DiffView patch={file.patch} />
) : (
- <p className="px-4 py-3 text-xs text-muted-foreground italic">
+ <p className="text-muted-foreground px-4 py-3 text-xs italic">
{dict.binaryFile}
</p>
- )
- )}
+ ))}
</div>
);
})}
@@ -70,22 +70,30 @@ function TreeNodeItem({
defaultOpenDepth: number;
}) {
const [open, setOpen] = useState(
- depth < defaultOpenDepth || (selectedPath?.startsWith(node.path + "/") ?? false)
+ depth < defaultOpenDepth || (selectedPath?.startsWith(node.path + "/") ?? false),
);
if (node.type === "tree") {
return (
<li>
<button
onClick={() => setOpen((o) => !o)}
- className="flex items-center gap-1.5 w-full text-left px-2 py-0.5 rounded text-sm hover:bg-accent transition-colors"
+ className="hover:bg-accent flex w-full items-center gap-1.5 rounded px-2 py-0.5 text-left text-sm transition-colors"
style={{ paddingLeft: `${8 + depth * 12}px` }}
>
- <svg viewBox="0 0 16 16" className={cn("w-4 h-4 fill-current text-yellow-500 shrink-0 transition-transform", open && "rotate-0")} aria-hidden>
- {open
- ? <path d="M1.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H7.5c-.55 0-1.07-.26-1.4-.7l-.9-1.2a.25.25 0 00-.2-.1H1.75z"/>
- : <path d="M1.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H7.5c-.55 0-1.07-.26-1.4-.7l-.9-1.2a.25.25 0 00-.2-.1H1.75z"/>
- }
+ <svg
+ viewBox="0 0 16 16"
+ className={cn(
+ "h-4 w-4 shrink-0 fill-current text-yellow-500 transition-transform",
+ open && "rotate-0",
+ )}
+ aria-hidden
+ >
+ {open ? (
+ <path d="M1.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H7.5c-.55 0-1.07-.26-1.4-.7l-.9-1.2a.25.25 0 00-.2-.1H1.75z" />
+ ) : (
+ <path d="M1.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H7.5c-.55 0-1.07-.26-1.4-.7l-.9-1.2a.25.25 0 00-.2-.1H1.75z" />
+ )}
</svg>
<span className="truncate">{node.name}</span>
</button>
@@ -116,15 +124,15 @@ function TreeNodeItem({
<Link
href={`/${lang}/repository/${owner}/${repo}?path=${encodeURIComponent(node.path)}&branch=${encodeURIComponent(branch)}`}
className={cn(
- "flex items-center gap-1.5 w-full text-sm px-2 py-0.5 rounded transition-colors truncate",
+ "flex w-full items-center gap-1.5 truncate rounded px-2 py-0.5 text-sm transition-colors",
isSelected
? "bg-accent text-accent-foreground font-medium"
- : "hover:bg-accent text-muted-foreground hover:text-foreground"
+ : "hover:bg-accent text-muted-foreground hover:text-foreground",
)}
style={{ paddingLeft: `${8 + depth * 12}px` }}
>
- <svg viewBox="0 0 16 16" className="w-4 h-4 fill-current shrink-0 opacity-50" aria-hidden>
- <path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75z"/>
+ <svg viewBox="0 0 16 16" className="h-4 w-4 shrink-0 fill-current opacity-50" aria-hidden>
+ <path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75z" />
</svg>
<span className="truncate">{node.name}</span>
</Link>
@@ -152,7 +160,7 @@ export function FileTree({
const tree = buildTree(items);
return (
- <ul className="text-sm space-y-0.5">
+ <ul className="space-y-0.5 text-sm">
{tree.map((node) => (
<TreeNodeItem
key={node.path}
@@ -22,7 +22,7 @@ export function LangSwitcher({ currentLang }: { currentLang: Locale }) {
<span key={lang} className="flex items-center gap-0.5">
{i > 0 && <span className="text-muted-foreground/40">|</span>}
{lang === currentLang ? (
- <span className="font-semibold text-foreground">{labels[lang]}</span>
+ <span className="text-foreground font-semibold">{labels[lang]}</span>
) : (
<Link
href={switchTo(lang)}
@@ -3,35 +3,42 @@ import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown";
const components: Components = {
- h1: ({ children }) => <h1 className="text-xl font-bold mt-6 mb-3 first:mt-0">{children}</h1>,
- h2: ({ children }) => <h2 className="text-lg font-semibold mt-5 mb-2 first:mt-0">{children}</h2>,
- h3: ({ children }) => <h3 className="text-base font-semibold mt-4 mb-1.5 first:mt-0">{children}</h3>,
- h4: ({ children }) => <h4 className="text-sm font-semibold mt-3 mb-1 first:mt-0">{children}</h4>,
- p: ({ children }) => <p className="text-sm leading-relaxed mb-3 last:mb-0">{children}</p>,
+ h1: ({ children }) => <h1 className="mt-6 mb-3 text-xl font-bold first:mt-0">{children}</h1>,
+ h2: ({ children }) => <h2 className="mt-5 mb-2 text-lg font-semibold first:mt-0">{children}</h2>,
+ h3: ({ children }) => (
+ <h3 className="mt-4 mb-1.5 text-base font-semibold first:mt-0">{children}</h3>
+ ),
+ h4: ({ children }) => <h4 className="mt-3 mb-1 text-sm font-semibold first:mt-0">{children}</h4>,
+ p: ({ children }) => <p className="mb-3 text-sm leading-relaxed last:mb-0">{children}</p>,
a: ({ href, children }) => (
- <a href={href} target="_blank" rel="noreferrer" className="text-blue-600 dark:text-blue-400 underline underline-offset-2 hover:opacity-80">
+ <a
+ href={href}
+ target="_blank"
+ rel="noreferrer"
+ className="text-blue-600 underline underline-offset-2 hover:opacity-80 dark:text-blue-400"
+ >
{children}
</a>
),
- ul: ({ children }) => <ul className="list-disc pl-5 mb-3 space-y-1 text-sm">{children}</ul>,
- ol: ({ children }) => <ol className="list-decimal pl-5 mb-3 space-y-1 text-sm">{children}</ol>,
+ ul: ({ children }) => <ul className="mb-3 list-disc space-y-1 pl-5 text-sm">{children}</ul>,
+ ol: ({ children }) => <ol className="mb-3 list-decimal space-y-1 pl-5 text-sm">{children}</ol>,
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
blockquote: ({ children }) => (
- <blockquote className="border-l-4 border-border pl-4 my-3 text-muted-foreground italic text-sm">
+ <blockquote className="border-border text-muted-foreground my-3 border-l-4 pl-4 text-sm italic">
{children}
</blockquote>
),
code: ({ className, children, ...props }) => {
const isBlock = className?.startsWith("language-");
if (isBlock) {
return (
- <pre className="bg-muted rounded-lg px-4 py-3 overflow-x-auto my-3 text-xs font-mono leading-5">
+ <pre className="bg-muted my-3 overflow-x-auto rounded-lg px-4 py-3 font-mono text-xs leading-5">
<code className={className}>{children}</code>
</pre>
);
}
return (
- <code className="bg-muted rounded px-1.5 py-0.5 text-xs font-mono" {...props}>
+ <code className="bg-muted rounded px-1.5 py-0.5 font-mono text-xs" {...props}>
{children}
</code>
);
@@ -41,19 +48,23 @@ const components: Components = {
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,
table: ({ children }) => (
- <div className="overflow-x-auto my-3">
- <table className="w-full text-sm border-collapse">{children}</table>
+ <div className="my-3 overflow-x-auto">
+ <table className="w-full border-collapse text-sm">{children}</table>
</div>
),
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
th: ({ children }) => (
- <th className="border border-border px-3 py-1.5 text-left font-semibold text-xs">{children}</th>
- ),
- td: ({ children }) => (
- <td className="border border-border px-3 py-1.5 text-xs">{children}</td>
+ <th className="border-border border px-3 py-1.5 text-left text-xs font-semibold">{children}</th>
),
+ td: ({ children }) => <td className="border-border border px-3 py-1.5 text-xs">{children}</td>,
input: ({ checked, disabled }) => (
- <input type="checkbox" checked={checked} disabled={disabled} readOnly className="mr-1.5 align-middle" />
+ <input
+ type="checkbox"
+ checked={checked}
+ disabled={disabled}
+ readOnly
+ className="mr-1.5 align-middle"
+ />
),
};
@@ -33,33 +33,27 @@ export function RepoCard({
return (
<Link
href={`/${lang}/repository/${owner}/${name}`}
- className="block rounded-lg border border-border bg-card p-5 hover:border-foreground/30 hover:shadow-sm transition-all"
+ className="border-border bg-card hover:border-foreground/30 block rounded-lg border p-5 transition-all hover:shadow-sm"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
{/* Repo name. */}
- <p className="font-semibold text-blue-600 dark:text-blue-400 truncate">
- {name}
- </p>
+ <p className="truncate font-semibold text-blue-600 dark:text-blue-400">{name}</p>
{/* Description. */}
{repo.description ? (
- <p className="text-sm text-muted-foreground mt-1 line-clamp-2">
- {repo.description}
- </p>
+ <p className="text-muted-foreground mt-1 line-clamp-2 text-sm">{repo.description}</p>
) : (
- <p className="text-sm text-muted-foreground/50 mt-1 italic">
- {dict.noDescription}
- </p>
+ <p className="text-muted-foreground/50 mt-1 text-sm italic">{dict.noDescription}</p>
)}
{/* Topic tags. */}
{repo.topics.length > 0 && (
- <div className="flex items-center gap-1.5 mt-2 flex-wrap">
+ <div className="mt-2 flex flex-wrap items-center gap-1.5">
{repo.topics.map((topic) => (
<span
key={topic}
- className="text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full px-2.5 py-0.5"
+ className="rounded-full bg-blue-500/10 px-2.5 py-0.5 text-xs text-blue-600 dark:text-blue-400"
>
{topic}
</span>
@@ -68,30 +62,32 @@ export function RepoCard({
)}
{/* Meta info. */}
- <div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground flex-wrap">
+ <div className="text-muted-foreground mt-3 flex flex-wrap items-center gap-4 text-xs">
{langColor && repo.language && (
<span className="flex items-center gap-1.5">
<span
- className="w-2.5 h-2.5 rounded-full shrink-0"
+ className="h-2.5 w-2.5 shrink-0 rounded-full"
style={{ background: langColor }}
/>
{repo.language}
</span>
)}
<span className="flex items-center gap-1">
- <svg viewBox="0 0 16 16" className="w-3.5 h-3.5 fill-current" aria-hidden>
- <path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
+ <svg viewBox="0 0 16 16" className="h-3.5 w-3.5 fill-current" aria-hidden>
+ <path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z" />
</svg>
{repo.stargazers_count}
</span>
- <span>{dict.updated} {formatDate(repo.updated_at, lang)}</span>
+ <span>
+ {dict.updated} {formatDate(repo.updated_at, lang)}
+ </span>
</div>
</div>
{/* Private/Public badge. */}
- <span className="shrink-0 text-xs border border-border rounded-full px-2 py-0.5 text-muted-foreground">
+ <span className="border-border text-muted-foreground shrink-0 rounded-full border px-2 py-0.5 text-xs">
{repo.private ? "Private" : "Public"}
</span>
</div>
@@ -13,14 +13,22 @@ type Props = {
branch: string;
};
-export async function Sidebar({ owner, repo, lang, filesLabel, selectedPath, branches, branch }: Props) {
+export async function Sidebar({
+ owner,
+ repo,
+ lang,
+ filesLabel,
+ selectedPath,
+ branches,
+ branch,
+}: Props) {
const { tree } = await getTree(owner, repo, branch);
const defaultOpenDepth = Number(process.env.FILE_TREE_DEPTH) || 0;
return (
- <aside className="flex flex-col h-full overflow-hidden">
- <div className="px-3 py-2.5 border-b border-border space-y-2">
- <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
+ <aside className="flex h-full flex-col overflow-hidden">
+ <div className="border-border space-y-2 border-b px-3 py-2.5">
+ <p className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
{filesLabel}
</p>
<Suspense>
@@ -15,37 +15,34 @@ export function SidebarDrawer({ sidebar, filesLabel, children }: Props) {
<div className="flex flex-1 overflow-hidden">
{/* Mobile: overlay backdrop. */}
{open && (
- <div
- className="fixed inset-0 z-40 bg-black/40 md:hidden"
- onClick={() => setOpen(false)}
- />
+ <div className="fixed inset-0 z-40 bg-black/40 md:hidden" onClick={() => setOpen(false)} />
)}
{/* Sidebar: fixed drawer on mobile, static column on desktop. */}
<aside
className={[
- "fixed inset-y-0 left-0 z-50 w-64 bg-background",
- "border-r border-border flex flex-col overflow-hidden",
+ "bg-background fixed inset-y-0 left-0 z-50 w-64",
+ "border-border flex flex-col overflow-hidden border-r",
"transition-transform duration-200",
- "md:relative md:inset-auto md:z-auto md:translate-x-0 md:shrink-0",
+ "md:relative md:inset-auto md:z-auto md:shrink-0 md:translate-x-0",
open ? "translate-x-0" : "-translate-x-full",
].join(" ")}
>
{sidebar}
</aside>
{/* Main content area. */}
- <main className="flex-1 overflow-auto min-w-0">
+ <main className="min-w-0 flex-1 overflow-auto">
{/* Mobile-only Files toggle button. */}
- <div className="md:hidden flex items-center px-3 py-2 border-b border-border">
+ <div className="border-border flex items-center border-b px-3 py-2 md:hidden">
<button
onClick={() => setOpen(true)}
aria-label={filesLabel}
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
+ className="text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1.5 text-xs transition-colors"
>
<svg
viewBox="0 0 20 20"
- className="w-4 h-4"
+ className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2"
@@ -14,7 +14,7 @@ export function ThemeToggle() {
const dark = useSyncExternalStore(
subscribe,
() => document.documentElement.classList.contains("dark"),
- () => false
+ () => false,
);
const toggle = () => {
@@ -26,17 +26,17 @@ export function ThemeToggle() {
return (
<button
onClick={toggle}
- className="p-2 rounded-md hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
+ className="hover:bg-accent text-muted-foreground hover:text-foreground rounded-md p-2 transition-colors"
aria-label="Toggle theme"
suppressHydrationWarning
>
{dark ? (
- <svg viewBox="0 0 16 16" className="w-4 h-4 fill-current" aria-hidden>
- <path d="M8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.061zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.061 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"/>
+ <svg viewBox="0 0 16 16" className="h-4 w-4 fill-current" aria-hidden>
+ <path d="M8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.061zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.061 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z" />
</svg>
) : (
- <svg viewBox="0 0 16 16" className="w-4 h-4 fill-current" aria-hidden>
- <path d="M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786z"/>
+ <svg viewBox="0 0 16 16" className="h-4 w-4 fill-current" aria-hidden>
+ <path d="M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786z" />
</svg>
)}
</button>
@@ -1,5 +1,5 @@
const BASE = "https://api.github.com";
-const PAT = process.env.GITHUB_PAT!;
+const PAT = process.env.GITHUB_PAT!;
const headers = {
Authorization: `Bearer ${PAT}`,
@@ -24,7 +24,10 @@ export async function getRepo(owner: string, repo: string) {
// Allowed repo list (from environment variables).
export function getAllowedRepos(): { owner: string; repo: string }[] {
const owner = process.env.GITHUB_OWNER!;
- const repos = (process.env.GITHUB_REPOS ?? "").split(",").map((r) => r.trim()).filter(Boolean);
+ const repos = (process.env.GITHUB_REPOS ?? "")
+ .split(",")
+ .map((r) => r.trim())
+ .filter(Boolean);
return repos.map((repo) => ({ owner, repo }));
}
@@ -35,14 +38,17 @@ export async function getTree(owner: string, repo: string, ref = "HEAD") {
// File contents.
export async function getContents(owner: string, repo: string, path: string, ref = "HEAD") {
- return ghFetch<GhContent>(
- `repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`,
- { ref }
- );
+ return ghFetch<GhContent>(`repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, { ref });
}
// Commit list. The `sha` param accepts a branch name or commit SHA.
-export async function getCommits(owner: string, repo: string, sha = "HEAD", perPage = 30, page = 1) {
+export async function getCommits(
+ owner: string,
+ repo: string,
+ sha = "HEAD",
+ perPage = 30,
+ page = 1,
+) {
return ghFetch<GhCommit[]>(`repos/${owner}/${repo}/commits`, {
sha,
per_page: String(perPage),
@@ -56,7 +62,13 @@ export async function getBranches(owner: string, repo: string) {
}
// PR list.
-export async function getPulls(owner: string, repo: string, state: "open" | "closed" | "all" = "all", perPage = 30, page = 1) {
+export async function getPulls(
+ owner: string,
+ repo: string,
+ state: "open" | "closed" | "all" = "all",
+ perPage = 30,
+ page = 1,
+) {
return ghFetch<GhPull[]>(`repos/${owner}/${repo}/pulls`, {
state,
per_page: String(perPage),
@@ -16,14 +16,32 @@ export function formatDate(iso: string, lang = "ko") {
export function getLanguage(filename: string) {
const ext = filename.split(".").pop() ?? "";
const map: Record<string, string> = {
- ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx",
- py: "python", go: "go", rs: "rust", java: "java",
- kt: "kotlin", swift: "swift", rb: "ruby", php: "php",
- md: "markdown", mdx: "mdx", json: "json",
- yaml: "yaml", yml: "yaml", toml: "toml",
- css: "css", scss: "scss", html: "html",
- sh: "bash", bash: "bash", zsh: "bash",
- sql: "sql", graphql: "graphql",
+ ts: "typescript",
+ tsx: "tsx",
+ js: "javascript",
+ jsx: "jsx",
+ py: "python",
+ go: "go",
+ rs: "rust",
+ java: "java",
+ kt: "kotlin",
+ swift: "swift",
+ rb: "ruby",
+ php: "php",
+ md: "markdown",
+ mdx: "mdx",
+ json: "json",
+ yaml: "yaml",
+ yml: "yaml",
+ toml: "toml",
+ css: "css",
+ scss: "scss",
+ html: "html",
+ sh: "bash",
+ bash: "bash",
+ zsh: "bash",
+ sql: "sql",
+ graphql: "graphql",
dockerfile: "dockerfile",
};
return map[ext.toLowerCase()] ?? "text";
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
@@ -0,0 +1,4 @@
+# Revisions listed here are skipped by `git blame` when contributors opt in via:
+# git config blame.ignoreRevsFile .git-blame-ignore-revs
+#
+# Add the squash-merge SHA of bulk reformat commits below (one per line).
@@ -0,0 +1,10 @@
+* text=auto eol=lf
+
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.webp binary
+*.woff binary
+*.woff2 binary
@@ -0,0 +1,10 @@
+.next/
+out/
+build/
+node_modules/
+package-lock.json
+next-env.d.ts
+public/
+.vercel/
+coverage/
+*.tsbuildinfo
@@ -0,0 +1,10 @@
+{
+ "semi": true,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "arrowParens": "always",
+ "endOfLine": "lf",
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
@@ -1,5 +1,7 @@
<!-- BEGIN:nextjs-agent-rules -->
+
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
+
<!-- END:nextjs-agent-rules -->
@@ -9,17 +9,24 @@ This project uses a recent version of Next.js with breaking changes. Before writ
## Commands
```bash
-npm run dev # Start development server (http://localhost:3000)
-npm run build # Production build
-npm run start # Start production server
-npm run lint # Run ESLint
+npm run dev # Start development server (http://localhost:3000)
+npm run build # Production build
+npm run start # Start production server
+npm run lint # Run ESLint
+npm run lint:fix # Run ESLint with --fix
+npm run format # Run Prettier write (includes Tailwind class sort)
+npm run format:check # Run Prettier check (fails on diff — used in CI)
+npm run typecheck # Run tsc --noEmit
```
No test framework is configured.
+Coding conventions and the full toolchain rationale live in [CONTRIBUTING.md](CONTRIBUTING.md#code-conventions).
+
## Environment Setup
Copy `.env.example` to `.env.local` and fill in:
+
- `GITHUB_PAT` — Fine-grained GitHub personal access token (read-only: Contents + Pull requests)
- `GITHUB_OWNER` — GitHub username/org
- `GITHUB_REPOS` — Comma-separated repository names to expose
@@ -0,0 +1,13 @@
+// Commitlint configuration for showmycode.
+// Allowed types mirror the branch prefixes documented in CONTRIBUTING.md.
+// Note: `i18n` is not part of @commitlint/config-conventional, so we override type-enum.
+
+/** @type {import("@commitlint/types").UserConfig} */
+const config = {
+ extends: ["@commitlint/config-conventional"],
+ rules: {
+ "type-enum": [2, "always", ["feat", "fix", "chore", "refactor", "docs", "test", "i18n"]],
+ },
+};
+
+module.exports = config;
@@ -28,15 +28,15 @@ Thank you for your interest in contributing to showmycode! This guide will help
Create a branch from `main` using the following prefixes:
-| Prefix | Purpose | Example |
-|--------|---------|---------|
-| `feat/` | New feature | `feat/repo-search` |
-| `fix/` | Bug fix | `fix/dark-mode-flash` |
-| `chore/` | Config, dependencies, CI | `chore/upgrade-next` |
-| `refactor/` | Code restructuring | `refactor/github-api` |
-| `docs/` | Documentation only | `docs/setup-guide` |
-| `test/` | Adding or updating tests | `test/api-route` |
-| `i18n/` | Internationalization (translations, locale) | `i18n/add-ja-locale` |
+| Prefix | Purpose | Example |
+| ----------- | ------------------------------------------- | --------------------- |
+| `feat/` | New feature | `feat/repo-search` |
+| `fix/` | Bug fix | `fix/dark-mode-flash` |
+| `chore/` | Config, dependencies, CI | `chore/upgrade-next` |
+| `refactor/` | Code restructuring | `refactor/github-api` |
+| `docs/` | Documentation only | `docs/setup-guide` |
+| `test/` | Adding or updating tests | `test/api-route` |
+| `i18n/` | Internationalization (translations, locale) | `i18n/add-ja-locale` |
## Commit Messages
@@ -56,34 +56,63 @@ i18n: add Japanese locale support
## Pull Requests
1. Ensure your branch is up to date with `main`
-2. Run checks locally before pushing:
- ```bash
- npm run build && npm run lint
- ```
+2. Run checks locally before pushing (see [Local Verification](#local-verification))
3. Open a PR against `main` and fill in the PR template
-4. PRs are merged via **squash and merge**
+4. PRs are merged via **squash and merge** — the **PR title** becomes the final commit message on `main`, so it must follow Conventional Commits
### What makes a good PR
- **Small and focused** — one concern per PR
-- **Descriptive title** — follows Conventional Commits format
+- **Descriptive title** — follows Conventional Commits format (this title is what lands on `main`)
- **Filled-in template** — explains what changed and how to verify
-- **Passing CI** — build and lint must pass
+- **Passing CI** — build, lint, format check, and type check must pass
## Internationalization (i18n)
All user-facing strings must support both **Korean (KO)** and **English (EN)**. When adding or modifying UI text:
-1. Add the string to both `dictionaries/ko.json` and `dictionaries/en.json`
+1. Add the string to both `locales/ko.json` and `locales/en.json`
2. Access it via the dictionary passed through server components
3. Never hardcode display text in components
-## Code Style
+## Code Conventions
+
+The project uses automated tooling to enforce style. Run these locally and they will run again in CI on every PR.
+
+| Tool | Command | What it does |
+| ---------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| Prettier | `npm run format` / `npm run format:check` | Formats `.ts`, `.tsx`, `.js`, `.json`, `.md`, `.css`, etc. Includes Tailwind class ordering via `prettier-plugin-tailwindcss`. |
+| ESLint | `npm run lint` / `npm run lint:fix` | Next.js core-web-vitals + TypeScript rules. |
+| TypeScript | `npm run typecheck` | `tsc --noEmit` against `tsconfig.json` (strict mode). |
+| Commitlint | `npm run commitlint` | Validates Conventional Commits on PR titles. Allowed types: `feat`, `fix`, `chore`, `refactor`, `docs`, `test`, `i18n`. |
+
+Additional rules:
+
+- **TypeScript** — all code must be typed; avoid `any`. Co-locate types next to implementations (see `lib/github.ts` for the pattern).
+- **Server Components by default** — only mark a component `"use client"` when interactivity is required.
+- **Tailwind CSS** — use utility classes; avoid inline styles. Class order is enforced by Prettier.
+- **Comments** — write all code comments in English and end them with a period (consistent with `CLAUDE.md`). This is not lint-enforced; reviewers will ask for fixes.
+
+### git blame hygiene
+
+Bulk reformat commits are listed in `.git-blame-ignore-revs`. To skip them in `git blame` output:
+
+```bash
+git config blame.ignoreRevsFile .git-blame-ignore-revs
+```
+
+## Local Verification
+
+Run before pushing a PR:
+
+```bash
+npm run format:check
+npm run lint
+npm run typecheck
+npm run build
+```
-- **TypeScript** — all code must be typed; avoid `any`
-- **Server Components by default** — only use `"use client"` when interactivity is required
-- **Tailwind CSS** — use utility classes; avoid inline styles
-- Follow existing patterns in the codebase
+If `format:check` fails, run `npm run format` to auto-fix.
## Reporting Issues
@@ -20,12 +20,16 @@
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
+ "@commitlint/cli": "^20.5.2",
+ "@commitlint/config-conventional": "^20.5.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20.19.37",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.1",
+ "prettier": "^3.8.3",
+ "prettier-plugin-tailwindcss": "^0.7.3",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -283,6 +287,349 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@commitlint/cli": {
+ "version": "20.5.2",
+ "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.2.tgz",
+ "integrity": "sha512-IXr5xd3IX8SEG936P8gcpozRplkDeDSwJlt8UvoY1winwIy2udTbQ/cOCgbaaxcjdDqVoS29VUcz/wkwnSozbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/format": "^20.5.0",
+ "@commitlint/lint": "^20.5.0",
+ "@commitlint/load": "^20.5.2",
+ "@commitlint/read": "^20.5.0",
+ "@commitlint/types": "^20.5.0",
+ "tinyexec": "^1.0.0",
+ "yargs": "^17.0.0"
+ },
+ "bin": {
+ "commitlint": "cli.js"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/config-conventional": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz",
+ "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.5.0",
+ "conventional-changelog-conventionalcommits": "^9.2.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/config-validator": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz",
+ "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.5.0",
+ "ajv": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/config-validator/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@commitlint/ensure": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz",
+ "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.5.0",
+ "lodash.camelcase": "^4.3.0",
+ "lodash.kebabcase": "^4.1.1",
+ "lodash.snakecase": "^4.1.1",
+ "lodash.startcase": "^4.4.0",
+ "lodash.upperfirst": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/execute-rule": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz",
+ "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/format": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz",
+ "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.5.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/is-ignored": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz",
+ "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.5.0",
+ "semver": "^7.6.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/is-ignored/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@commitlint/lint": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz",
+ "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/is-ignored": "^20.5.0",
+ "@commitlint/parse": "^20.5.0",
+ "@commitlint/rules": "^20.5.0",
+ "@commitlint/types": "^20.5.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/load": {
+ "version": "20.5.2",
+ "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.2.tgz",
+ "integrity": "sha512-zmr0RGDz7vThxW1I8ohb9yBjnGuH9mqwJpn21hInjGla+IlLOkS9ey0+dD5HlkzFlY0lX2NYdA2lDW6/0rO7Gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^20.5.0",
+ "@commitlint/execute-rule": "^20.0.0",
+ "@commitlint/resolve-extends": "^20.5.2",
+ "@commitlint/types": "^20.5.0",
+ "cosmiconfig": "^9.0.1",
+ "cosmiconfig-typescript-loader": "^6.1.0",
+ "is-plain-obj": "^4.1.0",
+ "lodash.mergewith": "^4.6.2",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/message": {
+ "version": "20.4.3",
+ "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz",
+ "integrity": "sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/parse": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz",
+ "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.5.0",
+ "conventional-changelog-angular": "^8.2.0",
+ "conventional-commits-parser": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/read": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz",
+ "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/top-level": "^20.4.3",
+ "@commitlint/types": "^20.5.0",
+ "git-raw-commits": "^5.0.0",
+ "minimist": "^1.2.8",
+ "tinyexec": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/resolve-extends": {
+ "version": "20.5.2",
+ "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.2.tgz",
+ "integrity": "sha512-8EhSCU9eNos/5cI1yg64GW79UH1c64O69AfStCsj4zqy6An/qIphVEXj4/+2M6056T8coz00f+UXFn4WUUP1HQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^20.5.0",
+ "@commitlint/types": "^20.5.0",
+ "global-directory": "^5.0.0",
+ "import-meta-resolve": "^4.0.0",
+ "lodash.mergewith": "^4.6.2",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/resolve-extends/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@commitlint/rules": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz",
+ "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/ensure": "^20.5.0",
+ "@commitlint/message": "^20.4.3",
+ "@commitlint/to-lines": "^20.0.0",
+ "@commitlint/types": "^20.5.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/to-lines": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz",
+ "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/top-level": {
+ "version": "20.4.3",
+ "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.3.tgz",
+ "integrity": "sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/types": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz",
+ "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "conventional-commits-parser": "^6.3.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@conventional-changelog/git-client": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz",
+ "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@simple-libs/child-process-utils": "^1.0.0",
+ "@simple-libs/stream-utils": "^1.2.0",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "conventional-commits-filter": "^5.0.0",
+ "conventional-commits-parser": "^6.4.0"
+ },
+ "peerDependenciesMeta": {
+ "conventional-commits-filter": {
+ "optional": true
+ },
+ "conventional-commits-parser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@conventional-changelog/git-client/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
@@ -1874,6 +2221,35 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
+ "node_modules/@simple-libs/child-process-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz",
+ "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@simple-libs/stream-utils": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/dangreen"
+ }
+ },
+ "node_modules/@simple-libs/stream-utils": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz",
+ "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/dangreen"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -2872,6 +3248,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -2934,6 +3320,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array-ify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz",
+ "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/array-includes": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
@@ -3370,6 +3763,21 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3409,20 +3817,119 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/compare-func": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz",
+ "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-ify": "^1.0.0",
+ "dot-prop": "^5.1.0"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
+ "node_modules/conventional-changelog-angular": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz",
+ "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/conventional-changelog-conventionalcommits": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz",
+ "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/conventional-commits-parser": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz",
+ "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@simple-libs/stream-utils": "^1.2.0",
+ "meow": "^13.0.0"
+ },
+ "bin": {
+ "conventional-commits-parser": "dist/cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
+ "node_modules/cosmiconfig": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
+ "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cosmiconfig-typescript-loader": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz",
+ "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jiti": "2.6.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "cosmiconfig": ">=9",
+ "typescript": ">=5"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3629,6 +4136,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3672,6 +4192,26 @@
"node": ">=10.13.0"
}
},
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.24.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
@@ -4370,6 +4910,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -4521,6 +5078,16 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -4600,6 +5167,23 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/git-raw-commits": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz",
+ "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@conventional-changelog/git-client": "^2.6.0",
+ "meow": "^13.0.0"
+ },
+ "bin": {
+ "git-raw-commits": "src/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -4613,6 +5197,22 @@
"node": ">=10.13.0"
}
},
+ "node_modules/global-directory": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz",
+ "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "6.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -4884,6 +5484,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -4894,6 +5505,16 @@
"node": ">=0.8.19"
}
},
+ "node_modules/ini": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
+ "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
"node_modules/inline-style-parser": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
@@ -4957,6 +5578,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-async-function": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@@ -5133,6 +5761,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-generator-function": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
@@ -5229,6 +5867,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -5468,6 +6116,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -5816,6 +6471,13 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -5832,13 +6494,55 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.kebabcase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+ "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.snakecase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.startcase": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
+ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.upperfirst": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -6193,6 +6897,19 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/meow": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+ "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -7203,6 +7920,25 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -7298,6 +8034,101 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.3.tgz",
+ "integrity": "sha512-lckXaWWdo2ZVXoMoUO3WIBiz9hVY+YBEh1gYyMFfrWP9WZW/wpFXQKizHx7WrFQFMkcG0bGShdpp531X1n+qpg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-hermes": "*",
+ "@prettier/plugin-oxc": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig": "*",
+ "prettier": "^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-multiline-arrays": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-sort-imports": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-hermes": {
+ "optional": true
+ },
+ "@prettier/plugin-oxc": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@zackad/prettier-plugin-twig": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-multiline-arrays": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7609,6 +8440,26 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -8021,6 +8872,28 @@
"node": ">= 0.4"
}
},
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -8148,6 +9021,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -8269,6 +9155,16 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tinyexec": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
+ "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -8899,13 +9795,70 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -6,7 +6,12 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "eslint"
+ "lint": "eslint",
+ "lint:fix": "eslint --fix",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "typecheck": "tsc --noEmit",
+ "commitlint": "commitlint --edit"
},
"dependencies": {
"@radix-ui/react-select": "^2.2.6",
@@ -21,12 +26,16 @@
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
+ "@commitlint/cli": "^20.5.2",
+ "@commitlint/config-conventional": "^20.5.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20.19.37",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.1",
+ "prettier": "^3.8.3",
+ "prettier-plugin-tailwindcss": "^0.7.3",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -17,7 +17,7 @@ function redirectToLocale(request: NextRequest): NextResponse | undefined {
// Pass through if locale prefix already present.
const hasLocalePrefix = locales.some(
- (l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`)
+ (l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`),
);
if (hasLocalePrefix) return;
@@ -77,10 +77,10 @@ http://localhost:3000 에서 확인할 수 있습니다.
1. [Vercel](https://vercel.com)에 레포지토리 연결
2. 대시보드 → Settings → Environment Variables에 아래 3개 등록
-| Key | Value |
-|---|---|
-| `GITHUB_PAT` | GitHub Personal Access Token |
-| `GITHUB_OWNER` | GitHub 유저명 |
+| Key | Value |
+| -------------- | ---------------------------- |
+| `GITHUB_PAT` | GitHub Personal Access Token |
+| `GITHUB_OWNER` | GitHub 유저명 |
| `GITHUB_REPOS` | 쉼표로 구분된 레포 이름 목록 |
3. Deploy
@@ -90,32 +90,37 @@ http://localhost:3000 에서 확인할 수 있습니다.
## 오픈소스 전환 체크리스트
### 보안
+
- [ ] `.env.local`이 `.gitignore`에 포함되어 있는지 확인
- [ ] `.env.example` 파일 생성 (실제 값 없이 키 이름만 포함)
- [ ] git log에 PAT 등 민감한 정보가 없는지 확인 (`git log --all`)
- [ ] `GITHUB_OWNER`, `GITHUB_REPOS` 환경 변수로만 제어되는지 확인
### 코드 정리
+
- [ ] 하드코딩된 개인 정보 제거 (이름, 이메일, URL 등)
- [ ] 불필요한 `console.log` 제거
- [ ] TypeScript 타입 오류 없는지 확인 (`npm run build`)
- [ ] 사용하지 않는 패키지 제거 (`npm prune`)
### 문서화
+
- [ ] `README.md` 작성 (이 파일)
- [ ] `.env.example` 작성
- [ ] `CONTRIBUTING.md` 작성 (기여 방법 안내)
- [ ] `LICENSE` 파일 추가 (MIT 권장)
- [ ] 주요 컴포넌트에 JSDoc 주석 추가
### GitHub 설정
+
- [ ] 레포지토리 Public으로 전환
- [ ] `Description` 및 `Topics` 태그 입력 (예: `nextjs`, `github-api`, `portfolio`)
- [ ] Issue 템플릿 추가 (`.github/ISSUE_TEMPLATE/`)
- [ ] PR 템플릿 추가 (`.github/pull_request_template.md`)
- [ ] `About` 섹션에 데모 URL 입력
### 선택 사항
+
- [ ] 데모 사이트 배포 후 README에 링크 추가
- [ ] 스크린샷 또는 GIF 데모 추가
- [ ] GitHub Actions CI 설정 (빌드 자동 검증)