From 5e576ae55d129d270322f3769bda8adaed6a370e Mon Sep 17 00:00:00 2001 From: Muragesh-24 Date: Thu, 21 May 2026 15:54:22 +0530 Subject: [PATCH 1/3] feat : added discussion feature --- src/app/api/metrics/discussions/route.ts | 191 +++++++++++++++++++++++ src/app/dashboard/page.tsx | 6 +- src/components/CommunityMetrics.tsx | 137 ++++++++++++++++ src/lib/auth.ts | 2 +- src/lib/metrics-cache.ts | 1 + 5 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 src/app/api/metrics/discussions/route.ts create mode 100644 src/components/CommunityMetrics.tsx diff --git a/src/app/api/metrics/discussions/route.ts b/src/app/api/metrics/discussions/route.ts new file mode 100644 index 00000000..4098a880 --- /dev/null +++ b/src/app/api/metrics/discussions/route.ts @@ -0,0 +1,191 @@ +import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; +import { authOptions } from "@/lib/auth"; +import { + getAccountToken, + getAllAccounts, + mergeMetrics, +} from "@/lib/github-accounts"; +import { + isMetricsCacheBypassed, + METRICS_CACHE_TTL_SECONDS, + metricsCacheKey, + withMetricsCache, +} from "@/lib/metrics-cache"; +import { resolveAppUser } from "@/lib/resolve-user"; + +export const dynamic = "force-dynamic"; + +interface DiscussionsMetrics { + discussionsStarted: number; + acceptedAnswers: number; + commentsPosted: number; +} + +const DISCUSSIONS_QUERY = ` + query DiscussionsMetrics($from: DateTime!, $to: DateTime!) { + viewer { + contributionsCollection(from: $from, to: $to) { + totalDiscussionContributions + totalDiscussionCommentContributions + totalDiscussionAnswerContributions + } + } + } +`; + +function getWindowDates(days: number) { + const to = new Date(); + const from = new Date(to.getTime() - days * 24 * 60 * 60 * 1000); + return { from: from.toISOString(), to: to.toISOString() }; +} + +async function fetchDiscussionsMetrics( + token: string, + days: number, + cacheContext: { bypass: boolean; userId: string } +): Promise { + const key = metricsCacheKey(cacheContext.userId, "discussions", { days }); + + return withMetricsCache( + { + bypass: cacheContext.bypass, + key, + ttlSeconds: METRICS_CACHE_TTL_SECONDS.discussions, + }, + async () => { + const { from, to } = getWindowDates(days); + const response = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + query: DISCUSSIONS_QUERY, + variables: { from, to }, + }), + cache: "no-store", + }); + + if (!response.ok) { + throw new Error("GitHub API error"); + } + + const data = (await response.json()) as { + data?: { + viewer?: { + contributionsCollection?: { + totalDiscussionContributions?: number | null; + totalDiscussionCommentContributions?: number | null; + totalDiscussionAnswerContributions?: number | null; + } | null; + } | null; + }; + }; + + const collection = data.data?.viewer?.contributionsCollection; + + return { + discussionsStarted: collection?.totalDiscussionContributions ?? 0, + acceptedAnswers: collection?.totalDiscussionAnswerContributions ?? 0, + commentsPosted: collection?.totalDiscussionCommentContributions ?? 0, + }; + } + ); +} + +function mergeDiscussionMetrics( + a: DiscussionsMetrics, + b: DiscussionsMetrics +): DiscussionsMetrics { + return { + discussionsStarted: a.discussionsStarted + b.discussionsStarted, + acceptedAnswers: a.acceptedAnswers + b.acceptedAnswers, + commentsPosted: a.commentsPosted + b.commentsPosted, + }; +} + +function formatDiscussionsMetrics(metrics: DiscussionsMetrics) { + return metrics; +} + +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.accessToken) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const accountId = req.nextUrl.searchParams.get("accountId"); + const bypass = isMetricsCacheBypassed(req); + const days = 30; + + if (!accountId) { + try { + const result = await fetchDiscussionsMetrics(session.accessToken, days, { + bypass, + userId: session.githubId ?? session.githubLogin ?? "primary", + }); + return Response.json(formatDiscussionsMetrics(result)); + } catch { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } + } + + if (!session.githubId || !session.githubLogin) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userRow = await resolveAppUser(session.githubId, session.githubLogin); + + if (!userRow) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + if (accountId === "combined") { + const accounts = await getAllAccounts( + { + token: session.accessToken, + githubId: session.githubId, + githubLogin: session.githubLogin, + }, + userRow.id + ); + + const results = await Promise.allSettled( + accounts.map((account) => + fetchDiscussionsMetrics(account.token, days, { + bypass, + userId: account.githubId, + }) + ) + ); + + const merged = mergeMetrics(results, mergeDiscussionMetrics); + + if (!merged) { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } + + return Response.json(formatDiscussionsMetrics(merged)); + } + + const token = + accountId === session.githubId + ? session.accessToken + : await getAccountToken(userRow.id, accountId); + + if (!token) { + return Response.json({ error: "Account not found" }, { status: 404 }); + } + + try { + const result = await fetchDiscussionsMetrics(token, days, { + bypass, + userId: accountId === session.githubId ? session.githubId : accountId, + }); + return Response.json(formatDiscussionsMetrics(result)); + } catch { + return Response.json({ error: "GitHub API error" }, { status: 502 }); + } +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 0a5de926..e71a3f2c 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,6 +1,7 @@ import ContributionGraph from "@/components/ContributionGraph"; import ContributionHeatmap from "@/components/ContributionHeatmap"; import PRMetrics from "@/components/PRMetrics"; +import CommunityMetrics from "@/components/CommunityMetrics"; import PRBreakdownChart from "@/components/PRBreakdownChart"; import GoalTracker from "@/components/GoalTracker"; import DashboardHeader from "@/components/DashboardHeader"; @@ -65,9 +66,10 @@ export default async function DashboardPage() { - {/* Row 2: PR metrics, PR breakdown & Time Chart */} -
+ {/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */} +
+
diff --git a/src/components/CommunityMetrics.tsx b/src/components/CommunityMetrics.tsx new file mode 100644 index 00000000..3f0d0e4f --- /dev/null +++ b/src/components/CommunityMetrics.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useAccount } from "@/components/AccountContext"; + +interface CommunityData { + discussionsStarted: number; + acceptedAnswers: number; + commentsPosted: number; +} + +export default function CommunityMetrics() { + const { selectedAccount } = useAccount(); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchMetrics = useCallback(() => { + setLoading(true); + setError(null); + + const url = + selectedAccount !== null + ? `/api/metrics/discussions?accountId=${encodeURIComponent(selectedAccount)}` + : "/api/metrics/discussions"; + + fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error("API error"); + } + return response.json(); + }) + .then((data: CommunityData) => setMetrics(data)) + .catch(() => + setError( + "We couldn't load your discussion analytics right now. Please try again in a moment." + ) + ) + .finally(() => setLoading(false)); + }, [selectedAccount]); + + useEffect(() => { + fetchMetrics(); + }, [fetchMetrics]); + + const stats = metrics + ? [ + { label: "Discussions Started (30d)", value: metrics.discussionsStarted }, + { label: "Accepted Answers", value: metrics.acceptedAnswers }, + { label: "Discussion Comments", value: metrics.commentsPosted }, + ] + : []; + + const isEmpty = + metrics != null && + metrics.discussionsStarted === 0 && + metrics.acceptedAnswers === 0 && + metrics.commentsPosted === 0; + + return ( +
+
+
+

+ Community Discussions +

+

+ GitHub Discussions activity across the selected account +

+
+ +
+ + {loading ? ( +
+ Loading discussion analytics +
+ {[1, 2, 3].map((item) => ( + +
+ ) : error ? ( +
+

{error}

+ +
+ ) : metrics ? ( +
+
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ))} +
+ + {isEmpty && ( +
+ No discussion activity yet in this 30-day window. +
+ )} +
+ ) : null} +
+ ); +} \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/auth.ts index fb3f4210..5e47700f 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -12,7 +12,7 @@ export const authOptions: NextAuthOptions = { clientId: process.env.GITHUB_ID ?? "", clientSecret: process.env.GITHUB_SECRET ?? "", authorization: { - params: { scope: "read:user user:email repo" }, + params: { scope: "read:user user:email repo read:discussion" }, }, }), ], diff --git a/src/lib/metrics-cache.ts b/src/lib/metrics-cache.ts index ee6491db..33147af0 100644 --- a/src/lib/metrics-cache.ts +++ b/src/lib/metrics-cache.ts @@ -3,6 +3,7 @@ import type { NextRequest } from "next/server"; export const METRICS_CACHE_TTL_SECONDS = { contributions: 5 * 60, + discussions: 10 * 60, repos: 10 * 60, prs: 10 * 60, "pr-review-time": 10 * 60, From 4bafcdd8104e5ad13423b303d62bb72b4765d0d9 Mon Sep 17 00:00:00 2001 From: Muragesh-24 Date: Thu, 21 May 2026 16:02:29 +0530 Subject: [PATCH 2/3] fix : responsiveness --- src/app/dashboard/page.tsx | 2 +- src/components/CommunityMetrics.tsx | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index e71a3f2c..d1cb9376 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -67,7 +67,7 @@ export default async function DashboardPage() {
{/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */} -
+
diff --git a/src/components/CommunityMetrics.tsx b/src/components/CommunityMetrics.tsx index 3f0d0e4f..0df3e324 100644 --- a/src/components/CommunityMetrics.tsx +++ b/src/components/CommunityMetrics.tsx @@ -59,20 +59,20 @@ export default function CommunityMetrics() { metrics.commentsPosted === 0; return ( -
-
-
+
+
+

Community Discussions

-

+

GitHub Discussions activity across the selected account

@@ -86,12 +86,12 @@ export default function CommunityMetrics() { className="space-y-4" > Loading discussion analytics -
+
{[1, 2, 3].map((item) => ( @@ -109,16 +109,16 @@ export default function CommunityMetrics() {
) : metrics ? (
-
+
{stats.map((stat) => (
-
+
{stat.value}
-
+
{stat.label}
From 0827de8b852993eab188bffa8909836796130b29 Mon Sep 17 00:00:00 2001 From: Muragesh-24 Date: Thu, 21 May 2026 20:26:13 +0530 Subject: [PATCH 3/3] added eof endline --- src/app/api/metrics/discussions/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/metrics/discussions/route.ts b/src/app/api/metrics/discussions/route.ts index 4098a880..acc0df2f 100644 --- a/src/app/api/metrics/discussions/route.ts +++ b/src/app/api/metrics/discussions/route.ts @@ -188,4 +188,4 @@ export async function GET(req: NextRequest) { } catch { return Response.json({ error: "GitHub API error" }, { status: 502 }); } -} \ No newline at end of file +}