Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

pinned-repos.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

pinned-repos.tsxBlame142 lines · 1 contributor
8ec29ffClaude1/**
2 * Block J13 — Pinned repos management UI.
3 *
4 * GET /settings/pins — pick up to 6 repos to feature on your profile
5 * POST /settings/pins — save the selection (replaces prior set)
6 */
7
8import { Hono } from "hono";
9import { Layout } from "../views/layout";
10import { softAuth, requireAuth } from "../middleware/auth";
11import type { AuthEnv } from "../middleware/auth";
12import {
13 MAX_PINS,
14 listPinCandidates,
15 listPinnedForUser,
16 setPinsForUser,
17} from "../lib/pinned-repos";
18
19const pins = new Hono<AuthEnv>();
20
21pins.get("/settings/pins", softAuth, requireAuth, async (c) => {
22 const user = c.get("user")!;
23 const [pinned, candidates] = await Promise.all([
24 listPinnedForUser(user.id),
25 listPinCandidates(user.id),
26 ]);
27 const pinnedIds = new Set(pinned.map((p) => p.repositoryId));
28 const error = c.req.query("error");
29 const saved = c.req.query("saved") === "1";
30
31 return c.html(
32 <Layout title="Pinned repositories" user={user}>
33 <h2>Pinned repositories</h2>
34 <p style="color: var(--text-muted); max-width: 620px">
35 Choose up to {MAX_PINS} repositories to feature at the top of your
36 profile. They appear in the order you select them.
37 </p>
38
39 {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
40 {saved && (
41 <div
42 style="padding: 10px 12px; background: rgba(63, 185, 80, 0.1); border: 1px solid var(--green); color: var(--green); border-radius: var(--radius); margin: 12px 0"
43 >
44 Saved.
45 </div>
46 )}
47
48 <form method="POST" action="/settings/pins" style="margin-top: 16px">
49 {candidates.length === 0 ? (
50 <div class="empty-state">
51 <p>You don't own any repositories yet.</p>
52 </div>
53 ) : (
54 <ul
55 style="list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 8px"
56 data-testid="pin-candidates"
57 >
58 {candidates.map((c) => (
59 <li
60 style="border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--bg-secondary)"
61 >
62 <label
63 style="display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px"
64 >
65 <input
66 type="checkbox"
67 name="repoId"
68 value={c.id}
69 checked={pinnedIds.has(c.id)}
70 />
71 <span style="flex: 1">
72 <strong>{c.name}</strong>
73 {c.isPrivate && (
74 <span
75 style="margin-left: 6px; font-size: 10px; padding: 1px 6px; border-radius: 10px; background: rgba(139, 148, 158, 0.15); color: var(--text-muted); text-transform: uppercase"
76 >
77 Private
78 </span>
79 )}
80 </span>
81 </label>
82 </li>
83 ))}
84 </ul>
85 )}
86
87 <div style="margin-top: 16px; display: flex; gap: 8px; align-items: center">
88 <button type="submit" class="btn btn-primary">
89 Save pinned repositories
90 </button>
91 <span style="color: var(--text-muted); font-size: 12px">
92 The first {MAX_PINS} you tick are kept.
93 </span>
94 </div>
95 </form>
96
97 {pinned.length > 0 && (
98 <div style="margin-top: 28px">
99 <h3>Current pins (preview)</h3>
100 <ul
101 style="list-style: none; padding: 0; margin: 8px 0 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 8px"
102 >
103 {pinned.map((p) => (
104 <li
105 style="border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px"
106 >
107 <div>
108 <a href={`/${p.ownerUsername}/${p.name}`}>
109 <strong>{p.ownerUsername}/{p.name}</strong>
110 </a>
111 </div>
112 {p.description && (
113 <div style="color: var(--text-muted); font-size: 12px; margin-top: 4px">
114 {p.description}
115 </div>
116 )}
117 <div style="color: var(--text-muted); font-size: 11px; margin-top: 6px">
118 {p.starCount} {"\u2605"} · {p.forkCount} forks
119 </div>
120 </li>
121 ))}
122 </ul>
123 </div>
124 )}
125 </Layout>
126 );
127});
128
129pins.post("/settings/pins", softAuth, requireAuth, async (c) => {
130 const user = c.get("user")!;
131 const form = await c.req.parseBody({ all: true });
132 const raw = form.repoId;
133 const ids: string[] = Array.isArray(raw)
134 ? raw.map((x) => String(x))
135 : raw != null
136 ? [String(raw)]
137 : [];
138 await setPinsForUser(user.id, ids);
139 return c.redirect("/settings/pins?saved=1");
140});
141
142export default pins;