Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

community.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.

community.tsxBlame195 lines · 1 contributor
f9e7c01Claude1/**
2 * Block J12 — Community profile / health scorecard route.
3 *
4 * GET /:owner/:repo/community
5 *
6 * Renders GitHub-parity "Community standards" checklist: README, LICENSE,
7 * CODE_OF_CONDUCT, CONTRIBUTING, issue + PR templates, description, topics.
8 * Scored in percentage and broken down by required vs recommended.
9 *
10 * Never 500s — falls back to a zero-score report when git or DB reads fail.
11 */
12
13import { Hono } from "hono";
14import { and, eq } from "drizzle-orm";
15import { db } from "../db";
16import { repositories, repoTopics, users } from "../db/schema";
17import { Layout } from "../views/layout";
18import { RepoHeader, RepoNav } from "../views/components";
19import { softAuth } from "../middleware/auth";
20import type { AuthEnv } from "../middleware/auth";
21import { computeHealth, type HealthReport } from "../lib/community";
22import { getUnreadCount } from "../lib/unread";
23
24const community = new Hono<AuthEnv>();
25
26community.get("/:owner/:repo/community", softAuth, async (c) => {
27 const user = c.get("user");
28 const { owner: ownerName, repo: repoName } = c.req.param();
29
30 let repoRow: {
31 id: string;
32 description: string | null;
33 starCount: number;
34 forkCount: number;
35 } | null = null;
36 try {
37 const rows = await db
38 .select({
39 id: repositories.id,
40 description: repositories.description,
41 starCount: repositories.starCount,
42 forkCount: repositories.forkCount,
43 })
44 .from(repositories)
45 .innerJoin(users, eq(repositories.ownerId, users.id))
46 .where(
47 and(
48 eq(users.username, ownerName),
49 eq(repositories.name, repoName)
50 )
51 )
52 .limit(1);
53 repoRow = rows[0] || null;
54 } catch {
55 repoRow = null;
56 }
57 if (!repoRow) return c.notFound();
58
59 let topics: string[] = [];
60 try {
61 const rows = await db
62 .select({ topic: repoTopics.topic })
63 .from(repoTopics)
64 .where(eq(repoTopics.repositoryId, repoRow.id));
65 topics = rows.map((r) => r.topic);
66 } catch {
67 topics = [];
68 }
69
70 const report: HealthReport = await computeHealth({
71 owner: ownerName,
72 repo: repoName,
73 description: repoRow.description,
74 topics,
75 });
76
77 const unread = user ? await getUnreadCount(user.id) : 0;
78
79 const barColor =
80 report.score >= 80
81 ? "var(--green)"
82 : report.score >= 50
83 ? "var(--yellow)"
84 : "var(--red)";
85
86 return c.html(
87 <Layout
88 title={`Community standards — ${ownerName}/${repoName}`}
89 user={user}
90 notificationCount={unread}
91 >
92 <RepoHeader
93 owner={ownerName}
94 repo={repoName}
95 starCount={repoRow.starCount}
96 forkCount={repoRow.forkCount}
97 currentUser={user?.username || null}
98 />
99 <RepoNav owner={ownerName} repo={repoName} active="insights" />
100 <h2>Community standards</h2>
101 <p style="color: var(--text-muted); max-width: 640px">
102 Healthy projects set expectations and onboard new contributors. Here's
103 how this repo scores on GlueCron's community profile checklist.
104 </p>
105
106 <div
107 style="margin: 16px 0 24px; padding: 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius)"
108 data-testid="community-score"
109 >
110 <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px">
111 <strong style="font-size: 18px">{report.score}%</strong>
112 <span style="color: var(--text-muted); font-size: 13px">
113 {report.passed} of {report.total} items present · {report.requiredPassed}/{report.requiredTotal} required
114 </span>
115 </div>
116 <div style="height: 8px; background: var(--bg); border-radius: 4px; overflow: hidden">
117 <div
118 style={`width: ${report.score}%; height: 100%; background: ${barColor}; transition: width 0.3s`}
119 />
120 </div>
121 <div style="margin-top: 8px; font-size: 12px; color: var(--text-muted)">
122 {report.meetsRequired
123 ? "All required items present — nice work."
124 : "Add the required items to reach the minimum community profile."}
125 </div>
126 </div>
127
128 <ul style="list-style: none; padding: 0; margin: 0">
129 {report.items.map((item) => (
130 <li
131 style="display: flex; align-items: flex-start; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border)"
132 data-testid={`community-item-${item.key}`}
133 >
134 <span
135 style={`font-size: 20px; color: ${item.present ? "var(--green)" : item.required ? "var(--red)" : "var(--text-muted)"}; line-height: 1`}
136 aria-label={item.present ? "present" : "missing"}
137 >
138 {item.present ? "\u2713" : item.required ? "\u2717" : "\u25CB"}
139 </span>
140 <div style="flex: 1">
141 <div style="display: flex; align-items: center; gap: 8px">
142 <strong>{item.label}</strong>
143 {item.required && (
144 <span
145 style="font-size: 10px; padding: 1px 6px; border-radius: 10px; background: rgba(248, 81, 73, 0.1); color: var(--red); text-transform: uppercase; letter-spacing: 0.5px"
146 >
147 Required
148 </span>
149 )}
150 </div>
151 <div style="color: var(--text-muted); font-size: 13px; margin-top: 2px">
152 {item.description}
153 </div>
154 {!item.present && item.suggestedPath && (
155 <div style="margin-top: 6px">
156 <a
157 href={`/${ownerName}/${repoName}/new/main?path=${encodeURIComponent(item.suggestedPath)}`}
158 class="btn"
159 style="font-size: 12px; padding: 4px 10px"
160 >
161 Add {item.suggestedPath}
162 </a>
163 </div>
164 )}
165 {!item.present && item.key === "description" && (
166 <div style="margin-top: 6px">
167 <a
168 href={`/${ownerName}/${repoName}/settings`}
169 class="btn"
170 style="font-size: 12px; padding: 4px 10px"
171 >
172 Edit description
173 </a>
174 </div>
175 )}
176 {!item.present && item.key === "topics" && (
177 <div style="margin-top: 6px">
178 <a
179 href={`/${ownerName}/${repoName}/settings`}
180 class="btn"
181 style="font-size: 12px; padding: 4px 10px"
182 >
183 Add topics
184 </a>
185 </div>
186 )}
187 </div>
188 </li>
189 ))}
190 </ul>
191 </Layout>
192 );
193});
194
195export default community;