From dcacecbf38784c3ee55a0a200636e7d472f23847 Mon Sep 17 00:00:00 2001 From: kunal-511 Date: Mon, 2 Jun 2025 21:50:45 +0530 Subject: [PATCH 1/2] Beginner Friendly Issues --- src/app/analytics/[owner]/[repo]/page.tsx | 835 +++++++++++++--------- src/components/github/BeginnerIssues.tsx | 183 +++++ src/lib/github-types.ts | 32 + src/lib/github.ts | 169 +++-- src/lib/utils.ts | 9 + 5 files changed, 816 insertions(+), 412 deletions(-) create mode 100644 src/components/github/BeginnerIssues.tsx diff --git a/src/app/analytics/[owner]/[repo]/page.tsx b/src/app/analytics/[owner]/[repo]/page.tsx index 4c5bfd3..a15f050 100644 --- a/src/app/analytics/[owner]/[repo]/page.tsx +++ b/src/app/analytics/[owner]/[repo]/page.tsx @@ -6,20 +6,31 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; -import { ArrowLeft, Star, GitFork, Users, TrendingUp, AlertTriangle, Code, Activity, Shield } from "lucide-react"; +import { ArrowLeft, Star, GitFork, Users, TrendingUp, AlertTriangle, Code, Activity, Shield, HandHelping } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; -// Import our new components import GrowthChart from "@/components/charts/GrowthChart"; import LanguageChart from "@/components/charts/LanguageChart"; import ContributorChart from "@/components/charts/ContributorChart"; import RiskAssessment from "@/components/analytics/RiskAssessment"; - -import { AdvancedAnalytics } from "@/lib/github"; - - - +import { BeginnerIssues } from "@/components/github/BeginnerIssues"; + +import { Repository, HistoricalData, ContributorData, TechnologyStack, RiskAssessment as RiskAssessmentType } from "@/lib/github"; + +interface BeginnerIssue { + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; +} interface CompetitiveAnalysis { targetRepository: { @@ -62,16 +73,101 @@ interface CompetitiveAnalysis { }; } +interface ExtendedAnalytics { + repository: Repository; + contributors: ContributorData[]; + releases: number; + issues: { + open: number; + closed: number; + }; + pullRequests: { + open: number; + closed: number; + merged: number; + }; + commits: { + total: number; + lastMonth: number; + }; + beginnerIssues?: { + totalCount: number; + nodes: Array<{ + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; + }>; + }; + historical: HistoricalData[]; + technologyStack: TechnologyStack; + riskAssessment: RiskAssessmentType; + trends: { + starsGrowth: number; + forksGrowth: number; + contributorsGrowth: number; + commitActivity: number; + }; +} + +// Helper function to check if repository has beginner-friendly issues +function hasBeginnersIssues(repository: any): boolean { + if (!repository?.beginnerIssues?.nodes?.length) { + return false; + } + + const BEGINNER_LABEL_KEYWORDS = [ + 'good first issue', + 'beginner', + 'starter', + 'easy', + 'help wanted', + 'first-timer', + 'first timer', + 'good-first-issue', + 'good first', + 'help-wanted', + 'up-for-grabs', + 'low hanging fruit', + 'new contributor', + 'entry level', + 'easy pick', + 'easy fix', + 'newbie', + 'junior', + 'trivial', + ]; + + return repository.beginnerIssues.nodes.some((issue: any) => { + if (!issue.labels?.nodes?.length) return false; + + return issue.labels.nodes.some((label: any) => { + const labelName = label.name.toLowerCase(); + return BEGINNER_LABEL_KEYWORDS.some(keyword => + labelName.includes(keyword.toLowerCase()) + ); + }); + }); +} + export default function AnalyticsPage() { const params = useParams(); const owner = params.owner as string; const repo = params.repo as string; - const [analytics, setAnalytics] = useState(null); + const [analytics, setAnalytics] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [competitiveAnalysis, setCompetitiveAnalysis] = useState(null); const [loadingCompetitive, setLoadingCompetitive] = useState(false); + const [showBeginnerIssues, setShowBeginnerIssues] = useState(false); useEffect(() => { const fetchAnalytics = async () => { @@ -84,7 +180,7 @@ export default function AnalyticsPage() { throw new Error(data.error || "Failed to fetch repository analytics"); } - setAnalytics(data); + setAnalytics(data as ExtendedAnalytics); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { @@ -152,397 +248,422 @@ export default function AnalyticsPage() { } const { repository, historical, contributors, technologyStack, riskAssessment, trends } = analytics; + const hasBeginnerIssues = hasBeginnersIssues(repository); return ( -
-
- {/* Header */} -
- - - -
- {repository.owner.login} -
-

