Commit920812bunknown_key
feat(dashboard): review queue + my open PRs panels
feat(dashboard): review queue + my open PRs panels Adds two side-by-side panels at the bottom of the command center: - "Needs your review": open non-draft PRs in the user's repos authored by others, ordered newest first, up to 6 entries. - "Your open PRs": open PRs the user authored across all repos they have access to, ordered by most recently updated. Both panels link directly to the PR detail page. Only rendered when at least one panel has entries, so the dashboard stays clean for new users. Data loaded in a single Promise.all so it adds one parallel roundtrip. https://claude.ai/code/session_01ACsT2Pc8GRoZwZRF8SK68Y
1 file changed+130−1920812b9d1f126cba048101c7f89f306ffdd079b
1 changed file+130−1
Modifiedsrc/routes/dashboard.tsx+130−1View fileUnifiedSplit
@@ -15,7 +15,7 @@
1515 */
1616
1717import { Hono } from "hono";
18import { eq, desc, and } from "drizzle-orm";
18import { eq, desc, and, inArray, ne, sql } from "drizzle-orm";
1919import { getCookie, setCookie } from "hono/cookie";
2020import { db } from "../db";
2121import {
@@ -169,6 +169,75 @@ dashboard.get("/dashboard", requireAuth, async (c) => {
169169 // DB not required for dashboard
170170 }
171171
172 // Review queue — open non-draft PRs in user's repos, authored by others
173 let reviewQueuePrs: Array<{
174 prNumber: number;
175 prTitle: string;
176 repoName: string;
177 createdAt: Date;
178 }> = [];
179 // Open PRs the user authored that are still open (anywhere on the platform)
180 let myOpenPrs: Array<{
181 prNumber: number;
182 prTitle: string;
183 repoName: string;
184 ownerUsername: string;
185 createdAt: Date;
186 }> = [];
187 try {
188 const repoIds = repos.map((r) => r.id);
189 const prQueries: Promise<any>[] = [];
190 if (repoIds.length > 0) {
191 prQueries.push(
192 db
193 .select({
194 prNumber: pullRequests.number,
195 prTitle: pullRequests.title,
196 repoName: repositories.name,
197 createdAt: pullRequests.createdAt,
198 })
199 .from(pullRequests)
200 .innerJoin(repositories, eq(pullRequests.repositoryId, repositories.id))
201 .where(
202 and(
203 inArray(pullRequests.repositoryId, repoIds),
204 eq(pullRequests.state, "open"),
205 ne(pullRequests.authorId, user.id),
206 eq(pullRequests.isDraft, false),
207 )
208 )
209 .orderBy(desc(pullRequests.createdAt))
210 .limit(6)
211 );
212 } else {
213 prQueries.push(Promise.resolve([]));
214 }
215 prQueries.push(
216 db
217 .select({
218 prNumber: pullRequests.number,
219 prTitle: pullRequests.title,
220 repoName: repositories.name,
221 ownerUsername: users.username,
222 createdAt: pullRequests.createdAt,
223 })
224 .from(pullRequests)
225 .innerJoin(repositories, eq(pullRequests.repositoryId, repositories.id))
226 .innerJoin(users, eq(repositories.ownerId, users.id))
227 .where(
228 and(
229 eq(pullRequests.authorId, user.id),
230 eq(pullRequests.state, "open"),
231 )
232 )
233 .orderBy(desc(pullRequests.updatedAt))
234 .limit(6)
235 );
236 const [queueRows, myPrRows] = await Promise.all(prQueries);
237 reviewQueuePrs = queueRows;
238 myOpenPrs = myPrRows;
239 } catch { /* non-blocking */ }
240
172241 const gradeColor = (grade: string) =>
173242 grade === "A+" || grade === "A"
174243 ? "var(--green)"
@@ -597,6 +666,66 @@ git push -u gluecron main</code></pre>
597666 {/* ─── Live Activity (SSE) ─── */}
598667 <LiveFeed topic={`user:${user.id}`} title="Live activity" />
599668
669 {/* ─── Review queue + My open PRs ─── */}
670 {(reviewQueuePrs.length > 0 || myOpenPrs.length > 0) && (
671 <>
672 <style dangerouslySetInnerHTML={{ __html: `
673 .dash-rq { border:1px solid var(--border); border-radius:12px; overflow:hidden; }
674 .dash-rq-head { display:flex; align-items:center; justify-content:space-between; padding:11px 16px; background:var(--bg-elevated); border-bottom:1px solid var(--border); font-size:13px; font-weight:600; }
675 .dash-rq-head-count { font-size:11px; font-weight:700; padding:1px 7px; border-radius:9999px; background:var(--bg-tertiary); color:var(--text-muted); }
676 .dash-rq-row { display:flex; align-items:center; gap:10px; padding:9px 16px; border-bottom:1px solid var(--border); font-size:13px; text-decoration:none; color:inherit; }
677 .dash-rq-row:last-child { border-bottom:none; }
678 .dash-rq-row:hover { background:var(--bg-hover); }
679 .dash-rq-repo { font-size:11px; color:var(--text-muted); flex:0 0 auto; }
680 .dash-rq-title { flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
681 .dash-rq-age { font-size:11px; color:var(--text-muted); flex:0 0 auto; }
682 .dash-rq-empty { padding:24px; text-align:center; color:var(--text-muted); font-size:13px; }
683 ` }} />
684 <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--space-4);margin-top:var(--space-8)">
685 {reviewQueuePrs.length > 0 && (
686 <div class="dash-rq">
687 <div class="dash-rq-head">
688 <span>{"⏳"} Needs your review</span>
689 <span class="dash-rq-head-count">{reviewQueuePrs.length}</span>
690 </div>
691 {reviewQueuePrs.map((pr) => (
692 <a
693 href={`/${user.username}/${pr.repoName}/pulls/${pr.prNumber}`}
694 class="dash-rq-row"
695 >
696 <span class="dash-rq-repo">{pr.repoName}</span>
697 <span class="dash-rq-title" title={pr.prTitle}>{pr.prTitle}</span>
698 <span class="dash-rq-age">
699 {formatRelative(pr.createdAt)}
700 </span>
701 </a>
702 ))}
703 </div>
704 )}
705 {myOpenPrs.length > 0 && (
706 <div class="dash-rq">
707 <div class="dash-rq-head">
708 <span>{"○"} Your open PRs</span>
709 <span class="dash-rq-head-count">{myOpenPrs.length}</span>
710 </div>
711 {myOpenPrs.map((pr) => (
712 <a
713 href={`/${pr.ownerUsername}/${pr.repoName}/pulls/${pr.prNumber}`}
714 class="dash-rq-row"
715 >
716 <span class="dash-rq-repo">{pr.repoName}</span>
717 <span class="dash-rq-title" title={pr.prTitle}>{pr.prTitle}</span>
718 <span class="dash-rq-age">
719 {formatRelative(pr.createdAt)}
720 </span>
721 </a>
722 ))}
723 </div>
724 )}
725 </div>
726 </>
727 )}
728
600729 {/* ─── Quick Links ─── */}
601730 <div style="margin-top: var(--space-8); display: flex; gap: var(--space-4); flex-wrap: wrap">
602731 <a href="/explore" class="btn">Browse public repos</a>
603732