diff --git a/src/app/api/metrics/discussions/route.ts b/src/app/api/metrics/discussions/route.ts new file mode 100644 index 00000000..2b815d2f --- /dev/null +++ b/src/app/api/metrics/discussions/route.ts @@ -0,0 +1,120 @@ +import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; +import { authOptions } from "@/lib/auth"; + +export const dynamic = "force-dynamic"; + +const GITHUB_API = "https://api.github.com"; + +interface DiscussionData { + discussionsStarted: number; + commentsGiven: number; + markedAsAnswer: number; +} + +async function fetchJson(url: string, token: string): Promise { + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", + }, + cache: "no-store", + }); + if (!res.ok) throw new Error("GitHub API error"); + return (await res.json()) as T; +} + +async function fetchGraphQL( + query: string, + token: string +): Promise { + const res = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query }), + cache: "no-store", + }); + if (!res.ok) throw new Error("GitHub GraphQL error"); + const json = await res.json(); + return json.data as T; +} + +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.accessToken || !session.githubLogin) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const login = session.githubLogin; + const token = session.accessToken; + + try { + // 1) Discussions started — REST search + const discussionsRes = await fetchJson<{ total_count: number }>( + `${GITHUB_API}/search/issues?q=author:${login}+type:discussion&per_page=1`, + token + ); + const discussionsStarted = + typeof discussionsRes.total_count === "number" + ? discussionsRes.total_count + : 0; + + // 2) Discussion comments and marked-as-answer via GraphQL + const query = ` + { + user(login: "${login}") { + contributionsCollection { + totalCommitContributions + } + } + search(query: "commenter:${login} type:discussion", type: DISCUSSION, first: 100) { + discussionCount + } + } + `; + + let commentsGiven = 0; + let markedAsAnswer = 0; + + try { + // GraphQL for marked-as-answer count + const answersQuery = ` + { + search(query: "answered-by:${login} type:discussion", type: DISCUSSION, first: 1) { + discussionCount + } + } + `; + const answersData = await fetchGraphQL<{ + search: { discussionCount: number }; + }>(answersQuery, token); + markedAsAnswer = answersData?.search?.discussionCount ?? 0; + + // GraphQL for comments given + const commentsData = await fetchGraphQL<{ + search: { discussionCount: number }; + }>(query, token); + commentsGiven = commentsData?.search?.discussionCount ?? 0; + } catch { + // GraphQL may not support discussion search — fallback to 0 + } + + const data: DiscussionData = { + discussionsStarted, + commentsGiven, + markedAsAnswer, + }; + + return Response.json(data, { + headers: { + // cache for 5 minutes to avoid rate limit issues + "Cache-Control": "private, max-age=300", + }, + }); + } 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..bc46400c 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,3 +1,4 @@ +import DiscussionsWidget from "@/components/DiscussionsWidget"; import ContributionGraph from "@/components/ContributionGraph"; import ContributionHeatmap from "@/components/ContributionHeatmap"; import PRMetrics from "@/components/PRMetrics"; @@ -83,6 +84,10 @@ export default async function DashboardPage() { + {/* Row 3b: Discussion activity */} +
+ +
{/* Row 4: Pinned repositories */}
diff --git a/src/components/DiscussionsWidget.tsx b/src/components/DiscussionsWidget.tsx new file mode 100644 index 00000000..e0b5e1a9 --- /dev/null +++ b/src/components/DiscussionsWidget.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +interface DiscussionData { + discussionsStarted: number; + commentsGiven: number; + markedAsAnswer: number; +} + +export default function DiscussionsWidget() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(() => { + setLoading(true); + setError(null); + fetch("/api/metrics/discussions") + .then((r) => { + if (!r.ok) throw new Error("API error"); + return r.json(); + }) + .then((d: DiscussionData) => setData(d)) + .catch(() => + setError("We couldn't load your discussion metrics right now.") + ) + .finally(() => setLoading(false)); + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const stats = data + ? [ + { + label: "Discussions Started", + value: data.discussionsStarted, + title: "Total discussions you have opened", + }, + { + label: "Comments Given", + value: data.commentsGiven, + title: "Discussions you have commented on", + }, + { + label: "Marked as Answer", + value: data.markedAsAnswer, + title: "Your replies marked as the accepted answer", + }, + ] + : []; + + return ( +
+

+ Discussion Activity +

+ + {loading ? ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ) : error ? ( +
+

{error}

+ +
+ ) : ( +
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ))} +
+ )} +
+ ); +} \ No newline at end of file