diff --git a/src/app/[interval]/[[...date]]/components/IssuesListModalContent.tsx b/src/app/[interval]/[[...date]]/components/IssuesListModalContent.tsx index 0b448a351..937a851a9 100644 --- a/src/app/[interval]/[[...date]]/components/IssuesListModalContent.tsx +++ b/src/app/[interval]/[[...date]]/components/IssuesListModalContent.tsx @@ -22,6 +22,7 @@ export default function IssuesListModalContent({ author={issue.author} number={issue.number} href={`https://github.com/${issue.repository}/issues/${issue.number}`} + allowTitleWrap={true} icon={ issue.state === "closed" || issue.closedAt ? ( diff --git a/src/app/[interval]/[[...date]]/components/LeftSidebar.tsx b/src/app/[interval]/[[...date]]/components/LeftSidebar.tsx new file mode 100644 index 000000000..745dd7813 --- /dev/null +++ b/src/app/[interval]/[[...date]]/components/LeftSidebar.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import type { IntervalMetrics } from "@/app/[interval]/[[...date]]/queries"; +import { + Users, + Star, + TrendingUp, + CircleDot, + GitMerge, + CheckCircle, +} from "lucide-react"; +import { CounterWithIcon } from "@/components/counter-with-icon"; +import Link from "next/link"; +import PullRequestsListModalContent from "./PullRequestsListModalContent"; +import IssuesListModalContent from "./IssuesListModalContent"; +import ContributorsListModalContent from "./ContributorsListModalContent"; +import { formatTimeframeTitle } from "@/lib/date-utils"; + +interface LeftSidebarProps { + metrics: IntervalMetrics; +} + +interface Contributor { + username: string; + totalScore: number; + summary?: string | null; +} + +export function LeftSidebar({ metrics }: LeftSidebarProps) { + const timeframeTitle = formatTimeframeTitle( + metrics.interval.intervalStart, + metrics.interval.intervalType, + ); + + return ( + <> + {/* Top Contributors Card */} + + + + + + + Contributors ({metrics.activeContributors}) + + + + +
+ {metrics.topContributors + .slice(0, 10) + .map((contributor: Contributor, index) => ( + e.stopPropagation()} + > +
+ + + + {contributor.username[0].toUpperCase()} + + +
+
+

+ {contributor.username} +

+ + #{index + 1} + +
+
+ + + {contributor.totalScore.toFixed(1)} pts + +
+ {contributor.summary && ( +

+ {contributor.summary.replace( + `${contributor.username}: `, + "", + )} +

+ )} +
+
+ + ))} + {metrics.topContributors.length > 10 && ( +
+ +{metrics.topContributors.length - 10} more contributors +
+ )} +
+
+
+
+
+ + + Contributors + + + +
+ + {/* Pull Requests Card */} + + + + + +
+ + + + + + + Pull Requests ({metrics.pullRequests.new}) +
+
+
+ + + {metrics.pullRequests.new} + +
+
+ + + {metrics.pullRequests.merged} + +
+
+
+
+ + +
+ {metrics.topPullRequests.slice(0, 10).map((pr, index) => ( +
+
+

+ #{pr.number} {pr.title} +

+
+ ))} + {metrics.topPullRequests.length > 10 && ( +
+ +{metrics.topPullRequests.length - 10} more pull requests +
+ )} +
+
+
+
+
+ + + Pull Requests + + + +
+ + {/* Issues Card */} + + + + + +
+ + + + + + Issues ({metrics.issues.new}) +
+
+
+ + {metrics.issues.new} +
+
+ + {metrics.issues.closed} +
+
+
+
+ + +
+ {metrics.topIssues.slice(0, 10).map((issue, index) => ( +
+
+

+ #{issue.number} {issue.title} +

+
+ ))} + {metrics.topIssues.length > 10 && ( +
+ +{metrics.topIssues.length - 10} more issues +
+ )} +
+
+
+
+
+ + + Issues + + + +
+ + ); +} diff --git a/src/app/[interval]/[[...date]]/components/MainContent.tsx b/src/app/[interval]/[[...date]]/components/MainContent.tsx new file mode 100644 index 000000000..6d7cddf84 --- /dev/null +++ b/src/app/[interval]/[[...date]]/components/MainContent.tsx @@ -0,0 +1,144 @@ +import { StatCard } from "@/components/stat-card"; +import { CounterWithIcon } from "@/components/counter-with-icon"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import type { IntervalMetrics } from "@/app/[interval]/[[...date]]/queries"; +import { + Users, + GitPullRequest, + MessageCircleWarning, + CircleDot, + GitMerge, + CheckCircle, +} from "lucide-react"; +import { formatTimeframeTitle } from "@/lib/date-utils"; +import { SummaryContent } from "./SummaryContent"; +import ContributorsListModalContent from "./ContributorsListModalContent"; +import PullRequestsListModalContent from "./PullRequestsListModalContent"; +import IssuesListModalContent from "./IssuesListModalContent"; + +interface MainContentProps { + metrics: IntervalMetrics; + summaryContent: string | null; +} + +interface Contributor { + username: string; + totalScore: number; +} + +export function MainContent({ metrics, summaryContent }: MainContentProps) { + const timeframeTitle = formatTimeframeTitle( + metrics.interval.intervalStart, + metrics.interval.intervalType, + ); + + return ( +
+ {/* Main Statistics Cards */} +
+ + } + > +
+
+ {metrics.activeContributors} +
+
+ {metrics.topContributors + .slice(0, 3) + .map((contributor: Contributor) => ( + + + + {contributor.username[0].toUpperCase()} + + + ))} + {metrics.topContributors.length > 3 && ( +
+ +{metrics.topContributors.length - 3} +
+ )} +
+
+
+ + + } + > +
+
+ {metrics.pullRequests.total} +
+
+ + +
+
+
+ + } + > +
+
{metrics.issues.total}
+
+ + +
+
+
+
+ + +
+ ); +} diff --git a/src/app/[interval]/[[...date]]/components/PullRequestsListModalContent.tsx b/src/app/[interval]/[[...date]]/components/PullRequestsListModalContent.tsx index 9f3902b8a..76dd51e42 100644 --- a/src/app/[interval]/[[...date]]/components/PullRequestsListModalContent.tsx +++ b/src/app/[interval]/[[...date]]/components/PullRequestsListModalContent.tsx @@ -22,6 +22,7 @@ export default function PullRequestsListModalContent({ className="px-4" number={pr.number} href={`https://github.com/${pr.repository}/pull/${pr.number}`} + allowTitleWrap={true} icon={ pr.mergedAt ? ( diff --git a/src/app/[interval]/[[...date]]/components/RightSidebar.tsx b/src/app/[interval]/[[...date]]/components/RightSidebar.tsx new file mode 100644 index 000000000..102562f3d --- /dev/null +++ b/src/app/[interval]/[[...date]]/components/RightSidebar.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Progress } from "@/components/ui/progress"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import type { IntervalMetrics } from "@/app/[interval]/[[...date]]/queries"; +import { GitCommitVertical, FileCode, Target } from "lucide-react"; + +interface RightSidebarProps { + metrics: IntervalMetrics; +} + +export function RightSidebar({ metrics }: RightSidebarProps) { + const { codeChanges, focusAreas } = metrics; + + // Calculate percentage for the commit activity bar + const totalChanges = codeChanges.additions + codeChanges.deletions; + const additionPercentage = + totalChanges > 0 ? (codeChanges.additions / totalChanges) * 100 : 50; + + return ( + <> + + + + + Code Activity + + + + {/* Commit Volume Visualization */} +
+
+ Volume + + {codeChanges.commitCount} commits + +
+
+ +
+
+
+ +{codeChanges.additions.toLocaleString()} + -{codeChanges.deletions.toLocaleString()} +
+
+ + + +
+
+ + + {codeChanges.files} files changed + +
+
+
+ + + + Files Changed ({metrics.topFilesChanged?.length || 0} of{" "} + {codeChanges.files}) + +

+ Sorted by most changes (additions + deletions) +

+
+ {metrics.topFilesChanged && metrics.topFilesChanged.length > 0 ? ( + +
+ {metrics.topFilesChanged.map((file, index) => ( +
+ + {file.path} + +
+ + +{file.additions} + + + -{file.deletions} + +
+
+ ))} +
+
+ ) : ( +

+ No file changes to display. +

+ )} +
+
+ + + + {/* Focus Areas */} + {focusAreas && focusAreas.length > 0 && ( + + + + + Focus Areas + + + +
+ {focusAreas.slice(0, 10).map((focusArea, index) => ( + + {focusArea.area} + + ))} + {focusAreas.length > 10 && ( + + +{focusAreas.length - 10} more + + )} +
+
+
+ )} + + ); +} diff --git a/src/app/[interval]/[[...date]]/components/SummaryContent.skeleton.tsx b/src/app/[interval]/[[...date]]/components/SummaryContent.skeleton.tsx index 85d796959..9b421697f 100644 --- a/src/app/[interval]/[[...date]]/components/SummaryContent.skeleton.tsx +++ b/src/app/[interval]/[[...date]]/components/SummaryContent.skeleton.tsx @@ -5,7 +5,7 @@ export function SummaryContentSkeleton({ className }: { className?: string }) { return (
diff --git a/src/app/[interval]/[[...date]]/components/SummaryContent.tsx b/src/app/[interval]/[[...date]]/components/SummaryContent.tsx index aa87d569c..3673ecc25 100644 --- a/src/app/[interval]/[[...date]]/components/SummaryContent.tsx +++ b/src/app/[interval]/[[...date]]/components/SummaryContent.tsx @@ -29,7 +29,7 @@ const remarkRemoveFirstH1 = () => { // Custom H2 component to apply primary color const CustomH2 = (props: HTMLProps) => { - return

; + return

; }; interface SummaryContentProps { @@ -48,7 +48,7 @@ export function SummaryContent({ return (
diff --git a/src/app/[interval]/[[...date]]/page.tsx b/src/app/[interval]/[[...date]]/page.tsx index ae51a81a4..34ab14f67 100644 --- a/src/app/[interval]/[[...date]]/page.tsx +++ b/src/app/[interval]/[[...date]]/page.tsx @@ -16,11 +16,11 @@ import { import { UTCDate } from "@date-fns/utc"; import { addDays } from "date-fns"; import { DateNavigation } from "./components/DateNavigation"; -import { SummaryContent } from "./components/SummaryContent"; -import { StatCardsDisplay } from "./components/StatCardsDisplay"; -import { CodeChangesDisplay } from "./components/CodeChangesDisplay"; import { LlmCopyButton } from "@/components/ui/llm-copy-button"; import { IntervalSelector } from "./components/IntervalSelector"; +import { LeftSidebar } from "./components/LeftSidebar"; +import { MainContent } from "./components/MainContent"; +import { RightSidebar } from "./components/RightSidebar"; interface PageProps { params: Promise<{ @@ -124,27 +124,50 @@ export default async function IntervalSummaryPage({ params }: PageProps) { ); return ( -
-
-
- - - +
+
+
+
+ + + +
+ + +
+ {/* LEFT SIDEBAR */} +
+ +
+ + {/* MAIN CONTENT */} +
+ +
+ + {/* RIGHT SIDEBAR - Hidden on md, shown on lg+ */} +
+ +
+
+ + {/* RIGHT SIDEBAR CONTENT FOR TABLET (md) - Shown below main content */} +
+
+ +
+
- - -
- - -
-
); diff --git a/src/app/[interval]/[[...date]]/queries.ts b/src/app/[interval]/[[...date]]/queries.ts index 58e1d2bc1..ac7a47f51 100644 --- a/src/app/[interval]/[[...date]]/queries.ts +++ b/src/app/[interval]/[[...date]]/queries.ts @@ -180,6 +180,7 @@ export async function getMetricsForInterval( codeChanges: repoMetrics.codeChanges, topContributors: repoMetrics.topContributors, focusAreas: repoMetrics.focusAreas, + topFilesChanged: repoMetrics.topFilesChanged, topIssues, topPullRequests, detailedContributorSummaries, // Add the new field here diff --git a/src/components/activity-item.tsx b/src/components/activity-item.tsx index 846b07881..1d9d1ca07 100644 --- a/src/components/activity-item.tsx +++ b/src/components/activity-item.tsx @@ -12,6 +12,7 @@ interface ActivityItemProps { icon: React.ReactNode; metadata?: React.ReactNode; className?: string; + allowTitleWrap?: boolean; } export function ActivityItem({ @@ -23,6 +24,7 @@ export function ActivityItem({ icon, metadata, className, + allowTitleWrap = false, }: ActivityItemProps) { return (
-

{title}

+

+ {title} +

diff --git a/src/lib/pipelines/export/queries.ts b/src/lib/pipelines/export/queries.ts index 478d1931e..4701300a7 100644 --- a/src/lib/pipelines/export/queries.ts +++ b/src/lib/pipelines/export/queries.ts @@ -283,6 +283,17 @@ export async function getProjectMetrics(params: QueryParams = {}) { .sort((a, b) => b.count - a.count) .slice(0, 10); + // Get all files changed (sorted by total changes: additions + deletions) + const topFilesChanged = prFiles + .map((file) => ({ + path: file.path, + additions: file.additions || 0, + deletions: file.deletions || 0, + totalChanges: (file.additions || 0) + (file.deletions || 0), + })) + .filter((file) => file.totalChanges > 0) + .sort((a, b) => b.totalChanges - a.totalChanges); + // Get completed items (PRs merged in this period) const completedItems = mergedPRsThisPeriod.map((pr) => ({ title: pr.title, @@ -299,6 +310,7 @@ export async function getProjectMetrics(params: QueryParams = {}) { topContributors, codeChanges, focusAreas, + topFilesChanged, completedItems, }; }