diff --git a/src/app/analytics/[owner]/[repo]/page.tsx b/src/app/analytics/[owner]/[repo]/page.tsx index 9bc4b18..4c5bfd3 100644 --- a/src/app/analytics/[owner]/[repo]/page.tsx +++ b/src/app/analytics/[owner]/[repo]/page.tsx @@ -6,17 +6,19 @@ 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 { Star, GitFork, Users, TrendingUp, AlertTriangle, Code, Activity, Shield, HandHelping } from "lucide-react"; +import { ArrowLeft, Star, GitFork, Users, TrendingUp, AlertTriangle, Code, Activity, Shield } 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 { BeginnerIssues } from "@/components/github/BeginnerIssues"; -import { Repository, HistoricalData, ContributorData, TechnologyStack, RiskAssessment as RiskAssessmentType } from "@/lib/github"; +import { AdvancedAnalytics } from "@/lib/github"; + + interface CompetitiveAnalysis { @@ -60,101 +62,16 @@ 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: Repository): 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) => { - if (!issue.labels?.nodes?.length) return false; - - return issue.labels.nodes.some((label) => { - 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 () => { @@ -167,7 +84,7 @@ export default function AnalyticsPage() { throw new Error(data.error || "Failed to fetch repository analytics"); } - setAnalytics(data as ExtendedAnalytics); + setAnalytics(data); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { @@ -235,422 +152,397 @@ export default function AnalyticsPage() { } const { repository, historical, contributors, technologyStack, riskAssessment, trends } = analytics; - const hasBeginnerIssues = hasBeginnersIssues(repository); return ( -
- {/* Repository Header */} - - - {repository.owner.login} -
- - {repository.fullName} - -

{repository.description}

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

{repository.fullName}

+

{repository.description}

+
+ {repository.language && ( + + {repository.language} + + )} + + {repository.stargazerCount.toLocaleString()} stars - )} + + {repository.forkCount.toLocaleString()} forks + +
- {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 -

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

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

-
-
+
- - - Contributors - - - -
- {contributors?.length?.toLocaleString() ?? 0} -
-

- Active contributors -

-
-
+ {/* 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

+
+
+
+ {/* Competitive Analysis */} - - Health Score - + + Competitive Analysis + -
- {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 ? ( +
+
+
+
+ {competitiveAnalysis.analysis.competitivePosition.position} +
+
Market Position
-
Market Position
-
-
-
- {competitiveAnalysis.analysis.competitivePosition.percentile}% +
+
+ {competitiveAnalysis.analysis.competitivePosition.percentile}% +
+
Better than competitors
-
Better than competitors
-
-
-
- {Math.round(competitiveAnalysis.analysis.averageStars).toLocaleString()} +
+
+ {Math.round(competitiveAnalysis.analysis.averageStars).toLocaleString()} +
+
Avg competitor stars
-
Avg competitor stars
-
- ) : ( -
- 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

+ ) : ( +
+ Click "Analyze Competition" to discover similar repositories and your competitive position +
+ )}
+ + {/* Growth Trends Tab */} + - Forks Growth + Historical Growth Trends -
- {trends?.forksGrowth > 0 ? '+' : ''}{trends?.forksGrowth ?? 0}% -
-

Last 3 months vs previous 3 months

+ {historical.length > 0 ? ( + + ) : ( +
+ No historical data available +
+ )}
- - - 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} - -
- ))} -
-
-
- )} -
+ {/* 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

+
+
+
+ - {/* Technology Stack Tab */} - -
- {/* Language Distribution */} + {/* Contributors Tab */} + - - Language Distribution + + Top Contributors + + + - {technologyStack?.languages?.length > 0 ? ( - + {contributors.length > 0 ? ( + ) : (
- No language data available + No contributor data available
)}
- {/* Frameworks & Technologies */} - - - Frameworks & Technologies - - - {technologyStack?.frameworks?.length > 0 ? ( -
-

Detected Frameworks:

-
- {technologyStack.frameworks.map((framework, index) => ( - - {framework} + {/* Top Contributors List */} + {contributors.length > 0 && ( + + + Contributor Details + + +
+ {contributors.slice(0, 8).map((contributor, index) => ( +
+ {contributor.login} +
+
{contributor.login}
+
{contributor.contributions} contributions
+
+ + #{index + 1} - ))} -
+
+ ))}
- ) : ( -
No frameworks detected
- )} - - {technologyStack?.languages?.length > 0 && ( -
-

Language Breakdown:

-
- {technologyStack.languages.slice(0, 5).map((lang) => ( -
- {lang.name} - {lang.percentage}% -
- ))} + + + )} + + + {/* 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
+ )} - {/* Dependencies */} - {technologyStack?.dependencies?.length > 0 && ( - - - Dependencies - - -
- {technologyStack.dependencies.map((dep) => ( -
-
{dep.name}
-
{dep.version}
- - {dep.type} - + {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} + +
+ ))} +
+
+
+ )} + - {/* Risk Assessment Tab */} - - {riskAssessment ? ( + {/* Risk Assessment Tab */} + - ) : ( - - -
- No risk assessment data available -
-
-
- )} -
- - - {/* Actions */} -
- - + + + + {/* Actions */} +
+ + +
); diff --git a/src/components/github/BeginnerIssues.tsx b/src/components/github/BeginnerIssues.tsx deleted file mode 100644 index c7f8f79..0000000 --- a/src/components/github/BeginnerIssues.tsx +++ /dev/null @@ -1,183 +0,0 @@ -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 8bb9562..6f41ddf 100644 --- a/src/lib/github-types.ts +++ b/src/lib/github-types.ts @@ -42,22 +42,6 @@ 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; }; @@ -125,22 +109,6 @@ 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 e0e2445..3ec97ec 100644 --- a/src/lib/github.ts +++ b/src/lib/github.ts @@ -53,22 +53,6 @@ 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 { @@ -88,22 +72,6 @@ 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 { @@ -158,40 +126,19 @@ export interface RiskAssessment { export interface AdvancedAnalytics { repository: Repository; - 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; - }>; - }; - }>; + historical: HistoricalData[]; + contributors: ContributorData[]; + technologyStack: TechnologyStack; + riskAssessment: RiskAssessment; + trends: { + starsGrowth: number; + forksGrowth: number; + contributorsGrowth: number; + commitActivity: number; }; } -export interface ContributorCommitData { + export interface ContributorCommitData { week: string; // ISO date string for the week commits: number; additions: number; @@ -284,26 +231,6 @@ 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 } @@ -381,26 +308,6 @@ 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 - } - } - } - } } } } @@ -427,7 +334,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 +346,7 @@ export class GitHubService { per_page: 1 }), timeoutPromise - ]) as { data: unknown[]; headers: { link?: string } }; + ]) as { data: any[]; headers: { link?: string } }; // GitHub returns the total count in the Link header for pagination const linkHeader = contributorsResponse.headers.link; @@ -500,7 +407,6 @@ export class GitHubService { name: repo.licenseInfo.name, key: repo.licenseInfo.key, } : null, - beginnerIssues: repo.beginnerIssues, }, contributors: contributorsCount, releases: repo.releases.totalCount, @@ -517,7 +423,6 @@ 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); @@ -573,7 +478,6 @@ export class GitHubService { name: repo.licenseInfo.name, key: repo.licenseInfo.key, } : null, - beginnerIssues: repo.beginnerIssues, })), totalCount: search.repositoryCount, hasNextPage: search.pageInfo.hasNextPage, @@ -913,16 +817,34 @@ 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, - contributors: basicStats.contributors, - releases: basicStats.releases, - issues: basicStats.issues, - pullRequests: basicStats.pullRequests, - commits: basicStats.commits, - beginnerIssues: basicStats.beginnerIssues, + historical, + contributors, + technologyStack, + riskAssessment, + trends }; } catch (error) { console.error('Error fetching advanced analytics:', error); @@ -1321,16 +1243,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 diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 41850f8..bd0c391 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,12 +4,3 @@ 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' - }) -}