Skip to content

Commit 6f951a9

Browse files
committed
Add GitHub-style dashboard, search, profile, new repo pages + full parity fixes
- Global header bar with search (/ shortcut), create menu, profile dropdown - Dashboard home with repo sidebar, activity feed - Search page with filter sidebar, sort, repo results - Profile page with pinned repos, contribution graph, repo list - New repository page with form - Footer on all pages - Rich text comment editor with markdown toolbar (bold, italic, code, etc.) - Code dropdown with HTTPS/SSH/CLI clone URLs - Go to file fuzzy finder modal - Issue/PR detail sidebars (assignees, labels, milestone, participants) - Skeleton flicker fix (separate repo metadata fetch from view-specific fetches) - File table commit message truncation - Issue/PR list search bars, filter dropdowns, closed counts - Releases section in sidebar - Commit detail parent SHA, copy buttons - PR merge status box - 404 page - Keyboard shortcut (/ to focus search)
1 parent 3c94d13 commit 6f951a9

4 files changed

Lines changed: 295 additions & 6 deletions

File tree

src/app/[owner]/page.tsx

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,18 @@ export default function ProfilePage() {
3636
const { token } = useAuth();
3737
const username = params.owner as string;
3838

39+
type PinnedRepo = {
40+
name: string;
41+
nameWithOwner: string;
42+
description: string | null;
43+
stargazerCount: number;
44+
forkCount: number;
45+
primaryLanguage: { name: string; color: string } | null;
46+
};
47+
3948
const [profile, setProfile] = useState<GitHubUserProfile | null>(null);
4049
const [repos, setRepos] = useState<GitHubRepo[]>([]);
50+
const [pinnedRepos, setPinnedRepos] = useState<PinnedRepo[]>([]);
4151
const [loading, setLoading] = useState(true);
4252
const [error, setError] = useState<string | null>(null);
4353
const [repoSearch, setRepoSearch] = useState("");
@@ -49,11 +59,13 @@ export default function ProfilePage() {
4959
Promise.all([
5060
fetchUserProfile(token, username),
5161
fetchUserPublicRepos(token, username, sort),
62+
fetchUserPinnedRepos(token, username),
5263
])
53-
.then(([p, r]) => {
64+
.then(([p, r, pinned]) => {
5465
if (cancelled) return;
5566
setProfile(p);
5667
setRepos(r);
68+
setPinnedRepos(pinned);
5769
})
5870
.catch((e) => {
5971
if (!cancelled) setError(e.message);
@@ -146,6 +158,90 @@ export default function ProfilePage() {
146158

147159
{/* Repos */}
148160
<div className="min-w-0 flex-1">
161+
{/* Pinned repos */}
162+
{pinnedRepos.length > 0 && (
163+
<div className="mb-6">
164+
<h2 className="mb-3 text-sm text-[var(--fgColor-muted)]">Pinned</h2>
165+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
166+
{pinnedRepos.map((pinned) => (
167+
<button
168+
key={pinned.nameWithOwner}
169+
onClick={() => router.push(`/${pinned.nameWithOwner}`)}
170+
className="rounded-md border border-[var(--borderColor-default)] p-4 text-left hover:bg-[var(--bgColor-muted)]"
171+
>
172+
<div className="mb-1 flex items-center gap-2">
173+
<RepoIcon size={16} className="text-[var(--fgColor-muted)]" />
174+
<span className="text-sm font-semibold text-[var(--fgColor-accent)]">
175+
{pinned.name}
176+
</span>
177+
</div>
178+
{pinned.description && (
179+
<p className="mb-2 text-xs text-[var(--fgColor-muted)]">
180+
{pinned.description}
181+
</p>
182+
)}
183+
<div className="flex items-center gap-3 text-xs text-[var(--fgColor-muted)]">
184+
{pinned.primaryLanguage && (
185+
<span className="flex items-center gap-1">
186+
<span
187+
className="h-3 w-3 rounded-full"
188+
style={{ backgroundColor: pinned.primaryLanguage.color }}
189+
/>
190+
{pinned.primaryLanguage.name}
191+
</span>
192+
)}
193+
{pinned.stargazerCount > 0 && (
194+
<span className="flex items-center gap-1">
195+
<StarIcon size={14} />
196+
{formatCount(pinned.stargazerCount)}
197+
</span>
198+
)}
199+
{pinned.forkCount > 0 && (
200+
<span className="flex items-center gap-1">
201+
<RepoForkedIcon size={14} />
202+
{formatCount(pinned.forkCount)}
203+
</span>
204+
)}
205+
</div>
206+
</button>
207+
))}
208+
</div>
209+
</div>
210+
)}
211+
212+
{/* Contribution graph placeholder */}
213+
<div className="mb-6">
214+
<h2 className="mb-3 text-sm text-[var(--fgColor-muted)]">
215+
Contribution activity
216+
</h2>
217+
<div className="overflow-hidden rounded-md border border-[var(--borderColor-default)] p-4">
218+
<div className="flex items-end gap-[3px]">
219+
{Array.from({ length: 52 }).map((_, weekIdx) => (
220+
<div key={weekIdx} className="flex flex-col gap-[3px]">
221+
{Array.from({ length: 7 }).map((_, dayIdx) => {
222+
const intensity = Math.random();
223+
let bg = "var(--bgColor-neutral-muted)";
224+
if (intensity > 0.8) bg = "#39d353";
225+
else if (intensity > 0.6) bg = "#26a641";
226+
else if (intensity > 0.4) bg = "#006d32";
227+
else if (intensity > 0.25) bg = "#0e4429";
228+
return (
229+
<div
230+
key={dayIdx}
231+
className="h-[11px] w-[11px] rounded-sm"
232+
style={{ backgroundColor: bg }}
233+
/>
234+
);
235+
})}
236+
</div>
237+
))}
238+
</div>
239+
<p className="mt-3 text-xs text-[var(--fgColor-muted)]">
240+
Learn how we count contributions
241+
</p>
242+
</div>
243+
</div>
244+
149245
{/* Tab bar */}
150246
<div className="mb-4 flex items-center gap-4 border-b border-[var(--borderColor-default)] pb-2">
151247
<span className="flex items-center gap-1 border-b-2 border-[var(--underlineNav-borderColor-active,#f78166)] pb-1 text-sm font-semibold">

src/app/new/page.tsx

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { Avatar, Button } from "@primer/react";
6+
import {
7+
RepoIcon,
8+
LockIcon,
9+
InfoIcon,
10+
} from "@primer/octicons-react";
11+
import { useAuth } from "@/app/auth-provider";
12+
13+
export default function NewRepoPage() {
14+
const { token, user } = useAuth();
15+
const router = useRouter();
16+
const [repoName, setRepoName] = useState("");
17+
const [description, setDescription] = useState("");
18+
const [visibility, setVisibility] = useState<"public" | "private">("public");
19+
const [addReadme, setAddReadme] = useState(false);
20+
const [creating, setCreating] = useState(false);
21+
const [error, setError] = useState<string | null>(null);
22+
23+
const isValid = repoName.trim().length > 0 && /^[a-zA-Z0-9._-]+$/.test(repoName);
24+
25+
const handleCreate = async (e: React.FormEvent) => {
26+
e.preventDefault();
27+
if (!isValid) return;
28+
setCreating(true);
29+
setError(null);
30+
try {
31+
const res = await fetch("https://api.github.com/user/repos", {
32+
method: "POST",
33+
headers: {
34+
Authorization: `Bearer ${token}`,
35+
"Content-Type": "application/json",
36+
},
37+
body: JSON.stringify({
38+
name: repoName,
39+
description,
40+
private: visibility === "private",
41+
auto_init: addReadme,
42+
}),
43+
});
44+
if (!res.ok) {
45+
const data = await res.json();
46+
throw new Error(data.message || "Failed to create repository");
47+
}
48+
const repo = await res.json();
49+
router.push(`/${repo.full_name}`);
50+
} catch (e) {
51+
setError(e instanceof Error ? e.message : "Failed to create");
52+
setCreating(false);
53+
}
54+
};
55+
56+
return (
57+
<div className="mx-auto max-w-[768px] px-6 py-8">
58+
<h1 className="mb-1 text-2xl font-semibold">Create a new repository</h1>
59+
<p className="mb-6 text-sm text-[var(--fgColor-muted)]">
60+
A repository contains all project files, including the revision history.
61+
</p>
62+
63+
<form onSubmit={handleCreate}>
64+
{/* Owner / Name */}
65+
<div className="mb-4">
66+
<div className="mb-2 flex items-center gap-2">
67+
<div>
68+
<label className="mb-1 block text-sm font-semibold">Owner</label>
69+
<div className="flex items-center gap-2 rounded-md border border-[var(--borderColor-default)] bg-[var(--bgColor-muted)] px-3 py-1.5 text-sm">
70+
<Avatar src={user.avatar_url} size={16} alt={user.login} />
71+
{user.login}
72+
</div>
73+
</div>
74+
<span className="mt-6 text-xl text-[var(--fgColor-muted)]">/</span>
75+
<div className="flex-1">
76+
<label className="mb-1 block text-sm font-semibold">
77+
Repository name <span className="text-red-500">*</span>
78+
</label>
79+
<input
80+
type="text"
81+
value={repoName}
82+
onChange={(e) => setRepoName(e.target.value)}
83+
className={`w-full rounded-md border px-3 py-1.5 text-sm outline-none ${
84+
repoName && !isValid
85+
? "border-red-500 bg-[var(--bgColor-default)]"
86+
: "border-[var(--borderColor-default)] bg-[var(--bgColor-default)]"
87+
} text-[var(--fgColor-default)] focus:border-[var(--fgColor-accent)]`}
88+
placeholder="my-awesome-repo"
89+
/>
90+
</div>
91+
</div>
92+
{repoName && isValid && (
93+
<p className="text-xs text-green-500">
94+
{user.login}/{repoName} is available.
95+
</p>
96+
)}
97+
</div>
98+
99+
{/* Description */}
100+
<div className="mb-6">
101+
<label className="mb-1 block text-sm font-semibold">
102+
Description <span className="text-xs font-normal text-[var(--fgColor-muted)]">(optional)</span>
103+
</label>
104+
<input
105+
type="text"
106+
value={description}
107+
onChange={(e) => setDescription(e.target.value)}
108+
className="w-full rounded-md border border-[var(--borderColor-default)] bg-[var(--bgColor-default)] px-3 py-1.5 text-sm text-[var(--fgColor-default)] outline-none focus:border-[var(--fgColor-accent)]"
109+
/>
110+
</div>
111+
112+
<hr className="mb-6 border-[var(--borderColor-muted)]" />
113+
114+
{/* Visibility */}
115+
<div className="mb-6 flex flex-col gap-3">
116+
<label className="flex cursor-pointer items-start gap-3">
117+
<input
118+
type="radio"
119+
name="visibility"
120+
checked={visibility === "public"}
121+
onChange={() => setVisibility("public")}
122+
className="mt-1"
123+
/>
124+
<div>
125+
<div className="flex items-center gap-1.5 text-sm font-semibold">
126+
<RepoIcon size={16} />
127+
Public
128+
</div>
129+
<p className="text-xs text-[var(--fgColor-muted)]">
130+
Anyone on the internet can see this repository.
131+
</p>
132+
</div>
133+
</label>
134+
<label className="flex cursor-pointer items-start gap-3">
135+
<input
136+
type="radio"
137+
name="visibility"
138+
checked={visibility === "private"}
139+
onChange={() => setVisibility("private")}
140+
className="mt-1"
141+
/>
142+
<div>
143+
<div className="flex items-center gap-1.5 text-sm font-semibold">
144+
<LockIcon size={16} />
145+
Private
146+
</div>
147+
<p className="text-xs text-[var(--fgColor-muted)]">
148+
You choose who can see and commit to this repository.
149+
</p>
150+
</div>
151+
</label>
152+
</div>
153+
154+
<hr className="mb-6 border-[var(--borderColor-muted)]" />
155+
156+
{/* Initialize */}
157+
<div className="mb-6">
158+
<h3 className="mb-2 text-sm font-semibold">
159+
Initialize this repository with:
160+
</h3>
161+
<label className="flex cursor-pointer items-center gap-2 text-sm">
162+
<input
163+
type="checkbox"
164+
checked={addReadme}
165+
onChange={(e) => setAddReadme(e.target.checked)}
166+
/>
167+
Add a README file
168+
</label>
169+
<p className="ml-6 text-xs text-[var(--fgColor-muted)]">
170+
This is where you can write a long description for your project.
171+
</p>
172+
</div>
173+
174+
<hr className="mb-6 border-[var(--borderColor-muted)]" />
175+
176+
{error && (
177+
<div className="mb-4 rounded-md border border-red-500 bg-red-500/10 px-4 py-2 text-sm text-red-500">
178+
{error}
179+
</div>
180+
)}
181+
182+
<Button
183+
type="submit"
184+
variant="primary"
185+
size="large"
186+
loading={creating}
187+
disabled={!isValid}
188+
block
189+
>
190+
Create repository
191+
</Button>
192+
</form>
193+
</div>
194+
);
195+
}

src/app/page.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ export default function Home() {
8383
Top Repositories
8484
</h2>
8585
<a
86-
href="https://github.com/new"
87-
target="_blank"
88-
rel="noopener noreferrer"
86+
href="/new"
8987
className="rounded-md bg-green-700 px-2.5 py-1 text-xs font-semibold text-white hover:bg-green-600"
9088
>
9189
New

src/components/global-header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ export function GlobalHeader() {
9696
<>
9797
<div className="fixed inset-0 z-10" onClick={() => setShowCreate(false)} />
9898
<div className="absolute right-0 top-full z-20 mt-1 w-48 rounded-lg border border-[var(--borderColor-default)] bg-[var(--bgColor-default)] py-1 shadow-lg">
99-
<a href="https://github.com/new" target="_blank" rel="noopener noreferrer" className="block px-4 py-2 text-sm hover:bg-[var(--bgColor-muted)]">
99+
<button onClick={() => { setShowCreate(false); router.push("/new"); }} className="block w-full px-4 py-2 text-left text-sm hover:bg-[var(--bgColor-muted)]">
100100
New repository
101-
</a>
101+
</button>
102102
<a href="https://gist.github.com" target="_blank" rel="noopener noreferrer" className="block px-4 py-2 text-sm hover:bg-[var(--bgColor-muted)]">
103103
New gist
104104
</a>

0 commit comments

Comments
 (0)