{repository.fullName}

-

{repository.description}

-
- {repository.language && ( - - {repository.language} - - )} - - {repository.stargazerCount.toLocaleString()} stars - - - {repository.forkCount.toLocaleString()} forks +
+ {/* Repository Header */} + + + {repository.owner.login} +
+ + {repository.fullName} + +

{repository.description}

+
+ + {repository.stargazerCount.toLocaleString()} + + + {repository.forkCount.toLocaleString()} + + {repository.language && ( + + {repository.language} -
+ )}
-
+ {hasBeginnerIssues && ( + + )} + + + + {/* Beginner Issues Section */} + {showBeginnerIssues && repository.beginnerIssues && ( + + )} + + {/* Tabs Navigation */} + + + + + Overview + + + + Growth + + + + Contributors + + + + Tech Stack + + + + Risk Assessment + + + + {/* Overview Tab */} + + {/* Key Metrics Grid */} +
+ + + Stars + + + +
{repository.stargazerCount.toLocaleString()}
+

+ {trends?.starsGrowth > 0 ? ( + + ) : ( + + )} + {Math.abs(trends?.starsGrowth ?? 0)}% growth +

+
+
- {/* Tabs Navigation */} - - - - - Overview - - - - Growth - - - - Contributors - - - - Tech Stack - - - - Risk Assessment - - - - {/* Overview Tab */} - - {/* Key Metrics Grid */} -
- - - Stars - - - -
{repository.stargazerCount.toLocaleString()}
-

- {trends.starsGrowth > 0 ? ( - - ) : ( - - )} - {Math.abs(trends.starsGrowth)}% growth -

-
-
- - - - Forks - - - -
{repository.forkCount.toLocaleString()}
-

- {trends.forksGrowth > 0 ? ( - - ) : ( - - )} - {Math.abs(trends.forksGrowth)}% growth -

-
-
- - - - Contributors - - - -
- {contributors.length > 0 ? contributors.length : "N/A"} -
-

- {contributors.length > 0 ? "Active contributors" : "Data unavailable"} -

-
-
- - - - Health Score - - - -
- {Math.round((riskAssessment.busFactor.score + riskAssessment.maintenanceStatus.score + riskAssessment.communityHealth.score) / 3)}% -
-

Overall health

-
-
-
+ + + Forks + + + +
{repository.forkCount.toLocaleString()}
+

+ {trends?.forksGrowth > 0 ? ( + + ) : ( + + )} + {Math.abs(trends?.forksGrowth ?? 0)}% growth +

