--- import BaseLayout from "@/layouts/BaseLayout.astro"; import { getDatabase, schema } from "@/db"; import { eq, desc, and, gte, sql } from "drizzle-orm"; import { BarChart3, TrendingUp, Clock, GitPullRequest, Users, Calendar, Download, ChevronDown } from "lucide-react"; const user = Astro.locals.user; if (!user) { return Astro.redirect("/login"); } const db = getDatabase(); const period = Astro.url.searchParams.get("period") || "4w"; const view = Astro.url.searchParams.get("view") || "personal"; // Calculate date range const now = new Date(); let startDate: Date; switch (period) { case "1w": startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case "4w": startDate = new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000); break; case "3m": startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); break; case "1y": startDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); break; default: startDate = new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000); } // Fetch user metrics let myMetrics = { prsOpened: 0, prsMerged: 0, reviewsGiven: 0, avgTimeToMerge: 0, avgReviewTime: 0, }; let weeklyTrends: { week: string; opened: number; merged: number; reviewed: number }[] = []; let topContributors: { username: string; avatarUrl?: string; prs: number; reviews: number }[] = []; let reviewStats = { approvals: 0, changesRequested: 0, comments: 0 }; try { // Get user's PRs const myPrs = await db.query.pullRequests.findMany({ where: and( eq(schema.pullRequests.authorId, user.id), gte(schema.pullRequests.createdAt, startDate) ), }); myMetrics.prsOpened = myPrs.length; myMetrics.prsMerged = myPrs.filter(pr => pr.mergedAt).length; // Calculate avg time to merge (in hours) const mergedPrs = myPrs.filter(pr => pr.mergedAt && pr.createdAt); if (mergedPrs.length > 0) { const totalTime = mergedPrs.reduce((acc, pr) => { const created = new Date(pr.createdAt).getTime(); const merged = new Date(pr.mergedAt!).getTime(); return acc + (merged - created); }, 0); myMetrics.avgTimeToMerge = Math.round(totalTime / mergedPrs.length / (1000 * 60 * 60)); } // Get reviews given const myReviews = await db.query.pullRequestReviews.findMany({ where: and( eq(schema.pullRequestReviews.reviewerId, user.id), gte(schema.pullRequestReviews.createdAt, startDate) ), }); myMetrics.reviewsGiven = myReviews.length; reviewStats.approvals = myReviews.filter(r => r.state === "approved").length; reviewStats.changesRequested = myReviews.filter(r => r.state === "changes_requested").length; reviewStats.comments = myReviews.filter(r => r.state === "commented").length; // Generate weekly trends (last 4 weeks) for (let i = 3; i >= 0; i--) { const weekStart = new Date(now.getTime() - (i + 1) * 7 * 24 * 60 * 60 * 1000); const weekEnd = new Date(now.getTime() - i * 7 * 24 * 60 * 60 * 1000); const weekLabel = weekStart.toLocaleDateString("en-US", { month: "short", day: "numeric" }); const weekPrs = myPrs.filter(pr => { const created = new Date(pr.createdAt); return created >= weekStart && created < weekEnd; }); const weekMerged = myPrs.filter(pr => { if (!pr.mergedAt) return false; const merged = new Date(pr.mergedAt); return merged >= weekStart && merged < weekEnd; }); const weekReviews = myReviews.filter(r => { const created = new Date(r.createdAt); return created >= weekStart && created < weekEnd; }); weeklyTrends.push({ week: weekLabel, opened: weekPrs.length, merged: weekMerged.length, reviewed: weekReviews.length, }); } if (view === "team") { const allPrs = await db.query.pullRequests.findMany({ where: gte(schema.pullRequests.createdAt, startDate), with: { author: true }, limit: 100, }); const contributorMap = new Map(); for (const pr of allPrs) { if (pr.author) { const existing = contributorMap.get(pr.author.id) || { username: pr.author.username, avatarUrl: pr.author.avatarUrl || undefined, prs: 0, reviews: 0 }; existing.prs++; contributorMap.set(pr.author.id, existing); } } topContributors = Array.from(contributorMap.values()) .sort((a, b) => b.prs - a.prs) .slice(0, 10); } } catch (e) { console.error("Failed to fetch metrics:", e); } function formatDuration(hours: number): string { if (hours < 1) return "< 1h"; if (hours < 24) return `${hours}h`; return `${Math.round(hours / 24)}d`; } ---

Engineering Insights

Track productivity, review velocity, and development cadence

PRs Opened
{myMetrics.prsOpened}
PRs Merged
{myMetrics.prsMerged}
{myMetrics.prsOpened > 0 ? Math.round((myMetrics.prsMerged / myMetrics.prsOpened) * 100) : 0}% merge rate
Reviews Given
{myMetrics.reviewsGiven}
Avg Time to Merge
{formatDuration(myMetrics.avgTimeToMerge)}

Weekly Velocity

{weeklyTrends.map((week) => { const maxValue = Math.max(...weeklyTrends.flatMap(w => [w.opened, w.merged, w.reviewed]), 1); return (
{week.week}
{week.opened} opened {week.merged} merged {week.reviewed} reviewed
); })}

Review Breakdown

{reviewStats.approvals}
Approvals
{reviewStats.changesRequested}
Changes
{reviewStats.comments}
Comments