+
+
- {/* Competitive Analysis */} - - Competitive Analysis - + + Contributors + - {competitiveAnalysis ? ( -
-
-
-
- {competitiveAnalysis.analysis.competitivePosition.position} -
-
Market Position
+
+ {contributors?.length?.toLocaleString() ?? 0} +
+

+ Active contributors +

+ + + + + + Health Score + + + +
+ {Math.round( + ((riskAssessment?.busFactor?.score ?? 0) + + (riskAssessment?.maintenanceStatus?.score ?? 0) + + (riskAssessment?.communityHealth?.score ?? 0)) / 3 + )}% +
+

Overall health

+
+
+
+ + {/* Competitive Analysis */} + + + Competitive Analysis + + + + {competitiveAnalysis ? ( +
+
+
+
+ {competitiveAnalysis.analysis.competitivePosition.position}
-
-
- {competitiveAnalysis.analysis.competitivePosition.percentile}% -
-
Better than competitors
+
Market Position
+
+
+
+ {competitiveAnalysis.analysis.competitivePosition.percentile}%
-
-
- {Math.round(competitiveAnalysis.analysis.averageStars).toLocaleString()} -
-
Avg competitor stars
+
Better than competitors
+
+
+
+ {Math.round(competitiveAnalysis.analysis.averageStars).toLocaleString()}
+
Avg competitor stars
- ) : ( -
- Click "Analyze Competition" to discover similar repositories and your competitive position -
- )} +
+ ) : ( +
+ Click "Analyze Competition" to discover similar repositories and your competitive position +
+ )} + + + + + {/* Growth Trends Tab */} + + + + Historical Growth Trends + + + {historical?.length > 0 ? ( + + ) : ( +
+ No historical data available +
+ )} +
+
+ + {/* Growth Metrics */} +
+ + + Stars Growth + + +
+ {trends?.starsGrowth > 0 ? '+' : ''}{trends?.starsGrowth ?? 0}% +
+

Last 3 months vs previous 3 months

- - {/* Growth Trends Tab */} - - Historical Growth Trends + Forks Growth - {historical.length > 0 ? ( - - ) : ( -
- No historical data available -
- )} +
+ {trends?.forksGrowth > 0 ? '+' : ''}{trends?.forksGrowth ?? 0}% +
+

Last 3 months vs previous 3 months

- {/* Growth Metrics */} -
- - - Stars Growth - - -
- {trends.starsGrowth > 0 ? '+' : ''}{trends.starsGrowth}% -
-

Last 3 months vs previous 3 months

-
-
- - - - Forks Growth - - -
- {trends.forksGrowth > 0 ? '+' : ''}{trends.forksGrowth}% -
-

Last 3 months vs previous 3 months

-
-
- - - - Commit Activity - - -
- {trends.commitActivity > 0 ? '+' : ''}{trends.commitActivity}% -
-

Recent activity trend

-
-
-
-
+ + + Commit Activity + + +
+ {trends?.commitActivity > 0 ? '+' : ''}{trends?.commitActivity ?? 0}% +
+

Recent activity trend

+
+
+
+
+ + {/* Contributors Tab */} + + + + Top Contributors + + + + + + {contributors?.length > 0 ? ( + + ) : ( +
+ No contributor data available +
+ )} +
+
+ + {/* Top Contributors List */} + {contributors?.length > 0 && ( + + + Contributor Details + + +
+ {contributors.slice(0, 8).map((contributor) => ( +
+ {contributor.login} +
+
{contributor.login}
+
{contributor.contributions} contributions
+
+ + #{contributors.indexOf(contributor) + 1} + +
+ ))} +
+
+
+ )} +
- {/* Contributors Tab */} - + {/* Technology Stack Tab */} + +
+ {/* Language Distribution */} - - Top Contributors - - - + + Language Distribution - {contributors.length > 0 ? ( - + {technologyStack?.languages?.length > 0 ? ( + ) : (
- No contributor data available + No language data available
)}
- {/* Top Contributors List */} - {contributors.length > 0 && ( - - - Contributor Details - - -
- {contributors.slice(0, 8).map((contributor, index) => ( -
- {contributor.login} -
-
{contributor.login}
-
{contributor.contributions} contributions
-
- - #{index + 1} + {/* Frameworks & Technologies */} + + + Frameworks & Technologies + + + {technologyStack?.frameworks?.length > 0 ? ( +
+

Detected Frameworks:

+
+ {technologyStack.frameworks.map((framework, index) => ( + + {framework} -
- ))} -
-
-
- )} - - - {/* Technology Stack Tab */} - -
- {/* Language Distribution */} - - - Language Distribution - - - {technologyStack.languages.length > 0 ? ( - - ) : ( -
- No language data available + ))}
- )} -
-
- - {/* Frameworks & Technologies */} - - - Frameworks & Technologies - - - {technologyStack.frameworks.length > 0 ? ( -
-

Detected Frameworks:

-
- {technologyStack.frameworks.map((framework, index) => ( - - {framework} - - ))} -
-
- ) : ( -
No frameworks detected
- )} +
+ ) : ( +
No frameworks detected
+ )} - {technologyStack.languages.length > 0 && ( -
-

Language Breakdown:

-
- {technologyStack.languages.slice(0, 5).map((lang) => ( -
- {lang.name} - {lang.percentage}% -
- ))} -
+ {technologyStack?.languages?.length > 0 && ( +
+

Language Breakdown:

+
+ {technologyStack.languages.slice(0, 5).map((lang) => ( +
+ {lang.name} + {lang.percentage}% +
+ ))}
- )} - - -
- - {/* Dependencies */} - {technologyStack.dependencies.length > 0 && ( - - - Dependencies - - -
- {technologyStack.dependencies.map((dep, index) => ( -
-
{dep.name}
-
{dep.version}
- - {dep.type} - -
- ))}
-
-
- )} - + )} + + +
+ + {/* Dependencies */} + {technologyStack?.dependencies?.length > 0 && ( + + + Dependencies + + +
+ {technologyStack.dependencies.map((dep) => ( +
+
{dep.name}
+
{dep.version}
+ + {dep.type} + +
+ ))} +
+
+
+ )} +
- {/* Risk Assessment Tab */} - + {/* Risk Assessment Tab */} + + {riskAssessment ? ( - - - - {/* Actions */} -
- - -
+ ) : ( + + +
+ No risk assessment data available +
+
+
+ )} +
+ + + {/* Actions */} +
+ +
); diff --git a/src/components/github/BeginnerIssues.tsx b/src/components/github/BeginnerIssues.tsx new file mode 100644 index 0000000..c7f8f79 --- /dev/null +++ b/src/components/github/BeginnerIssues.tsx @@ -0,0 +1,183 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { HandHelping, ChevronDown, ChevronUp } from "lucide-react"; +import { formatDate } from '@/lib/utils'; + +interface BeginnerIssue { + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; +} + +interface BeginnerIssuesProps { + issues: { + totalCount: number; + pageInfo?: { + hasNextPage: boolean; + endCursor: string | null; + }; + nodes: BeginnerIssue[]; + }; +} + +// List of beginner-friendly label keywords (case-insensitive) +const BEGINNER_LABEL_KEYWORDS = [ + 'good first issue', + 'beginner', + 'starter', + 'easy', + 'help wanted', + 'first-timer', + 'first timer', + 'good-first-issue', + 'good first', + 'help-wanted', + 'up-for-grabs', + 'low hanging fruit', + 'new contributor', + 'entry level', + 'easy pick', + 'easy fix', + 'newbie', + 'junior', + 'trivial', +]; + +// Helper function to check if an issue has a beginner-friendly label +function hasBeginnersLabel(issue: BeginnerIssue): boolean { + if (!issue.labels || !issue.labels.nodes || issue.labels.nodes.length === 0) { + return false; + } + + return issue.labels.nodes.some(label => { + const labelName = label.name.toLowerCase(); + return BEGINNER_LABEL_KEYWORDS.some(keyword => + labelName.includes(keyword.toLowerCase()) + ); + }); +} + +export function BeginnerIssues({ issues }: BeginnerIssuesProps) { + const [displayCount, setDisplayCount] = useState(10); + const [isExpanded, setIsExpanded] = useState(false); + + if (!issues || issues.totalCount === 0) { + return null; + } + + // Filter issues with beginner-friendly labels + const beginnerFriendlyIssues = issues.nodes + .filter(hasBeginnersLabel) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + if (beginnerFriendlyIssues.length === 0) { + return null; + } + + const visibleIssues = beginnerFriendlyIssues.slice(0, displayCount); + const hasMoreIssues = beginnerFriendlyIssues.length > displayCount; + + const handleShowMore = () => { + setDisplayCount(prev => Math.min(prev + 10, beginnerFriendlyIssues.length)); + setIsExpanded(true); + }; + + const handleShowLess = () => { + setDisplayCount(10); + setIsExpanded(false); + }; + + return ( + + + + + Beginner-Friendly Issues ({beginnerFriendlyIssues.length}) + + + + + + {(hasMoreIssues || isExpanded) && ( + + {hasMoreIssues && !isExpanded && ( + + )} + {hasMoreIssues && isExpanded && ( + + )} + {isExpanded && ( + + )} + + )} + + ); +} \ No newline at end of file diff --git a/src/lib/github-types.ts b/src/lib/github-types.ts index 6f41ddf..8bb9562 100644 --- a/src/lib/github-types.ts +++ b/src/lib/github-types.ts @@ -42,6 +42,22 @@ export interface GitHubRepositoryResponse { issues: { totalCount: number; }; + beginnerIssues: { + totalCount: number; + nodes: Array<{ + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; + }>; + }; closedIssues: { totalCount: number; }; @@ -109,6 +125,22 @@ export interface GitHubSearchResponse { name: string; key: string; } | null; + beginnerIssues: { + totalCount: number; + nodes: Array<{ + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; + }>; + }; }>; }; } \ No newline at end of file diff --git a/src/lib/github.ts b/src/lib/github.ts index 3ec97ec..649f761 100644 --- a/src/lib/github.ts +++ b/src/lib/github.ts @@ -53,6 +53,22 @@ export interface Repository { name: string; key: string; } | null; + beginnerIssues?: { + totalCount: number; + nodes: Array<{ + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; + }>; + }; } export interface RepositoryStats { @@ -72,6 +88,22 @@ export interface RepositoryStats { total: number; lastMonth: number; }; + beginnerIssues?: { + totalCount: number; + nodes: Array<{ + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; + }>; + }; } export interface HistoricalData { @@ -126,19 +158,40 @@ export interface RiskAssessment { export interface AdvancedAnalytics { repository: Repository; - historical: HistoricalData[]; - contributors: ContributorData[]; - technologyStack: TechnologyStack; - riskAssessment: RiskAssessment; - trends: { - starsGrowth: number; - forksGrowth: number; - contributorsGrowth: number; - commitActivity: number; + contributors: number; + releases: number; + issues: { + open: number; + closed: number; + }; + pullRequests: { + open: number; + closed: number; + merged: number; + }; + commits: { + total: number; + lastMonth: number; + }; + beginnerIssues?: { + totalCount: number; + nodes: Array<{ + id: string; + number: number; + title: string; + url: string; + createdAt: string; + labels: { + nodes: Array<{ + name: string; + color: string; + }>; + }; + }>; }; } - export interface ContributorCommitData { +export interface ContributorCommitData { week: string; // ISO date string for the week commits: number; additions: number; @@ -231,6 +284,26 @@ export const REPOSITORY_QUERY = ` issues(states: [OPEN]) { totalCount } + beginnerIssues: issues(states: [OPEN], first: 100) { + totalCount + pageInfo { + hasNextPage + endCursor + } + nodes { + id + number + title + url + createdAt + labels(first: 10) { + nodes { + name + color + } + } + } + } closedIssues: issues(states: [CLOSED]) { totalCount } @@ -308,6 +381,26 @@ export const SEARCH_REPOSITORIES_QUERY = ` name key } + beginnerIssues: issues(states: [OPEN], first: 100) { + totalCount + pageInfo { + hasNextPage + endCursor + } + nodes { + id + number + title + url + createdAt + labels(first: 10) { + nodes { + name + color + } + } + } + } } } } @@ -358,27 +451,8 @@ export class GitHubService { } else { contributorsCount = contributorsResponse.data.length; } - } catch (contributorsError) { - console.warn('Could not fetch contributors count (using fallback):', contributorsError instanceof Error ? contributorsError.message : 'Unknown error'); - - // Try alternative approach using GraphQL if REST fails - try { - const contributorsGraphQL = await getGitHubGraphQL()( - `query GetContributors($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { - mentionableUsers(first: 1) { - totalCount - } - } - }`, - { owner, name } - ) as { repository?: { mentionableUsers?: { totalCount: number } } }; - - contributorsCount = contributorsGraphQL.repository?.mentionableUsers?.totalCount || 0; - } catch { - console.warn('GraphQL contributors fallback also failed, defaulting to 0'); - contributorsCount = 0; // Final fallback - } + } catch (error) { + console.warn('Failed to get contributors count:', error); } return { @@ -407,6 +481,7 @@ export class GitHubService { name: repo.licenseInfo.name, key: repo.licenseInfo.key, } : null, + beginnerIssues: repo.beginnerIssues, }, contributors: contributorsCount, releases: repo.releases.totalCount, @@ -423,6 +498,7 @@ export class GitHubService { total: repo.defaultBranchRef?.target?.history?.totalCount || 0, lastMonth: repo.defaultBranchRef?.target?.historyLastMonth?.totalCount || 0, }, + beginnerIssues: repo.beginnerIssues, }; } catch (error) { console.error('Error fetching repository stats:', error); @@ -478,6 +554,7 @@ export class GitHubService { name: repo.licenseInfo.name, key: repo.licenseInfo.key, } : null, + beginnerIssues: repo.beginnerIssues, })), totalCount: search.repositoryCount, hasNextPage: search.pageInfo.hasNextPage, @@ -817,34 +894,16 @@ export class GitHubService { */ static async getAdvancedAnalytics(owner: string, name: string): Promise { try { - // Get basic repository data const basicStats = await this.getRepositoryStats(owner, name); - // Get advanced data in parallel - const [historical, contributors, technologyStack] = await Promise.all([ - this.getHistoricalData(owner, name), - this.getContributorAnalysis(owner, name), - this.getTechnologyStack(owner, name) - ]); - - // Get risk assessment (depends on contributors data) - const riskAssessment = await this.getRiskAssessment(owner, name, contributors); - - // Calculate trends - const trends = { - starsGrowth: calculateGrowthRate(historical, 'stars'), - forksGrowth: calculateGrowthRate(historical, 'forks'), - contributorsGrowth: contributors.length > 0 ? 5 : 0, // Simplified - commitActivity: calculateGrowthRate(historical, 'commits') - }; - return { repository: basicStats.repository, - historical, - contributors, - technologyStack, - riskAssessment, - trends + contributors: basicStats.contributors, + releases: basicStats.releases, + issues: basicStats.issues, + pullRequests: basicStats.pullRequests, + commits: basicStats.commits, + beginnerIssues: basicStats.beginnerIssues, }; } catch (error) { console.error('Error fetching advanced analytics:', error); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391..41850f8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,3 +4,12 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function formatDate(dateString: string): string { + const date = new Date(dateString) + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} From 09021f5a619c905b6049ad2f2e739b305d01f4a3 Mon Sep 17 00:00:00 2001 From: kunal-511 Date: Mon, 2 Jun 2025 22:25:44 +0530 Subject: [PATCH 2/2] Fixed lint issues --- src/app/analytics/[owner]/[repo]/page.tsx | 21 ++--------- src/lib/github.ts | 45 ++++++++++++++++------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/app/analytics/[owner]/[repo]/page.tsx b/src/app/analytics/[owner]/[repo]/page.tsx index a15f050..9bc4b18 100644 --- a/src/app/analytics/[owner]/[repo]/page.tsx +++ b/src/app/analytics/[owner]/[repo]/page.tsx @@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; -import { ArrowLeft, Star, GitFork, Users, TrendingUp, AlertTriangle, Code, Activity, Shield, HandHelping } from "lucide-react"; +import { Star, GitFork, Users, TrendingUp, AlertTriangle, Code, Activity, Shield, HandHelping } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; @@ -18,19 +18,6 @@ import { BeginnerIssues } from "@/components/github/BeginnerIssues"; import { Repository, HistoricalData, ContributorData, TechnologyStack, RiskAssessment as RiskAssessmentType } from "@/lib/github"; -interface BeginnerIssue { - id: string; - number: number; - title: string; - url: string; - createdAt: string; - labels: { - nodes: Array<{ - name: string; - color: string; - }>; - }; -} interface CompetitiveAnalysis { targetRepository: { @@ -118,7 +105,7 @@ interface ExtendedAnalytics { } // Helper function to check if repository has beginner-friendly issues -function hasBeginnersIssues(repository: any): boolean { +function hasBeginnersIssues(repository: Repository): boolean { if (!repository?.beginnerIssues?.nodes?.length) { return false; } @@ -145,10 +132,10 @@ function hasBeginnersIssues(repository: any): boolean { 'trivial', ]; - return repository.beginnerIssues.nodes.some((issue: any) => { + return repository.beginnerIssues.nodes.some((issue) => { if (!issue.labels?.nodes?.length) return false; - return issue.labels.nodes.some((label: any) => { + return issue.labels.nodes.some((label) => { const labelName = label.name.toLowerCase(); return BEGINNER_LABEL_KEYWORDS.some(keyword => labelName.includes(keyword.toLowerCase()) diff --git a/src/lib/github.ts b/src/lib/github.ts index 649f761..e0e2445 100644 --- a/src/lib/github.ts +++ b/src/lib/github.ts @@ -427,7 +427,7 @@ export class GitHubService { const restClient = getGitHubRest(); // Create a timeout promise - const timeoutPromise = new Promise((_, reject) => { + const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Contributors request timeout')), 5000); // 5 second timeout }); @@ -439,7 +439,7 @@ export class GitHubService { per_page: 1 }), timeoutPromise - ]) as { data: any[]; headers: { link?: string } }; + ]) as { data: unknown[]; headers: { link?: string } }; // GitHub returns the total count in the Link header for pagination const linkHeader = contributorsResponse.headers.link; @@ -451,8 +451,27 @@ export class GitHubService { } else { contributorsCount = contributorsResponse.data.length; } - } catch (error) { - console.warn('Failed to get contributors count:', error); + } catch (contributorsError) { + console.warn('Could not fetch contributors count (using fallback):', contributorsError instanceof Error ? contributorsError.message : 'Unknown error'); + + // Try alternative approach using GraphQL if REST fails + try { + const contributorsGraphQL = await getGitHubGraphQL()( + `query GetContributors($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + mentionableUsers(first: 1) { + totalCount + } + } + }`, + { owner, name } + ) as { repository?: { mentionableUsers?: { totalCount: number } } }; + + contributorsCount = contributorsGraphQL.repository?.mentionableUsers?.totalCount || 0; + } catch { + console.warn('GraphQL contributors fallback also failed, defaulting to 0'); + contributorsCount = 0; // Final fallback + } } return { @@ -1302,16 +1321,16 @@ function detectFrameworks(languages: Array<{ name: string }>, dependencies: Arra return Array.from(frameworks); } -function calculateGrowthRate(historical: HistoricalData[], field: keyof HistoricalData): number { - if (historical.length < 2) return 0; +// function calculateGrowthRate(historical: HistoricalData[], field: keyof HistoricalData): number { +// if (historical.length < 2) return 0; - const recent = historical.slice(-3); // Last 3 months - const older = historical.slice(-6, -3); // Previous 3 months +// const recent = historical.slice(-3); // Last 3 months +// const older = historical.slice(-6, -3); // Previous 3 months - const recentAvg = recent.reduce((sum, item) => sum + (Number(item[field]) || 0), 0) / recent.length; - const olderAvg = older.reduce((sum, item) => sum + (Number(item[field]) || 0), 0) / older.length; +// const recentAvg = recent.reduce((sum, item) => sum + (Number(item[field]) || 0), 0) / recent.length; +// const olderAvg = older.reduce((sum, item) => sum + (Number(item[field]) || 0), 0) / older.length; - if (olderAvg === 0) return recentAvg > 0 ? 100 : 0; +// if (olderAvg === 0) return recentAvg > 0 ? 100 : 0; - return Math.round(((recentAvg - olderAvg) / olderAvg) * 100); -} \ No newline at end of file +// return Math.round(((recentAvg - olderAvg) / olderAvg) * 100); +// } \ No newline at end of file