├── .env.example ├── public ├── github-hero.png ├── vercel.svg ├── window.svg ├── file.svg ├── github-logo.svg ├── globe.svg └── next.svg ├── src ├── assets │ ├── cardExample.png │ ├── github-hero.png │ ├── github-logo.svg │ └── Points_icon.svg ├── app │ ├── not-found.tsx │ ├── dashboard │ │ ├── not-found.tsx │ │ ├── components │ │ │ ├── cardInfoUserSmall │ │ │ │ └── index.tsx │ │ │ ├── rateLimitModal │ │ │ │ └── index.tsx │ │ │ ├── charts │ │ │ │ ├── cardPopularReposChart │ │ │ │ │ └── index.tsx │ │ │ │ ├── wellStructuredRepoScoresChart │ │ │ │ │ └── index.tsx │ │ │ │ └── cardLanguageChart │ │ │ │ │ └── index.tsx │ │ │ ├── conquestCard │ │ │ │ └── index.tsx │ │ │ ├── cardInfoUserBigNumber │ │ │ │ └── index.tsx │ │ │ ├── modal │ │ │ │ └── index.tsx │ │ │ ├── modalShareCard │ │ │ │ └── index.tsx │ │ │ ├── stackAnalysisCard │ │ │ │ └── index.tsx │ │ │ └── footer │ │ │ │ └── index.tsx │ │ ├── layout.tsx │ │ ├── [user] │ │ │ └── page.tsx │ │ └── utils │ │ │ └── generateMetadata.ts │ ├── loading.tsx │ ├── page.tsx │ ├── globals.css │ ├── layout.tsx │ └── about │ │ └── page.tsx ├── lib │ ├── utils.ts │ ├── api │ │ ├── graphqlClient.ts │ │ └── queryGitHubData.ts │ ├── calcs │ │ ├── getPopularContributions.ts │ │ ├── calculateValorAgregado.ts │ │ ├── calcCommitStarsForks.ts │ │ ├── calculatePontosTotais.ts │ │ ├── calculateLanguageStats.ts │ │ ├── identifyWellStructuredRepos.ts │ │ ├── calcStructuredRepoScores.ts │ │ ├── formatRateLimitInfo.ts │ │ ├── calculateUnifiedCommitCount.ts │ │ ├── calculateAchievements.ts │ │ └── stackAnalysis.ts │ ├── getGithubData.ts │ └── types.ts ├── components │ ├── container │ │ └── index.tsx │ ├── cardInfo │ │ └── index.tsx │ ├── logo │ │ └── index.tsx │ ├── gradientText │ │ └── index.tsx │ ├── ui │ │ ├── tooltip.tsx │ │ ├── card.tsx │ │ └── chart.tsx │ ├── inputForm │ │ └── index.tsx │ ├── header │ │ └── index.tsx │ ├── darkVeilBG │ │ └── index.tsx │ └── LightRaysBG │ │ └── index.tsx └── constants │ └── valuesConfig.ts ├── postcss.config.mjs ├── .editorconfig ├── next.config.ts ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── LICENSE └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN_FOR_REQUESTS= 2 | NEXT_PUBLIC_HOST_URL= -------------------------------------------------------------------------------- /public/github-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreluizdasilvaa/CommitWorth/HEAD/public/github-hero.png -------------------------------------------------------------------------------- /src/assets/cardExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreluizdasilvaa/CommitWorth/HEAD/src/assets/cardExample.png -------------------------------------------------------------------------------- /src/assets/github-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreluizdasilvaa/CommitWorth/HEAD/src/assets/github-hero.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | export default function() { 4 | redirect('/') 5 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/dashboard/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from "./components/modal" 2 | export default function NotFound() { 3 | return 4 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/api/graphqlClient.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from "graphql-request" 2 | 3 | export const graphqlClient = new GraphQLClient("https://api.github.com/graphql", { 4 | headers: { 5 | Authorization: `Bearer ${process.env.GITHUB_TOKEN_FOR_REQUESTS}`, 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = crlf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /src/components/container/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | 3 | export function Container({ children }: { 4 | children: ReactNode 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /src/lib/calcs/getPopularContributions.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from "../types"; 2 | 3 | export function getPopularContributions(repos: Repository[]) { 4 | return repos 5 | .map(repo => ({ name: repo.name, stars: repo.stargazerCount ?? 0 })) 6 | .sort((a, b) => b.stars - a.stars) 7 | .slice(0, 5) 8 | } -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: 'https', 9 | hostname: 'github.githubassets.com' 10 | }, 11 | { 12 | protocol: 'https', 13 | hostname: 'avatars.githubusercontent.com' 14 | } 15 | ] 16 | } 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /src/lib/calcs/calculateValorAgregado.ts: -------------------------------------------------------------------------------- 1 | import { SCORING_CONFIG } from "@/constants/valuesConfig" 2 | 3 | export function calculateValorAgregado(totalCommits: number, totalStars: number, totalForks: number): number { 4 | const { valorPorCommit, valorPorStar, valorPorFork } = SCORING_CONFIG 5 | 6 | const valorAgregado = 7 | totalCommits * valorPorCommit + 8 | totalStars * valorPorStar + 9 | totalForks * valorPorFork 10 | 11 | return Math.round(valorAgregado * 100) / 100 12 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/lib/calcs/calcCommitStarsForks.ts: -------------------------------------------------------------------------------- 1 | import { GitHubStatsResponse, Repository } from "../types" 2 | import { calculateUnifiedCommitCount } from "./calculateUnifiedCommitCount" 3 | 4 | export function calcCommitStarsForks(data: GitHubStatsResponse, repos: Repository[]) { 5 | const totalStars = repos.reduce((total, repo) => total + (repo.stargazerCount ?? 0), 0) 6 | const totalForks = repos.reduce((total, repo) => total + (repo.forkCount ?? 0), 0) 7 | const totalCommits = calculateUnifiedCommitCount(data, repos) 8 | 9 | return { totalCommits, totalStars, totalForks } 10 | } -------------------------------------------------------------------------------- /src/lib/calcs/calculatePontosTotais.ts: -------------------------------------------------------------------------------- 1 | import { SCORING_CONFIG } from "@/constants/valuesConfig" 2 | 3 | export function calculatePontosTotais( 4 | totalCommits: number, 5 | totalStars: number, 6 | totalForks: number, 7 | wellStructuredReposCount: number 8 | ): number { 9 | const { pontosPorCommit, pontosPorStar, pontosPorFork, bonusRepoBemEstruturado } = SCORING_CONFIG 10 | 11 | return ( 12 | totalCommits * pontosPorCommit + 13 | totalStars * pontosPorStar + 14 | totalForks * pontosPorFork + 15 | wellStructuredReposCount * bonusRepoBemEstruturado 16 | ) 17 | } -------------------------------------------------------------------------------- /src/lib/calcs/calculateLanguageStats.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from "../types" 2 | 3 | export function calculateLanguageStats(repos: Repository[]) { 4 | const languageCount = new Map() 5 | 6 | repos.forEach(repo => { 7 | const mainLanguage = repo.languages?.nodes?.[0]?.name 8 | if (mainLanguage) { 9 | languageCount.set(mainLanguage, (languageCount.get(mainLanguage) ?? 0) + 1) 10 | } 11 | }) 12 | 13 | return Array.from(languageCount.entries()) 14 | .sort(([, a], [, b]) => b - a) 15 | .map(([language, count]) => ({ language, count })) 16 | .slice(0, 5) 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /src/lib/calcs/identifyWellStructuredRepos.ts: -------------------------------------------------------------------------------- 1 | import { Repository, WellStructuredRepo } from "../types"; 2 | 3 | export function identifyWellStructuredRepos(repos: Repository[]): WellStructuredRepo[] { 4 | return repos 5 | .filter(repo => repo.description && repo.description.length > 50 && repo.homepageUrl && repo.hasIssuesEnabled) 6 | .map(repo => ({ 7 | name: repo.name, 8 | description: repo.description, 9 | homepageUrl: repo.homepageUrl, 10 | stars: repo.stargazerCount ?? 0, 11 | forks: repo.forkCount ?? 0, 12 | mainLanguage: repo.languages?.nodes?.[0]?.name, 13 | })) 14 | .slice(0, 5) 15 | } -------------------------------------------------------------------------------- /src/components/cardInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from "lucide-react"; 2 | 3 | interface CardInfo { 4 | icon: LucideIcon; 5 | title: string; 6 | description: string; 7 | } 8 | 9 | export function CardInfo({ icon: Icon, title, description }: CardInfo) { 10 | return ( 11 |
12 |
13 | 14 |

{title}

15 |
16 |

{description}

17 |
18 | ) 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/logo/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import ImageGithub from '@/assets/github-logo.svg' 6 | 7 | export function Logo({ isImg, className }: { isImg?:boolean, className?: string }) { 8 | let linkHref = isImg ? '#' : '/' 9 | 10 | return ( 11 | 12 | Logo CommitWorth 18 |

CommitWorth

19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /src/lib/calcs/calcStructuredRepoScores.ts: -------------------------------------------------------------------------------- 1 | import { SCORING_CONFIG } from "@/constants/valuesConfig" 2 | import { WellStructuredRepo } from "../types" 3 | 4 | interface RepoScore { 5 | name: string 6 | score: number 7 | } 8 | 9 | export function calcStructuredRepoScores(wellStructuredRepos: WellStructuredRepo[]): RepoScore[] { 10 | const { pontosPorStar, pontosPorFork, bonusRepoBemEstruturado } = SCORING_CONFIG 11 | 12 | return wellStructuredRepos 13 | .map(repo => ({ 14 | name: repo.name, 15 | score: 16 | repo.stars * pontosPorStar + 17 | repo.forks * pontosPorFork + 18 | bonusRepoBemEstruturado, 19 | })) 20 | .sort((a, b) => b.score - a.score) 21 | } -------------------------------------------------------------------------------- /src/constants/valuesConfig.ts: -------------------------------------------------------------------------------- 1 | export const SCORING_CONFIG = { 2 | valorPorCommit: 2, 3 | valorPorStar: 0.50, 4 | valorPorFork: 1, 5 | pontosPorCommit: 1, 6 | pontosPorStar: 5, 7 | pontosPorFork: 3, 8 | bonusRepoBemEstruturado: 10, 9 | } as const 10 | 11 | // Constantes para os thresholds dos achievements 12 | export const ACHIEVEMENT_THRESHOLDS = { 13 | CODE_WARRIOR_COMMITS: 1000, 14 | CODE_EMPIRE_REPOS: 50, 15 | GITHUB_ARCHITECT_LANGUAGES: 10, 16 | GITHUB_STAR_TOTAL: 100, 17 | GOLDEN_PROJECT_STARS: 500, 18 | CODE_VETERAN_YEARS: 10, 19 | GITHUB_OLD_SCHOOL_YEARS: 5, 20 | STACK_SPECIALIST_SCORE: 70, 21 | TECH_LEADER_LEVEL: 'Tech Lead', 22 | SENIOR_LEVEL: 'Senior', 23 | POLYGLOT_LANGUAGES: 15, 24 | } as const -------------------------------------------------------------------------------- /src/lib/calcs/formatRateLimitInfo.ts: -------------------------------------------------------------------------------- 1 | import { GitHubStatsResponse } from "../types" 2 | 3 | export interface RateLimitInfo { 4 | limit: number 5 | remaining: number 6 | resetAtRelative: string 7 | } 8 | 9 | export function formatRateLimitInfo(rateLimit: GitHubStatsResponse['rateLimit']): RateLimitInfo { 10 | const resetDate = new Date(rateLimit.resetAt) 11 | const now = new Date() 12 | 13 | const secondsUntilReset = Math.max(0, Math.floor((resetDate.getTime() - now.getTime()) / 1000)) 14 | 15 | return { 16 | limit: rateLimit.limit, 17 | remaining: rateLimit.remaining, 18 | resetAtRelative: secondsUntilReset > 0 19 | ? `${Math.floor(secondsUntilReset / 60)}min ${secondsUntilReset % 60}s` 20 | : 'agora' 21 | } 22 | } -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return ( 3 |
4 |
5 |
8 |
11 |
12 |
13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/app/dashboard/components/cardInfoUserSmall/index.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react' 2 | 3 | interface CardInfoUserSmallProps { 4 | title: string; 5 | Icon: LucideIcon; 6 | value?: number; 7 | className?: string; 8 | } 9 | 10 | export function CardInfoUserSmall({ title, Icon, value, className }: CardInfoUserSmallProps) { 11 | return ( 12 |
13 |
14 |

{title}

15 | 16 | 17 |
18 | 19 |

{value}

20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /public/github-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/github-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/calcs/calculateUnifiedCommitCount.ts: -------------------------------------------------------------------------------- 1 | import { GitHubStatsResponse, Repository } from "../types" 2 | 3 | // calcula commits de forma mais precisa 4 | export function calculateUnifiedCommitCount(data: GitHubStatsResponse, nonForkRepos: Repository[]): number { 5 | // Soma dos commits dos repositórios individuais 6 | const repoCommits = nonForkRepos.reduce((total, repo) => { 7 | return total + (repo.defaultBranchRef?.target?.history?.totalCount ?? 0) 8 | }, 0) 9 | 10 | // Soma dos commits por repositório das contribuições 11 | const commitsByRepo = data.user.contributionsCollection?.commitContributionsByRepository?.reduce((total, repoContrib) => { 12 | return total + (repoContrib.contributions?.totalCount ?? 0) 13 | }, 0) ?? 0 14 | 15 | // Total de contribuições do último ano 16 | const contributionCommits = data.user.contributionsCollection?.contributionCalendar?.totalContributions ?? 0 17 | 18 | return Math.max(repoCommits, commitsByRepo, contributionCommits) 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commitworth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-tooltip": "^1.2.7", 13 | "axios": "^1.11.0", 14 | "class-variance-authority": "^0.7.1", 15 | "clsx": "^2.1.1", 16 | "date-fns": "^4.1.0", 17 | "graphql-request": "^7.2.0", 18 | "html-to-image": "^1.11.13", 19 | "lucide-react": "^0.534.0", 20 | "next": "15.4.5", 21 | "ogl": "^1.0.11", 22 | "react": "19.1.0", 23 | "react-dom": "19.1.0", 24 | "recharts": "^2.15.4", 25 | "sonner": "^2.0.6", 26 | "tailwind-merge": "^3.3.1" 27 | }, 28 | "devDependencies": { 29 | "@tailwindcss/postcss": "^4", 30 | "@types/node": "^20", 31 | "@types/react": "^19", 32 | "@types/react-dom": "^19", 33 | "tailwindcss": "^4", 34 | "tw-animate-css": "^1.3.6", 35 | "typescript": "^5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 André Luiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/app/dashboard/components/rateLimitModal/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { X } from "lucide-react"; 4 | import { useState } from "react"; 5 | 6 | export interface RateLimitProps { 7 | totalLimit: number; 8 | remainder: number; 9 | reset: string; 10 | } 11 | 12 | export function RateLimitModal({ 13 | totalLimit, 14 | remainder, 15 | reset 16 | }: RateLimitProps) { 17 | const [visible, setVisible] = useState(true) 18 | 19 | return ( 20 |
21 |
22 |

Total de buscas: {totalLimit}

23 | setVisible(false)} /> 24 |
25 |

Restantes: {remainder}

26 |

Reseta em {reset}

27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/gradientText/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | interface GradientTextProps { 4 | children: ReactNode; 5 | className?: string; 6 | colors?: string[]; 7 | animationSpeed?: number; 8 | showBorder?: boolean; 9 | } 10 | 11 | export function GradientText({ 12 | children, 13 | className = "", 14 | colors = ["#ffaa40", "#9c40ff", "#ffaa40"], 15 | animationSpeed = 2, 16 | showBorder = false, 17 | }: GradientTextProps) { 18 | const gradientStyle = { 19 | backgroundImage: `linear-gradient(to right, ${colors.join(", ")})`, 20 | animationDuration: `${animationSpeed}s`, 21 | }; 22 | 23 | return ( 24 | 27 | 36 | {children} 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/dashboard/components/charts/cardPopularReposChart/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | ChartConfig, 5 | ChartContainer, 6 | ChartTooltip, 7 | ChartTooltipContent 8 | } from "@/components/ui/chart" 9 | 10 | import { Bar, BarChart, XAxis } from "recharts" 11 | 12 | const chartConfig = { 13 | stars: { 14 | label: "stars", 15 | color: "#E3B341", 16 | }, 17 | } satisfies ChartConfig 18 | 19 | interface CardProps { 20 | title: string; 21 | value: { 22 | name: string; 23 | stars: number; 24 | }[]; 25 | } 26 | 27 | export function CardPopularReposChart({ title, value }: CardProps) { 28 | return ( 29 |
30 |
31 |

{title}

32 |
33 | 34 | 35 | 36 | value.slice(0, 11)} 42 | /> 43 | } /> 44 | 45 | 46 | 47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/app/dashboard/components/conquestCard/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | interface ConquestModalProps { 3 | title: string; 4 | description: string; 5 | isComplete: boolean; 6 | } 7 | 8 | export function ConquestModal({ description, isComplete, title }:ConquestModalProps) { 9 | return ( 10 |
14 |

{title}

15 |

{description}

16 | 17 |
18 | 22 | {isComplete ? 'Completo' : 'Em progresso'} 23 | 24 | 25 | {isComplete ? ( 26 |
27 |
28 | {title} 29 |
30 | ) : ( 31 |
32 |
33 | {title} 34 |
35 | )} 36 | 37 |
38 |
39 | ) 40 | } -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ) 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 62 | -------------------------------------------------------------------------------- /src/lib/api/queryGitHubData.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "graphql-request"; 2 | 3 | export const queryGitHubData = gql` 4 | query getUserStats($login: String!) { 5 | rateLimit { 6 | limit 7 | remaining 8 | resetAt 9 | } 10 | user(login: $login) { 11 | id 12 | login 13 | name 14 | avatarUrl 15 | createdAt 16 | repositories(first: 100, ownerAffiliations: OWNER) { 17 | nodes { 18 | name 19 | stargazerCount 20 | forkCount 21 | isFork 22 | description 23 | homepageUrl 24 | hasIssuesEnabled 25 | createdAt 26 | languages(first: 10) { 27 | nodes { 28 | name 29 | } 30 | } 31 | defaultBranchRef { 32 | target { 33 | ... on Commit { 34 | history { 35 | totalCount 36 | } 37 | } 38 | } 39 | } 40 | mentionableUsers(first: 10) { 41 | totalCount 42 | } 43 | } 44 | } 45 | contributionsCollection { 46 | contributionCalendar { 47 | totalContributions 48 | weeks { 49 | contributionDays { 50 | contributionCount 51 | date 52 | } 53 | } 54 | } 55 | commitContributionsByRepository { 56 | repository { 57 | name 58 | } 59 | contributions(first: 100) { 60 | totalCount 61 | nodes { 62 | occurredAt 63 | commitCount 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | ` -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Lalezar } from 'next/font/google' 3 | 4 | const lalezar = Lalezar({ 5 | subsets: ['latin'], 6 | weight: ['400', '400'], 7 | display: 'swap', 8 | }) 9 | 10 | export const metadata: Metadata = { 11 | title: "CommitWorth", 12 | description: "Descubra quanto valor você agregou ao mercado com seus commits no GitHub. Crie dashboards e cards personalizados para mostrar suas estatísticas de forma clara e profissional.", 13 | keywords: [ 14 | "commitworth", 15 | "dashboard github", 16 | "valor github", 17 | "estatísticas github", 18 | "github commits", 19 | "github stars", 20 | "desenvolvedor github", 21 | "cards github personalizados", 22 | "valor desenvolvedor", 23 | "metricas github", 24 | ], 25 | creator: "AndreVsemR", 26 | publisher: "CommitWorth", 27 | openGraph: { 28 | title: "CommitWorth - Seu Valor no GitHub em Números", 29 | description: 30 | "Veja suas estatísticas completas do GitHub e descubra quanto valor seus commits agregaram. Crie cards personalizados para compartilhar seu progresso.", 31 | url: process.env.NEXT_PUBLIC_HOST_URL, 32 | siteName: "CommitWorth", 33 | images: [ 34 | { 35 | url: "/github-logo.svg", 36 | width: 400, 37 | height: 400, 38 | alt: "CommitWorth Logo", 39 | }, 40 | ], 41 | locale: "pt_BR", 42 | type: "website", 43 | }, 44 | twitter: { 45 | card: "summary_large_image", 46 | title: "CommitWorth - Seu Valor no GitHub em Números", 47 | description: 48 | "Descubra o valor dos seus commits no GitHub, crie dashboards e cards personalizados para compartilhar com a comunidade.", 49 | creator: "@AndreVsemR", 50 | images: ["/github-logo.svg"], 51 | }, 52 | robots: { 53 | index: true, 54 | follow: true, 55 | googleBot: { 56 | index: true, 57 | follow: true, 58 | "max-video-preview": -1, 59 | "max-image-preview": "large", 60 | "max-snippet": -1, 61 | }, 62 | }, 63 | category: "Technology", 64 | applicationName: "CommitWorth", 65 | }; 66 | 67 | export default function RootLayout({ 68 | children, 69 | }: Readonly<{ 70 | children: React.ReactNode; 71 | }>) { 72 | return ( 73 | <> 74 | {children} 75 | 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /src/components/inputForm/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { useRouter } from "next/navigation"; 5 | import { toast } from "sonner"; 6 | import { Loader } from 'lucide-react' 7 | 8 | export function InputForm() { 9 | const router = useRouter() 10 | const [loading, setLoading] = React.useState(false) 11 | const [input, setInput] = React.useState("") 12 | 13 | async function setNameUser(e: React.FormEvent) { 14 | e.preventDefault() 15 | 16 | if(!input.trim()) { 17 | toast.warning("Preencha o campo com seu @nickname do github") 18 | return; 19 | } 20 | setLoading(true) 21 | const response = await fetch(`https://api.github.com/users/${input}`) 22 | if(!response || response.status === 404) { 23 | toast.warning("Usuário não encontrado, envie um usenick valido") 24 | setLoading(false) 25 | return; 26 | } 27 | 28 | router.replace(`/dashboard/${input}`) 29 | } 30 | 31 | return ( 32 |
36 | setInput(e.target.value)} 42 | /> 43 | 56 |
57 | ) 58 | } -------------------------------------------------------------------------------- /src/assets/Points_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/dashboard/components/charts/wellStructuredRepoScoresChart/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | ChartConfig, 5 | ChartContainer, 6 | ChartTooltip, 7 | ChartTooltipContent 8 | } from "@/components/ui/chart" 9 | 10 | import { Bar, BarChart, XAxis } from "recharts" 11 | 12 | import { 13 | Tooltip, 14 | TooltipContent, 15 | TooltipTrigger, 16 | } from "@/components/ui/tooltip" 17 | 18 | const chartConfig = { 19 | score: { 20 | label: "+pontos", 21 | color: "#748CAB", 22 | }, 23 | } satisfies ChartConfig 24 | 25 | interface CardProps { 26 | about?: string; 27 | title: string; 28 | value: { 29 | name: string; 30 | score: number; 31 | }[]; 32 | } 33 | 34 | export function WellStructuredRepoScoresChart({ title, value, about }: CardProps) { 35 | return ( 36 |
37 |
38 |
39 |

{title}

40 | {about && ( 41 | 42 | 43 |
44 | ! 45 |
46 |
47 | 48 |

{about}

49 |
50 |
51 | )} 52 |
53 |
54 | 55 | 56 | 57 | value.slice(0, 11)} 63 | /> 64 | } /> 65 | 66 | 67 | 68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/app/dashboard/components/cardInfoUserBigNumber/index.tsx: -------------------------------------------------------------------------------- 1 | import PointsIcon from '@/assets/Points_icon.svg' 2 | import { LucideIcon } from 'lucide-react' 3 | import Image from 'next/image'; 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipTrigger 8 | } from "@/components/ui/tooltip" 9 | 10 | // Não me orgulho desse componente :) 11 | interface CardInfoUserSmallProps { 12 | title: string; 13 | Icon?: LucideIcon; 14 | value?: number; 15 | className?: string; 16 | isPoints?: boolean; 17 | isFork?: boolean; 18 | about?: string; 19 | } 20 | 21 | export function CardInfoUserBigNumber({ title, about, Icon, isPoints, value, isFork, className }: CardInfoUserSmallProps) { 22 | return ( 23 |
24 |
25 |
26 |

{title}

27 | {about && ( 28 | 29 | 30 |
31 | ! 32 |
33 |
34 | 35 |

{about}

36 |
37 |
38 | )} 39 |
40 | 41 | {Icon && !isPoints ? ( 42 | 43 | ) : ( 44 | asd 49 | )} 50 |
51 | 52 |
53 |

56 | {!isPoints && !isFork && ( 57 | R$ 58 | )} 59 | {value && value.toLocaleString('pt-BR')} 60 |

61 |
62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/app/dashboard/components/charts/cardLanguageChart/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | ChartConfig, 5 | ChartContainer, 6 | ChartTooltip, 7 | ChartTooltipContent 8 | } from "@/components/ui/chart" 9 | 10 | import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts" 11 | 12 | import { 13 | Tooltip, 14 | TooltipContent, 15 | TooltipTrigger, 16 | } from "@/components/ui/tooltip" 17 | 18 | const chartConfig = { 19 | count: { 20 | label: "Repositórios", 21 | color: "#613DC1", 22 | }, 23 | } satisfies ChartConfig 24 | 25 | interface CardProps { 26 | about?: string; 27 | title: string; 28 | value: { 29 | language: string; 30 | count: number; 31 | }[] 32 | } 33 | 34 | export function CardLanguageChart({ title, value, about }: CardProps) { 35 | return ( 36 |
37 |
38 |
39 |

{title}

40 | {about && ( 41 | 42 | 43 |
44 | ! 45 |
46 |
47 | 48 |

{about}

49 |
50 |
51 | )} 52 |
53 |
54 | 55 | 56 | 57 | 58 | 64 | 70 | } /> 71 | 72 | 73 | 74 |
75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/app/dashboard/components/modal/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { FormEvent } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { toast } from "sonner" 6 | import { Loader } from "lucide-react" 7 | 8 | export function Modal() { 9 | const [userNick, setUserNick] = React.useState('') 10 | const [loading, setLoading] = React.useState(false) 11 | const router = useRouter() 12 | 13 | async function handleRedirect(e: React.FormEvent) { 14 | e.preventDefault() 15 | 16 | if(!userNick.trim()) { 17 | toast.warning("Preencha o campo com seu @nickname do github") 18 | return; 19 | } 20 | setLoading(true) 21 | const response = await fetch(`https://api.github.com/users/${userNick}`) 22 | if(!response || response.status === 404) { 23 | toast.warning("Usuário não encontrado, envie um usenick valido") 24 | setLoading(false) 25 | return; 26 | } 27 | 28 | router.replace(`/dashboard/${userNick}`) 29 | } 30 | 31 | return ( 32 |
33 | 34 |
35 |

Você informou seu nickname do github errado ou vazio

36 |

Precisamos do seu nickname de usuário do github, para conseguimos calcular quanto você gerou de valor, digite ele no campo abaixo 👇

37 | 38 |
42 | setUserNick(e.target.value)} 48 | /> 49 | 50 | 61 |
62 |
63 |
64 | ) 65 | } -------------------------------------------------------------------------------- /src/lib/getGithubData.ts: -------------------------------------------------------------------------------- 1 | import { graphqlClient } from "./api/graphqlClient" 2 | import { queryGitHubData } from "./api/queryGitHubData" 3 | import { 4 | GitHubCompleteData, 5 | GitHubStatsResponse, 6 | Repository, 7 | UserProps 8 | } from "@/lib/types" 9 | 10 | import { calculateAchievements } from "./calcs/calculateAchievements" 11 | import { analyzeStackAndSeniority } from "./calcs/stackAnalysis" 12 | import { formatRateLimitInfo } from "./calcs/formatRateLimitInfo" 13 | import { calculateLanguageStats } from "./calcs/calculateLanguageStats" 14 | import { calcCommitStarsForks } from "./calcs/calcCommitStarsForks" 15 | import { getPopularContributions } from "./calcs/getPopularContributions" 16 | import { identifyWellStructuredRepos } from "./calcs/identifyWellStructuredRepos" 17 | import { calculateValorAgregado } from "./calcs/calculateValorAgregado" 18 | import { calculatePontosTotais } from "./calcs/calculatePontosTotais" 19 | import { calcStructuredRepoScores } from "./calcs/calcStructuredRepoScores" 20 | 21 | async function fetchGitHubData(username: string): Promise { 22 | try { 23 | return await graphqlClient.request(queryGitHubData, { login: username }) 24 | } catch (error) { 25 | throw error 26 | } 27 | } 28 | 29 | export async function getGitHubStatsGraphQL(username: string): Promise { 30 | const data = await fetchGitHubData(username) 31 | 32 | const rateLimitInfo = formatRateLimitInfo(data.rateLimit) 33 | 34 | const userData: UserProps = { 35 | login: data.user.login, 36 | name: data.user.name, 37 | avatar_url: data.user.avatarUrl 38 | } 39 | 40 | // Filtrar repositórios (excluir forks) 41 | const repos = data.user.repositories.nodes 42 | const nonForkRepos:Repository[] = repos.filter(repo => !repo.isFork) 43 | 44 | const { totalCommits, totalStars, totalForks } = calcCommitStarsForks(data, nonForkRepos) 45 | 46 | // Calcular estatísticas de linguagens 47 | const languageRepoCount = calculateLanguageStats(nonForkRepos) 48 | 49 | // Obter contribuições populares 50 | const popularContributions = getPopularContributions(nonForkRepos) 51 | 52 | // Identificar repositórios bem estruturados 53 | const wellStructuredRepos = identifyWellStructuredRepos(nonForkRepos) 54 | 55 | // Calcular valor agregado 56 | const valorAgregado = calculateValorAgregado(totalCommits, totalStars, totalForks) 57 | 58 | // Calcular pontos totais 59 | const pontosTotais = calculatePontosTotais( 60 | totalCommits, 61 | totalStars, 62 | totalForks, 63 | wellStructuredRepos.length 64 | ) 65 | 66 | // Calcular scores dos repositórios bem estruturados 67 | const wellStructuredRepoScores = calcStructuredRepoScores(wellStructuredRepos) 68 | 69 | // Analisar stack e senioridade 70 | const stackAnalysis = analyzeStackAndSeniority(data) 71 | 72 | // Calcular conquistas passando dados pré-calculados para consistência 73 | const achievements = calculateAchievements(data, { 74 | totalCommits, 75 | totalStars, 76 | nonForkRepos, 77 | stackAnalysis 78 | }) 79 | 80 | return { 81 | userData, 82 | totalStars, 83 | totalForks, 84 | repoCountExcludingForks: nonForkRepos.length, 85 | popularContributions, 86 | wellStructuredRepoScores, 87 | totalCommits, 88 | valorAgregado, 89 | pontosTotais, 90 | languageRepoCount, 91 | rateLimitInfo, 92 | achievements, 93 | stackAnalysis 94 | } 95 | } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import { IdCard, IdCardLanyard, Gamepad, SmilePlus } from "lucide-react" 3 | import githubHero from '@/assets/github-hero.png' 4 | 5 | import { Container } from "@/components/container" 6 | import { Header } from "@/components/header" 7 | import DarkVeil from '@/components/darkVeilBG' 8 | import { InputForm } from "@/components/inputForm" 9 | import { CardInfo } from "@/components/cardInfo" 10 | import { GradientText } from "@/components/gradientText" 11 | 12 | export default function Home() { 13 | return ( 14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | Seu card 100% gratuito! 27 |
28 | 29 |

30 | Veja Quanto Você Gerou de valor com Seus Commits do github! 36 |

37 | 38 | 39 |
40 | 41 | Gihub foto 46 |
47 | 48 |
49 |

Como funciona?

50 | 51 |
52 | 57 | 58 | 63 | 64 | 69 |
70 |
71 | 72 |

Feito com ❤️ por André

73 |
74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { RateLimitInfo } from "./calcs/formatRateLimitInfo" 2 | import { StackAnalysis } from "./calcs/stackAnalysis" 3 | 4 | export interface UserProps { 5 | login: string 6 | name: string 7 | avatar_url: string 8 | } 9 | 10 | export interface GitHubStatsResponse { 11 | rateLimit: { 12 | limit: number 13 | remaining: number 14 | resetAt: string 15 | } 16 | user: { 17 | id: string 18 | login: string 19 | name: string 20 | avatarUrl: string 21 | createdAt: string 22 | repositories: { 23 | nodes: { 24 | name: string 25 | stargazerCount: number 26 | forkCount: number 27 | isFork: boolean 28 | description: string | null 29 | homepageUrl: string | null 30 | hasIssuesEnabled: boolean 31 | createdAt: string 32 | languages: { 33 | nodes: { 34 | name: string 35 | }[] 36 | } 37 | defaultBranchRef: { 38 | target: { 39 | history: { 40 | totalCount: number 41 | } 42 | } 43 | } | null 44 | mentionableUsers: { 45 | totalCount: number 46 | } 47 | }[] 48 | } 49 | contributionsCollection: { 50 | contributionCalendar: { 51 | totalContributions: number 52 | weeks: { 53 | contributionDays: { 54 | contributionCount: number 55 | date: string 56 | }[] 57 | }[] 58 | } 59 | commitContributionsByRepository: { 60 | repository: { 61 | name: string 62 | } 63 | contributions: { 64 | totalCount: number 65 | nodes: { 66 | occurredAt: string 67 | commitCount: number 68 | }[] 69 | } 70 | }[] 71 | } 72 | } 73 | } 74 | 75 | export interface Repository { 76 | name: string 77 | stargazerCount: number 78 | forkCount: number 79 | isFork: boolean 80 | description: string | null 81 | homepageUrl: string | null 82 | hasIssuesEnabled: boolean 83 | createdAt: string 84 | languages: { 85 | nodes: { 86 | name: string 87 | }[] 88 | } 89 | defaultBranchRef: { 90 | target: { 91 | history: { 92 | totalCount: number 93 | } 94 | } 95 | } | null 96 | mentionableUsers: { 97 | totalCount: number 98 | } 99 | } 100 | 101 | export interface WellStructuredRepo { 102 | name: string 103 | description: string | null 104 | homepageUrl: string | null 105 | stars: number 106 | forks: number 107 | mainLanguage?: string 108 | } 109 | 110 | export interface GitHubCompleteData { 111 | userData: UserProps 112 | totalStars: number 113 | totalForks: number 114 | repoCountExcludingForks: number 115 | popularContributions: { 116 | name: string 117 | stars: number 118 | }[] 119 | wellStructuredRepoScores: { 120 | name: string 121 | score: number 122 | }[] 123 | totalCommits: number 124 | valorAgregado: number 125 | pontosTotais: number 126 | languageRepoCount: { 127 | language: string 128 | count: number 129 | }[] 130 | rateLimitInfo: RateLimitInfo 131 | achievements: Achievement[] 132 | stackAnalysis: StackAnalysis 133 | } 134 | 135 | export interface Achievement { 136 | id: string; 137 | name: string; 138 | description: string; 139 | completed: boolean; 140 | progress?: number; 141 | maxProgress?: number; 142 | } 143 | 144 | export interface PreCalculatedData { 145 | totalCommits: number; 146 | totalStars: number; 147 | nonForkRepos: any[]; 148 | stackAnalysis?: StackAnalysis; 149 | } -------------------------------------------------------------------------------- /src/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from 'react' 4 | import Link from 'next/link' 5 | import { Logo } from '../logo' 6 | import { toast } from 'sonner' 7 | 8 | export function Header({ isDashboard }: { isDashboard?: boolean }) { 9 | const [isVisible, setIsVisible] = React.useState(false) 10 | 11 | return ( 12 | <> 13 |
14 |
15 | 16 | 17 | {isDashboard && ( 18 | 19 | Como são calculados os valores? 20 | 21 | )} 22 | {!isDashboard && ( 23 | <> 24 | 43 | 44 | 45 | 65 | 66 | )} 67 |
68 |
69 | 70 | {!isDashboard && ( 71 | 92 | )} 93 | 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/app/dashboard/components/modalShareCard/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { LinkedinIcon, TwitterIcon, FacebookIcon, } from 'lucide-react' 4 | import { Dispatch, SetStateAction } from 'react'; 5 | 6 | export function ModalShareCard({ setShowModal, nickname }: { 7 | setShowModal: Dispatch> 8 | nickname: string 9 | }) { 10 | const hostUrl = `${process.env.NEXT_PUBLIC_HOST_URL}`; 11 | const profileUrl = `${hostUrl}/dashboard/${nickname}`; 12 | 13 | const encodedUrl = encodeURIComponent(profileUrl); 14 | const text = encodeURIComponent(`Veja ${nickname} na CommitWorth`); 15 | 16 | const shareLinks = { 17 | linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`, 18 | twitter: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${text}`, 19 | facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, 20 | whatsapp: `https://api.whatsapp.com/send?text=${text}%20${encodedUrl}`, 21 | telegram: `https://t.me/share/url?url=${encodedUrl}&text=${text}` 22 | }; 23 | 24 | return ( 25 |
26 |
setShowModal(false)}>
27 | 28 |
29 |

30 | Compartilhe seu card em suas redes sociais! 31 |

32 |

Mostre para as pessoas o quanto você já agregou com suas contribuições

33 | 34 |
35 | 42 | 43 | 44 | 45 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 65 | 72 | 73 | 74 | 75 | 82 | 83 | 84 |
85 |
86 |
87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | body { 7 | background-color: #0D1321; 8 | } 9 | 10 | button { 11 | cursor: pointer; 12 | } 13 | 14 | ::-webkit-scrollbar { 15 | display: none; 16 | } 17 | 18 | html { 19 | -ms-overflow-style: none; 20 | scrollbar-width: none; 21 | } 22 | 23 | @keyframes pulse-scale { 24 | 0%, 100% { 25 | transform: scale(1); 26 | } 27 | 50% { 28 | transform: scale(1.08); 29 | } 30 | } 31 | 32 | .pulse-scale { 33 | animation: pulse-scale 1.2s ease-in-out infinite; 34 | } 35 | 36 | 37 | @theme { 38 | --font-lalezar: "Lalezar"; 39 | --font-inter: "inter"; 40 | 41 | --color-primarydark: #0D1321; 42 | --color-primaryblue: #1D2D44; 43 | --color-primarymediumblue: #3E5C76; 44 | --color-primarylightblue: #748CAB; 45 | --color-primarybege: #F0EBD8; 46 | 47 | --color-secondarygray: #C8C8C8; 48 | --color-secondarypurple: #613DC1; 49 | --color-secondarygreen: #32E875; 50 | --color-secondaryyellow: #E3B341; 51 | 52 | --animation-gradient: gradient 8s linear infinite; 53 | } 54 | 55 | @keyframes gradient { 56 | 0% { 57 | background-position: 0% 50%; 58 | } 59 | 50% { 60 | background-position: 100% 50%; 61 | } 62 | 100% { 63 | background-position: 0% 50%; 64 | } 65 | } 66 | 67 | .animate-gradient { 68 | animation: var(--animation-gradient); 69 | } 70 | 71 | @theme inline { 72 | --radius-sm: calc(var(--radius) - 4px); 73 | --radius-md: calc(var(--radius) - 2px); 74 | --radius-lg: var(--radius); 75 | --radius-xl: calc(var(--radius) + 4px); 76 | --color-background: var(--background); 77 | --color-foreground: var(--foreground); 78 | --color-card: var(--card); 79 | --color-card-foreground: var(--card-foreground); 80 | --color-popover: var(--popover); 81 | --color-popover-foreground: var(--popover-foreground); 82 | --color-primary: var(--primary); 83 | --color-primary-foreground: var(--primary-foreground); 84 | --color-secondary: var(--secondary); 85 | --color-secondary-foreground: var(--secondary-foreground); 86 | --color-muted: var(--muted); 87 | --color-muted-foreground: var(--muted-foreground); 88 | --color-accent: var(--accent); 89 | --color-accent-foreground: var(--accent-foreground); 90 | --color-destructive: var(--destructive); 91 | --color-border: var(--border); 92 | --color-input: var(--input); 93 | --color-ring: var(--ring); 94 | --color-chart-1: var(--chart-1); 95 | --color-chart-2: var(--chart-2); 96 | --color-chart-3: var(--chart-3); 97 | --color-chart-4: var(--chart-4); 98 | --color-chart-5: var(--chart-5); 99 | --color-sidebar: var(--sidebar); 100 | --color-sidebar-foreground: var(--sidebar-foreground); 101 | --color-sidebar-primary: var(--sidebar-primary); 102 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 103 | --color-sidebar-accent: var(--sidebar-accent); 104 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 105 | --color-sidebar-border: var(--sidebar-border); 106 | --color-sidebar-ring: var(--sidebar-ring); 107 | } 108 | 109 | :root { 110 | --radius: 0.625rem; 111 | --background: oklch(1 0 0); 112 | --foreground: oklch(0.129 0.042 264.695); 113 | --card: oklch(1 0 0); 114 | --card-foreground: oklch(0.129 0.042 264.695); 115 | --popover: oklch(1 0 0); 116 | --popover-foreground: oklch(0.129 0.042 264.695); 117 | --primary: oklch(0.208 0.042 265.755); 118 | --primary-foreground: oklch(0.984 0.003 247.858); 119 | --secondary: oklch(0.968 0.007 247.896); 120 | --secondary-foreground: oklch(0.208 0.042 265.755); 121 | --muted: oklch(0.968 0.007 247.896); 122 | --muted-foreground: oklch(0.554 0.046 257.417); 123 | --accent: oklch(0.968 0.007 247.896); 124 | --accent-foreground: oklch(0.208 0.042 265.755); 125 | --destructive: oklch(0.577 0.245 27.325); 126 | --border: oklch(0.929 0.013 255.508); 127 | --input: oklch(0.929 0.013 255.508); 128 | --ring: oklch(0.704 0.04 256.788); 129 | --chart-1: oklch(0.646 0.222 41.116); 130 | --chart-2: oklch(0.6 0.118 184.704); 131 | --chart-3: oklch(0.398 0.07 227.392); 132 | --chart-4: oklch(0.828 0.189 84.429); 133 | --chart-5: oklch(0.769 0.188 70.08); 134 | --sidebar: oklch(0.984 0.003 247.858); 135 | --sidebar-foreground: oklch(0.129 0.042 264.695); 136 | --sidebar-primary: oklch(0.208 0.042 265.755); 137 | --sidebar-primary-foreground: oklch(0.984 0.003 247.858); 138 | --sidebar-accent: oklch(0.968 0.007 247.896); 139 | --sidebar-accent-foreground: oklch(0.208 0.042 265.755); 140 | --sidebar-border: oklch(0.929 0.013 255.508); 141 | --sidebar-ring: oklch(0.704 0.04 256.788); 142 | } 143 | 144 | .dark { 145 | --background: oklch(0.129 0.042 264.695); 146 | --foreground: oklch(0.984 0.003 247.858); 147 | --card: oklch(0.208 0.042 265.755); 148 | --card-foreground: oklch(0.984 0.003 247.858); 149 | --popover: oklch(0.208 0.042 265.755); 150 | --popover-foreground: oklch(0.984 0.003 247.858); 151 | --primary: oklch(0.929 0.013 255.508); 152 | --primary-foreground: oklch(0.208 0.042 265.755); 153 | --secondary: oklch(0.279 0.041 260.031); 154 | --secondary-foreground: oklch(0.984 0.003 247.858); 155 | --muted: oklch(0.279 0.041 260.031); 156 | --muted-foreground: oklch(0.704 0.04 256.788); 157 | --accent: oklch(0.279 0.041 260.031); 158 | --accent-foreground: oklch(0.984 0.003 247.858); 159 | --destructive: oklch(0.704 0.191 22.216); 160 | --border: oklch(1 0 0 / 10%); 161 | --input: oklch(1 0 0 / 15%); 162 | --ring: oklch(0.551 0.027 264.364); 163 | --chart-1: oklch(0.488 0.243 264.376); 164 | --chart-2: oklch(0.696 0.17 162.48); 165 | --chart-3: oklch(0.769 0.188 70.08); 166 | --chart-4: oklch(0.627 0.265 303.9); 167 | --chart-5: oklch(0.645 0.246 16.439); 168 | --sidebar: oklch(0.208 0.042 265.755); 169 | --sidebar-foreground: oklch(0.984 0.003 247.858); 170 | --sidebar-primary: oklch(0.488 0.243 264.376); 171 | --sidebar-primary-foreground: oklch(0.984 0.003 247.858); 172 | --sidebar-accent: oklch(0.279 0.041 260.031); 173 | --sidebar-accent-foreground: oklch(0.984 0.003 247.858); 174 | --sidebar-border: oklch(1 0 0 / 10%); 175 | --sidebar-ring: oklch(0.551 0.027 264.364); 176 | } 177 | 178 | @layer base { 179 | * { 180 | @apply border-border outline-ring/50; 181 | } 182 | body { 183 | @apply bg-background text-foreground; 184 | } 185 | } -------------------------------------------------------------------------------- /src/app/dashboard/components/stackAnalysisCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { Medal, Code, TrendingUp, Clock } from 'lucide-react' 2 | import { StackAnalysis } from '@/lib/calcs/stackAnalysis' 3 | 4 | interface StackAnalysisCardProps { 5 | stackAnalysis: StackAnalysis 6 | className?: string 7 | } 8 | 9 | export function StackAnalysisCard({ stackAnalysis, className }: StackAnalysisCardProps) { 10 | const getSeniorityColor = (level: string) => { 11 | switch (level) { 12 | case 'Tech Lead': return 'text-secondaryyellow' 13 | case 'Senior': return 'text-secondarygreen' 14 | case 'Pleno': return 'text-primarylightblue' 15 | case 'Junior': return 'text-secondarygray' 16 | default: return 'text-secondarygray' 17 | } 18 | } 19 | 20 | const getSeniorityIcon = () => { 21 | switch (stackAnalysis.seniorityLevel) { 22 | case 'Tech Lead': return 23 | case 'Senior': return 24 | case 'Pleno': return 25 | case 'Junior': return 26 | default: return 27 | } 28 | } 29 | 30 | return ( 31 |
32 |
33 |
34 |

Análise de Stack & Senioridade

35 |
36 | 37 |
38 | {getSeniorityIcon()} 39 |
40 |
41 | 42 |
43 |
44 | 45 | {/* Stack Principal */} 46 |
47 |

48 | 49 | Stack Principal 50 |

51 |

{stackAnalysis.primaryStack}

52 |

53 | {stackAnalysis.stackSummary.primaryLanguagePercentage}% dos seus repositórios 54 |

55 |
56 | 57 | {/* Nível de Senioridade */} 58 |
59 |

60 | 61 | Nível de Senioridade 62 |

63 |

64 | {stackAnalysis.seniorityLevel} 65 |

66 |

67 | Score: {stackAnalysis.seniorityScore}/100 68 |

69 |
70 | 71 | {/* Resumo da Experiência */} 72 |
73 |

74 | 75 | Experiência 76 |

77 |
78 |

79 | Tempo de experiência:
80 | 81 | {stackAnalysis.stackSummary.experienceRange} 82 | 83 |

84 |

85 | Linguagens dominadas: 86 | 87 | {stackAnalysis.stackSummary.totalLanguages} 88 | 89 |

90 |
91 |
92 |
93 | 94 | {/* Top 3 Linguagens */} 95 | {stackAnalysis.stackExperience.length > 0 && ( 96 |
97 |

Top Linguagens

98 |
99 | {stackAnalysis.stackExperience.slice(0, 3).map((exp, index) => ( 100 |
101 |
102 | 103 | #{index + 1} 104 | 105 | {exp.language} 106 |
107 |
108 |

109 | {exp.repositories} repos 110 |

111 |

112 | {exp.yearsOfExperience} {exp.yearsOfExperience === 1 ? 'ano' : 'anos'} 113 |

114 |
115 |
116 | ))} 117 |
118 |
119 | )} 120 |
121 |
122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/lib/calcs/calculateAchievements.ts: -------------------------------------------------------------------------------- 1 | import { ACHIEVEMENT_THRESHOLDS } from "@/constants/valuesConfig"; 2 | import { Achievement, GitHubStatsResponse, PreCalculatedData } from "../types"; 3 | 4 | function calculateAccountAge(createdAt: string): number { 5 | try { 6 | const accountCreated = new Date(createdAt) 7 | const now = new Date() 8 | 9 | // Validar se a data é válida 10 | if (isNaN(accountCreated.getTime())) { 11 | console.warn('Data de criação da conta inválida:', createdAt) 12 | return 0 13 | } 14 | 15 | return (now.getTime() - accountCreated.getTime()) / (1000 * 60 * 60 * 24 * 365.25) 16 | } catch (error) { 17 | console.warn('Erro ao calcular idade da conta:', error) 18 | return 0 19 | } 20 | } 21 | 22 | function getUniqueLanguages(repos: any[]): Set { 23 | const uniqueLanguages = new Set() 24 | 25 | repos.forEach(repo => { 26 | if (repo.languages?.nodes) { 27 | repo.languages.nodes.forEach((lang: { name: string }) => { 28 | if (lang.name) { 29 | uniqueLanguages.add(lang.name) 30 | } 31 | }) 32 | } 33 | }) 34 | 35 | return uniqueLanguages 36 | } 37 | 38 | function getMaxStarsInRepo(repos: any[]): number { 39 | if (repos.length === 0) return 0 40 | 41 | return Math.max(...repos.map(repo => repo.stargazerCount ?? 0)) 42 | } 43 | 44 | export function calculateAchievements( 45 | data: GitHubStatsResponse, 46 | preCalculated?: PreCalculatedData 47 | ): Achievement[] { 48 | const achievements: Achievement[] = [] 49 | 50 | // Dados do usuário e repositórios 51 | const user = data.user 52 | const repos = preCalculated?.nonForkRepos ?? user.repositories.nodes.filter(repo => !repo.isFork) 53 | 54 | // Usar dados pré-calculados quando disponíveis para manter consistência 55 | const totalCommits = preCalculated?.totalCommits ?? repos.reduce((total, repo) => { 56 | return total + (repo.defaultBranchRef?.target?.history?.totalCount ?? 0) 57 | }, 0) 58 | 59 | const totalStars = preCalculated?.totalStars ?? repos.reduce((total, repo) => { 60 | return total + (repo.stargazerCount ?? 0) 61 | }, 0) 62 | 63 | // Calcular estatísticas derivadas 64 | const uniqueLanguages = getUniqueLanguages(repos) 65 | const maxStarsInRepo = getMaxStarsInRepo(repos) 66 | const accountAgeYears = user.createdAt ? calculateAccountAge(user.createdAt) : 0 67 | 68 | // Code Warrior (1000+ commits) 69 | achievements.push({ 70 | id: "code_warrior", 71 | name: "Code Warrior", 72 | description: "Fez mais de 1.000 commits no total", 73 | completed: totalCommits >= ACHIEVEMENT_THRESHOLDS.CODE_WARRIOR_COMMITS, 74 | progress: Math.min(totalCommits, ACHIEVEMENT_THRESHOLDS.CODE_WARRIOR_COMMITS), 75 | maxProgress: ACHIEVEMENT_THRESHOLDS.CODE_WARRIOR_COMMITS 76 | }) 77 | 78 | // Império do Código (50+ repositórios) 79 | achievements.push({ 80 | id: "code_empire", 81 | name: "Império do Código", 82 | description: "Criou mais de 50 repositórios públicos", 83 | completed: repos.length >= ACHIEVEMENT_THRESHOLDS.CODE_EMPIRE_REPOS, 84 | progress: Math.min(repos.length, ACHIEVEMENT_THRESHOLDS.CODE_EMPIRE_REPOS), 85 | maxProgress: ACHIEVEMENT_THRESHOLDS.CODE_EMPIRE_REPOS 86 | }) 87 | 88 | // Arquiteto do GitHub (10+ linguagens) 89 | const languageCount = uniqueLanguages.size 90 | achievements.push({ 91 | id: "github_architect", 92 | name: "Arquiteto do GitHub", 93 | description: "Criou repositórios com 10+ linguagens diferentes", 94 | completed: languageCount >= ACHIEVEMENT_THRESHOLDS.GITHUB_ARCHITECT_LANGUAGES, 95 | progress: Math.min(languageCount, ACHIEVEMENT_THRESHOLDS.GITHUB_ARCHITECT_LANGUAGES), 96 | maxProgress: ACHIEVEMENT_THRESHOLDS.GITHUB_ARCHITECT_LANGUAGES 97 | }) 98 | 99 | // Estrela do GitHub (100+ estrelas total) 100 | achievements.push({ 101 | id: "github_star", 102 | name: "Estrela do GitHub", 103 | description: "Recebeu mais de 100 estrelas no total", 104 | completed: totalStars >= ACHIEVEMENT_THRESHOLDS.GITHUB_STAR_TOTAL, 105 | progress: Math.min(totalStars, ACHIEVEMENT_THRESHOLDS.GITHUB_STAR_TOTAL), 106 | maxProgress: ACHIEVEMENT_THRESHOLDS.GITHUB_STAR_TOTAL 107 | }) 108 | 109 | // Projeto de Ouro (500+ estrelas em um repo) 110 | achievements.push({ 111 | id: "golden_project", 112 | name: "Projeto de Ouro", 113 | description: "Tem um repositório com 500+ estrelas", 114 | completed: maxStarsInRepo >= ACHIEVEMENT_THRESHOLDS.GOLDEN_PROJECT_STARS, 115 | progress: Math.min(maxStarsInRepo, ACHIEVEMENT_THRESHOLDS.GOLDEN_PROJECT_STARS), 116 | maxProgress: ACHIEVEMENT_THRESHOLDS.GOLDEN_PROJECT_STARS 117 | }) 118 | 119 | // Veterano do Código (10+ anos no GitHub) 120 | const veteranProgress = Math.floor(Math.min(accountAgeYears, ACHIEVEMENT_THRESHOLDS.CODE_VETERAN_YEARS)) 121 | achievements.push({ 122 | id: "code_veteran", 123 | name: "Veterano do Código", 124 | description: "Mais de 10 anos de GitHub", 125 | completed: accountAgeYears >= ACHIEVEMENT_THRESHOLDS.CODE_VETERAN_YEARS, 126 | progress: veteranProgress, 127 | maxProgress: ACHIEVEMENT_THRESHOLDS.CODE_VETERAN_YEARS 128 | }) 129 | 130 | // GitHub Old School (5+ anos no GitHub) 131 | const oldSchoolProgress = Math.floor(Math.min(accountAgeYears, ACHIEVEMENT_THRESHOLDS.GITHUB_OLD_SCHOOL_YEARS)) 132 | achievements.push({ 133 | id: "github_old_school", 134 | name: "GitHub Old School", 135 | description: "Conta criada há mais de 5 anos", 136 | completed: accountAgeYears >= ACHIEVEMENT_THRESHOLDS.GITHUB_OLD_SCHOOL_YEARS, 137 | progress: oldSchoolProgress, 138 | maxProgress: ACHIEVEMENT_THRESHOLDS.GITHUB_OLD_SCHOOL_YEARS 139 | }) 140 | 141 | // Achievements relacionados à Stack Analysis 142 | if (preCalculated?.stackAnalysis) { 143 | const stackAnalysis = preCalculated.stackAnalysis 144 | 145 | // Stack Specialist (Score 70+) 146 | achievements.push({ 147 | id: "stack_specialist", 148 | name: "Especialista de Stack", 149 | description: "Atingiu senioridade avançada na sua stack principal", 150 | completed: stackAnalysis.seniorityScore >= ACHIEVEMENT_THRESHOLDS.STACK_SPECIALIST_SCORE, 151 | progress: Math.min(stackAnalysis.seniorityScore, ACHIEVEMENT_THRESHOLDS.STACK_SPECIALIST_SCORE), 152 | maxProgress: ACHIEVEMENT_THRESHOLDS.STACK_SPECIALIST_SCORE 153 | }) 154 | 155 | // Tech Leader 156 | achievements.push({ 157 | id: "tech_leader", 158 | name: "Líder Técnico", 159 | description: "Alcançou o nível Tech Lead de senioridade", 160 | completed: stackAnalysis.seniorityLevel === ACHIEVEMENT_THRESHOLDS.TECH_LEADER_LEVEL, 161 | progress: stackAnalysis.seniorityLevel === ACHIEVEMENT_THRESHOLDS.TECH_LEADER_LEVEL ? 1 : 0, 162 | maxProgress: 1 163 | }) 164 | 165 | // Senior Developer 166 | achievements.push({ 167 | id: "senior_developer", 168 | name: "Desenvolvedor Sênior", 169 | description: "Atingiu o nível Sênior ou superior", 170 | completed: ['Senior', 'Tech Lead'].includes(stackAnalysis.seniorityLevel), 171 | progress: ['Senior', 'Tech Lead'].includes(stackAnalysis.seniorityLevel) ? 1 : 0, 172 | maxProgress: 1 173 | }) 174 | 175 | // Polyglot (15+ linguagens) 176 | achievements.push({ 177 | id: "polyglot", 178 | name: "Poliglota", 179 | description: "Domina 15 ou mais linguagens de programação", 180 | completed: stackAnalysis.stackSummary.totalLanguages >= ACHIEVEMENT_THRESHOLDS.POLYGLOT_LANGUAGES, 181 | progress: Math.min(stackAnalysis.stackSummary.totalLanguages, ACHIEVEMENT_THRESHOLDS.POLYGLOT_LANGUAGES), 182 | maxProgress: ACHIEVEMENT_THRESHOLDS.POLYGLOT_LANGUAGES 183 | }) 184 | } 185 | 186 | return achievements 187 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Lalezar, Inter } from 'next/font/google' 3 | import { Toaster } from "sonner"; 4 | import "./globals.css"; 5 | 6 | const lalezar = Lalezar({ 7 | subsets: ['latin'], 8 | weight: ['400', '400'], 9 | display: 'swap', 10 | }) 11 | 12 | const inter = Inter({ 13 | subsets: ['latin'], 14 | weight: ['400', '900'], 15 | display: 'swap', 16 | }) 17 | 18 | export const metadata: Metadata = { 19 | metadataBase: new URL(process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app' as string), 20 | title: { 21 | default: "CommitWorth - Dashboard GitHub com Valor Agregado e Pontuação", 22 | template: "%s | CommitWorth - Dashboard GitHub Completo", 23 | }, 24 | description: "🚀 Descubra o valor real dos seus commits na CommitWorth! Dashboard completo com estatísticas, pontuação gamificada, distintivos e geração de cards exclusivos. Análise gratuita de repositórios públicos.", 25 | keywords: [ 26 | // Palavras-chave primárias (alta relevância) 27 | "github dashboard", 28 | "github stats", 29 | "github estatísticas", 30 | "valor commits github", 31 | "github card generator", 32 | 33 | // Palavras-chave secundárias (média relevância) 34 | "dashboard desenvolvedor", 35 | "github analytics", 36 | "github metrics", 37 | "commits calculator", 38 | "github profile card", 39 | "estatísticas programador", 40 | "github achievements", 41 | "distintivos github", 42 | 43 | // Long-tail keywords (baixa concorrência, alta conversão) 44 | "calcular valor commits github", 45 | "gerar card github personalizado", 46 | "dashboard github gratuito", 47 | "analisar perfil github", 48 | "pontuação github commits", 49 | "commits worth calculator", 50 | "github contribution value", 51 | "portfolio desenvolvedor github", 52 | "github profile analytics", 53 | "métricas repositório github", 54 | 55 | // Termos relacionados ao público-alvo 56 | "desenvolvedor", 57 | "programador", 58 | "open source", 59 | "contribuições github", 60 | "repositórios públicos", 61 | "linguagens programação", 62 | "commits", 63 | "estrelas github", 64 | "forks github" 65 | ], 66 | authors: [{ 67 | name: "André Luiz", 68 | url: "https://github.com/andreluizdasilvaa/CommitWorth" 69 | }], 70 | creator: "André Luiz", 71 | publisher: "CommitWorth", 72 | category: "Technology", 73 | applicationName: "CommitWorth", 74 | 75 | openGraph: { 76 | title: "CommitWorth - Dashboard Completo com Valor Agregado 🚀", 77 | description: "💰 Descubra quanto valor você já agregou com seus commits! Dashboard gratuito com estatísticas completas, pontuação gamificada e geração de cards exclusivos. Análise de repositórios públicos do GitHub.", 78 | url: process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app', 79 | siteName: "CommitWorth - Dashboard", 80 | images: [ 81 | { 82 | url: "/github-hero.png", 83 | width: 401, 84 | height: 401, 85 | alt: "CommitWorth - Análise GitHub com valor agregado, commits e distintivos" 86 | } 87 | ], 88 | locale: "pt_BR", 89 | type: "website", 90 | countryName: "Brasil" 91 | }, 92 | 93 | twitter: { 94 | card: "summary_large_image", 95 | title: "CommitWorth - Valorize seus commits no GitHub! 💻✨", 96 | description: "🎯 Dashboard completo + Card personalizado + Distintivos exclusivos. Descubra o valor real dos seus commits agora!", 97 | images: ["/github-hero.png"], 98 | creator: "@andreVsemR", 99 | site: process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app' 100 | }, 101 | 102 | robots: { 103 | index: true, 104 | follow: true, 105 | noarchive: false, 106 | nosnippet: false, 107 | noimageindex: false, 108 | nocache: false, 109 | googleBot: { 110 | index: true, 111 | follow: true, 112 | noimageindex: false, 113 | 'max-video-preview': -1, 114 | 'max-image-preview': 'large', 115 | 'max-snippet': -1, 116 | }, 117 | }, 118 | icons: { 119 | icon: [ 120 | { url: "/github-logo.svg", type: "image/svg+xml", sizes: "any" } 121 | ], 122 | shortcut: "/github-logo.svg", 123 | apple: [ 124 | { url: "/github-hero.png", sizes: "180x180", type: "image/png" } 125 | ] 126 | }, 127 | 128 | alternates: { 129 | canonical: process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app', 130 | languages: { 131 | 'pt-BR': process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app', 132 | 'x-default': process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app' 133 | } 134 | }, 135 | 136 | other: { 137 | 'application/ld+json': JSON.stringify({ 138 | '@context': 'https://schema.org', 139 | '@type': 'WebApplication', 140 | name: 'CommitWorth', 141 | applicationCategory: 'DeveloperApplication', 142 | operatingSystem: 'Web Browser', 143 | description: 'Dashboard completo para desenvolvedores visualizarem estatísticas do GitHub, calcularem valor agregado dos commits e gerarem cards personalizados para compartilhamento.', 144 | url: process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app', 145 | author: { 146 | '@type': 'Person', 147 | name: 'André Luiz', 148 | url: 'https://github.com/andreluizdasilvaa', 149 | sameAs: [ 150 | 'https://github.com/andreluizdasilvaa', 151 | 'https://x.com/andreVsemR' 152 | ] 153 | }, 154 | creator: { 155 | '@type': 'Person', 156 | name: 'André Luiz' 157 | }, 158 | offers: { 159 | '@type': 'Offer', 160 | price: '0', 161 | priceCurrency: 'BRL', 162 | description: 'Dashboard gratuito do CommitWorth com análise completa de commits, cálculo de valor agregado e geração de cards personalizados' 163 | }, 164 | downloadUrl: process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app', 165 | screenshot: `${process.env.NEXT_PUBLIC_HOST_URL || 'https://commitworth.vercel.app'}/github-hero.png`, 166 | releaseNotes: 'Dashboard CommitWorth com cálculo de valor agregado, sistema de pontuação e distintivos de conquista', 167 | featureList: [ 168 | 'Análise completa de repositórios GitHub', 169 | 'Cálculo de valor agregado por commits', 170 | 'Sistema de pontuação gamificado', 171 | 'Distintivos de conquista exclusivos', 172 | 'Geração de cards personalizados', 173 | 'Compartilhamento para LinkedIn', 174 | 'Análise de linguagens de programação', 175 | 'Estatísticas de estrelas e forks' 176 | ], 177 | audience: { 178 | '@type': 'Audience', 179 | audienceType: 'Desenvolvedores de Software' 180 | } 181 | }) 182 | } 183 | }; 184 | 185 | export default function RootLayout({ 186 | children, 187 | }: Readonly<{ 188 | children: React.ReactNode; 189 | }>) { 190 | return ( 191 | 192 | 193 | {/* DNS prefetch for external resources */} 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 219 | 220 | {children} 221 | 222 | 223 | ); 224 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommitWorth 2 | 3 | ## Sumário 4 | 5 | 1. [Descrição Geral](#descrição-geral) 6 | 2. [Funcionamento](#funcionamento) 7 | 3. [Métricas do Dashboard](#métricas-do-dashboard) 8 | 4. [Análise de Stack & Senioridade](#análise-de-stack--senioridade) 9 | 5. [Distintivos de Conquista](#distintivos-de-conquista) 10 | 6. [Geração de Card Personalizado](#geração-de-card-personalizado) 11 | 7. [Contribuição](#contribuição) 12 | 13 | --- 14 | 15 | ## 🟢 Descrição Geral 16 | 17 | O **CommitWorth** é uma plataforma gamificada que calcula o "valor agregado" do trabalho de desenvolvedores utilizando dados públicos do GitHub. Basta informar um username válido para acessar um dashboard exclusivo com diversas métricas, análise de stack tecnológica, detecção de senioridade e conquistas. 18 | 19 | **✨ Principais Funcionalidades:** 20 | - 📊 **Análise completa** de repositórios e atividade no GitHub 21 | - 🎯 **Detecção automática** da stack principal do desenvolvedor 22 | - 🏆 **Classificação de senioridade** em 4 níveis (Junior → Tech Lead) 23 | - 💰 **Cálculo de valor agregado** baseado em contribuições 24 | - 🏅 **Sistema de achievements** com distintivos exclusivos 25 | - 🖼️ **Geração de cards** personalizados para compartilhamento 26 | 27 | - **Entrada:** Username do GitHub. 28 | - **Redirecionamento:** 29 | - Username válido: `/dashboard/` 30 | - Username inválido/inexistente: Página `not-found` com formulário para correção. 31 | 32 | --- 33 | 34 | ## ⚙️ Funcionamento 35 | 36 | 1. O usuário informa seu username do GitHub. 37 | 2. O sistema coleta dados públicos via API do GitHub. 38 | 3. **As métricas são processadas e a análise de stack é realizada:** 39 | - Identificação da linguagem/stack principal 40 | - Cálculo do score de senioridade 41 | - Análise de indicadores de complexidade 42 | - Determinação do nível de experiência 43 | 4. **Os dados são exibidos no dashboard** com métricas, gráficos e análises. 44 | 5. **O usuário pode gerar um card personalizado** com suas conquistas e stack. 45 | 6. Caso o username não exista, o usuário pode corrigir e tentar novamente. 46 | 47 | --- 48 | 49 | ## 🧠 Métricas do Dashboard 50 | 51 | O dashboard apresenta as seguintes métricas: 52 | 53 | - **Total de Estrelas:** Soma das estrelas em todos os repositórios públicos. 54 | - **Total de Repositórios:** Quantidade de repositórios públicos. 55 | - **Total de Commits:** Total de commits somados de todos os repositórios. 56 | - **Valor Agregado:** Valor fictício calculado com base na atividade no GitHub. 57 | 58 | ### Tabela de Valores 59 | 60 | | Tipo | Valor | 61 | |--------|---------| 62 | | Commit | R$2,00 | 63 | | Estrela| R$0,50 | 64 | | Fork | R$1,00 | 65 | 66 | - **Linguagens mais utilizadas:** Linguagens predominantes nos repositórios. 67 | - **Pontos do usuário:** Calculados conforme critérios abaixo: 68 | 69 | | Tipo | Pontos | 70 | |---------------------------|--------| 71 | | Commit | 1 | 72 | | Estrela | 5 | 73 | | Fork | 3 | 74 | | Repositório bem estruturado| 10 | 75 | 76 | - **Repositórios bem estruturados:** Repositórios que possuem descrição, página inicial e issues habilitadas. 77 | - **Total de Forks:** Soma dos forks dos repositórios públicos. 78 | 79 | --- 80 | 81 | ## 🎯 Análise de Stack & Senioridade 82 | 83 | O **CommitWorth** possui um sistema inteligente de análise que determina automaticamente a **stack principal** do desenvolvedor e seu **nível de senioridade** baseado em múltiplos fatores. 84 | 85 | ### 📊 Detecção de Stack Principal 86 | 87 | O sistema analisa todos os repositórios públicos e identifica a linguagem mais utilizada, mapeando-a para categorias de stack: 88 | 89 | | Linguagem | Stack Identificada | 90 | |-----------|-------------------| 91 | | JavaScript, TypeScript, React, Vue, Angular | Frontend/Fullstack | 92 | | Python | Backend/Data Science | 93 | | Java | Backend/Enterprise | 94 | | C# | Backend/.NET | 95 | | Go | Backend/Cloud | 96 | | Swift | Mobile/iOS | 97 | | Kotlin | Mobile/Android | 98 | | Rust, C++, C | Systems/Performance | 99 | | PHP, Ruby | Backend/Web | 100 | | R, MATLAB | Data Science | 101 | | Shell, Docker | DevOps/Infrastructure | 102 | 103 | ### 🏆 Níveis de Senioridade 104 | 105 | O sistema classifica desenvolvedores em **4 níveis** baseado em um score de 0-100: 106 | 107 | #### **Junior (0-34 pontos)** 108 | - Menos de 3 anos de experiência 109 | - Poucos projetos ou sem indicadores de complexidade 110 | - Foco em aprendizado e projetos pessoais 111 | 112 | #### **Pleno (35-59 pontos)** 113 | - 3-5 anos de experiência 114 | - Projetos com alguma complexidade 115 | - Início de contribuições colaborativas 116 | 117 | #### **Senior (60-79 pontos)** 118 | - 5-8 anos de experiência 119 | - Projetos complexos e bem documentados 120 | - Colaboração ativa e projetos populares 121 | 122 | #### **Tech Lead (80-100 pontos)** 123 | - 8+ anos de experiência 124 | - Múltiplos projetos complexos e populares 125 | - Liderança técnica evidente pelos projetos 126 | 127 | ### 🧮 Cálculo do Score de Senioridade 128 | 129 | O score é calculado através de uma fórmula ponderada considerando: 130 | 131 | | Fator | Peso | Descrição | 132 | |-------|------|-----------| 133 | | **Anos de Experiência** | 30% | Tempo desde o primeiro repositório da linguagem principal | 134 | | **Número de Repositórios** | 20% | Quantidade de projetos na stack principal | 135 | | **Média de Estrelas** | 20% | Popularidade média dos repositórios | 136 | | **Total de Commits** | 15% | Atividade e consistência no desenvolvimento | 137 | | **Indicadores de Complexidade** | 15% | Projetos colaborativos, bem documentados e populares | 138 | 139 | ### 🔍 Indicadores de Complexidade 140 | 141 | O sistema identifica automaticamente: 142 | 143 | - **Projetos Complexos:** Repositórios com 10+ estrelas ou 5+ colaboradores 144 | - **Projetos Colaborativos:** Repositórios com múltiplos contributors 145 | - **Projetos Bem Documentados:** Repos com descrição detalhada, homepage e issues habilitadas 146 | 147 | ### 📈 Métricas Exibidas 148 | 149 | No dashboard, você visualiza: 150 | 151 | - **Stack Principal:** Categoria dominante baseada na linguagem mais usada 152 | - **Nível de Senioridade:** Classificação com score detalhado 153 | - **Tempo de Experiência:** Faixa de experiência calculada 154 | - **Top 3 Linguagens:** Ranking com anos de experiência em cada uma 155 | - **Linguagens Dominadas:** Total de linguagens utilizadas 156 | 157 | --- 158 | 159 | ## 🏅 Distintivos de Conquista 160 | 161 | Os distintivos são desbloqueados conforme critérios específicos: 162 | 163 | ### 🎯 Achievements Tradicionais 164 | - **Code Warrior:** Mais de 1.000 commits. 165 | - **Império do Código:** Mais de 50 repositórios. 166 | - **Arquiteto do GitHub:** 10 ou mais linguagens diferentes utilizadas. 167 | - **Estrela do GitHub:** Mais de 100 estrelas. 168 | - **Projeto de Ouro:** Possuir ao menos 1 repositório com mais de 500 estrelas. 169 | - **Veterano do Código:** Conta com mais de 10 anos de GitHub. 170 | - **GitHub Old School:** Conta com mais de 5 anos de existência. 171 | 172 | ### 🚀 Achievements de Stack & Senioridade 173 | - **Especialista de Stack:** Score de senioridade 70+ na stack principal. 174 | - **Líder Técnico:** Atingir o nível Tech Lead de senioridade. 175 | - **Desenvolvedor Sênior:** Alcançar o nível Sênior ou superior. 176 | - **Poliglota:** Dominar 15 ou mais linguagens de programação. 177 | 178 | --- 179 | 180 | ## 🖼️ Geração de Card Personalizado 181 | 182 | Na parte inferior do dashboard, há um botão para gerar uma imagem personalizada contendo: 183 | 184 | - Nome do usuário 185 | - Username 186 | - Foto do GitHub 187 | - Valor agregado 188 | - Total de commits 189 | - Total de pontos 190 | - Distintivos conquistados 191 | - **Stack principal** 192 | - **Nível de senioridade com score** 193 | - **Tempo de experiência** 194 | - **Número de linguagens dominadas** 195 | 196 | **Funcionalidades:** 197 | - Download automático da imagem. 198 | - Compartilhamento direto no LinkedIn via modal com botão. 199 | - **Novo:** Informações de stack e senioridade para destacar expertise técnica. 200 | 201 | --- 202 | 203 | ## 🚀 Como Testar 204 | 205 | Para testar o projeto localmente: 206 | 207 | 1. **Clone o repositório:** 208 | ```bash 209 | git clone https://github.com/leticiaviana/CommitWorth.git 210 | cd CommitWorth 211 | ``` 212 | 213 | 2. **Instale as dependências:** 214 | ```bash 215 | npm install 216 | ``` 217 | 218 | 3. **Configure o token do GitHub:** 219 | - Crie um arquivo `.env.local` 220 | - Adicione: `GITHUB_TOKEN_FOR_REQUESTS=seu_token_aqui` 221 | - Obtenha seu token em: https://github.com/settings/tokens 222 | 223 | 4. **Execute o projeto:** 224 | ```bash 225 | npm run dev 226 | ``` 227 | 228 | 5. **Acesse:** http://localhost:3000 229 | 230 | ## 🛠️ Tecnologias Utilizadas 231 | 232 | - **Frontend:** Next.js 15 + React 19 + TypeScript 233 | - **Styling:** TailwindCSS 4 + CSS personalizado 234 | - **API:** GraphQL + GitHub API 235 | - **Gráficos:** Recharts 236 | - **UI Components:** Radix UI + Lucide React 237 | - **3D Effects:** OGL 238 | - **Geração de Imagens:** html-to-image 239 | 240 | --- 241 | 242 | ## 🤝 Contribuição 243 | 244 | Qualquer desenvolvedor pode contribuir com o projeto. Para isso: 245 | 246 | 1. Faça um fork do repositório. 247 | 2. Crie uma branch para sua feature ou correção. 248 | 3. Envie um pull request detalhando sua proposta. 249 | 250 | **Sugestões de melhoria, correções e novas métricas são bem-vindas!** 251 | 252 | --- 253 | 254 | ## 📄 Licença 255 | 256 | Este projeto está sob a licença MIT. Consulte o arquivo [LICENSE](LICENSE) para mais detalhes. 257 | -------------------------------------------------------------------------------- /src/app/dashboard/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useRef, useState } from "react" 4 | import { toPng } from "html-to-image" 5 | import { Logo } from '@/components/logo'; 6 | import { ModalShareCard } from "../modalShareCard"; 7 | import { toast } from "sonner"; 8 | import Image from "next/image"; 9 | import githubLogo from '@/assets/github-logo.svg' 10 | import { Achievement } from "@/lib/types"; 11 | import { StackAnalysis } from "@/lib/calcs/stackAnalysis"; 12 | 13 | interface CardFooter { 14 | valorAgregado: number; 15 | totalCommits: number; 16 | pontosTotais: number; 17 | name: string; 18 | nickname: string; 19 | avatar_url: string; 20 | achievements: Achievement[]; 21 | stackAnalysis: StackAnalysis; 22 | } 23 | 24 | export function Footer({ 25 | avatar_url, 26 | name, 27 | pontosTotais, 28 | totalCommits, 29 | nickname, 30 | valorAgregado, 31 | achievements, 32 | stackAnalysis 33 | }: CardFooter) { 34 | const [showModal, setShowModal] = useState(false) 35 | const cardRef = useRef(null) 36 | 37 | const handleDownload = async () => { 38 | if (!cardRef.current) return 39 | 40 | try { 41 | const dataUrl = await toPng(cardRef.current, { cacheBust: true }) 42 | 43 | const link = document.createElement("a") 44 | link.download = "meu-card.png" 45 | link.href = dataUrl 46 | link.click() 47 | setShowModal(true) 48 | } catch (err) { 49 | toast.error('Erro ao gerar imagem, tente novamente mais tarde') 50 | } 51 | } 52 | 53 | return ( 54 | <> 55 | {showModal && ( 56 | 57 | )} 58 |
59 |
60 | 61 |
62 | 63 |
68 | 72 | 73 |

74 | Veja Quanto eu Gerei com meus{" "} 75 | 76 | Commits 77 | {" "} 78 | no GitHub! 79 |

80 | 81 |
82 |
83 | 84 |
85 | 86 | {achievements.filter(item => item.completed === true).map(item => ( 87 |
88 |
89 | {item.name} 90 |
91 | ))} 92 | 93 |
94 | 95 |
96 |
97 |

98 | {name} 99 |

100 |

101 | @{nickname} 102 |

103 |
104 | 105 | {`Avatar 114 |
115 |
116 | 117 |
118 | 119 |
120 |

Total de commits

121 |

+{totalCommits}

122 |
123 | 124 |
125 |
126 |

Valor agregado

127 | Github Logo 132 |
133 |

R${valorAgregado.toLocaleString('pt-BR')}

134 |
135 | 136 |
137 |

Meus pontos

138 |

+{pontosTotais}

139 |
140 |
141 | 142 |
143 |
144 |

Stack Principal

145 |

{stackAnalysis.primaryStack}

146 |
147 | 148 |
149 |

Senioridade

150 |

{stackAnalysis.seniorityLevel}

151 |

Score: {stackAnalysis.seniorityScore}/100

152 |
153 | 154 |
155 |

Experiência

156 |

{stackAnalysis.stackSummary.experienceRange}

157 |

{stackAnalysis.stackSummary.totalLanguages} linguagens

158 |
159 |
160 |
161 |
162 |
163 | 164 | 165 |
166 |

167 | Compartilhe seu card em suas redes sociais!! 168 |

169 |

170 | Gere seu Card clicando no Botão abaixo! e mostre para o mundo quanto você já contribuiu com seus codigos. 171 |

172 | 173 | 176 |
177 |
178 |
179 | 180 | ) 181 | } 182 | -------------------------------------------------------------------------------- /src/lib/calcs/stackAnalysis.ts: -------------------------------------------------------------------------------- 1 | import { GitHubStatsResponse } from "../types"; 2 | 3 | export interface StackAnalysis { 4 | primaryStack: string; 5 | seniorityLevel: 'Junior' | 'Pleno' | 'Senior' | 'Tech Lead'; 6 | seniorityScore: number; 7 | stackExperience: { 8 | language: string; 9 | repositories: number; 10 | totalCommits: number; 11 | yearOfFirstUse: number; 12 | yearsOfExperience: number; 13 | seniorityIndicators: { 14 | hasComplexProjects: boolean; 15 | hasCollaborativeProjects: boolean; 16 | hasWellDocumentedProjects: boolean; 17 | avgStarsPerRepo: number; 18 | }; 19 | }[]; 20 | stackSummary: { 21 | totalLanguages: number; 22 | primaryLanguagePercentage: number; 23 | experienceRange: string; 24 | }; 25 | } 26 | 27 | // Mapeamento de linguagens para stacks principais 28 | const STACK_MAPPING: Record = { 29 | 'JavaScript': 'Frontend/Fullstack', 30 | 'TypeScript': 'Frontend/Fullstack', 31 | 'React': 'Frontend', 32 | 'Vue': 'Frontend', 33 | 'Angular': 'Frontend', 34 | 'Python': 'Backend/Data Science', 35 | 'Java': 'Backend/Enterprise', 36 | 'C#': 'Backend/.NET', 37 | 'Go': 'Backend/Cloud', 38 | 'Rust': 'Systems/Performance', 39 | 'C++': 'Systems/Game Dev', 40 | 'C': 'Systems/Embedded', 41 | 'PHP': 'Backend/Web', 42 | 'Ruby': 'Backend/Web', 43 | 'Swift': 'Mobile/iOS', 44 | 'Kotlin': 'Mobile/Android', 45 | 'Dart': 'Mobile/Flutter', 46 | 'Scala': 'Backend/Big Data', 47 | 'R': 'Data Science', 48 | 'MATLAB': 'Data Science/Engineering', 49 | 'Shell': 'DevOps/Infrastructure', 50 | 'PowerShell': 'DevOps/Windows', 51 | 'Dockerfile': 'DevOps/Containerization', 52 | 'HTML': 'Frontend', 53 | 'CSS': 'Frontend', 54 | 'SCSS': 'Frontend', 55 | 'Less': 'Frontend' 56 | }; 57 | 58 | // Pesos para diferentes fatores de senioridade 59 | const SENIORITY_WEIGHTS = { 60 | yearsOfExperience: 0.3, 61 | repositoryCount: 0.2, 62 | avgStarsPerRepo: 0.2, 63 | totalCommits: 0.15, 64 | complexityIndicators: 0.15 65 | } as const; 66 | 67 | function calculateAccountAge(createdAt: string): number { 68 | try { 69 | const accountCreated = new Date(createdAt); 70 | const now = new Date(); 71 | 72 | if (isNaN(accountCreated.getTime())) { 73 | return 0; 74 | } 75 | 76 | return (now.getTime() - accountCreated.getTime()) / (1000 * 60 * 60 * 24 * 365.25); 77 | } catch { 78 | return 0; 79 | } 80 | } 81 | 82 | function getLanguageFromRepo(repo: any): string[] { 83 | if (!repo.languages?.nodes) return []; 84 | return repo.languages.nodes.map((lang: any) => lang.name).filter(Boolean); 85 | } 86 | 87 | function calculateFirstUseYear(repos: any[], language: string): number { 88 | const reposWithLanguage = repos.filter(repo => 89 | getLanguageFromRepo(repo).includes(language) 90 | ); 91 | 92 | if (reposWithLanguage.length === 0) return new Date().getFullYear(); 93 | 94 | const earliestRepo = reposWithLanguage.reduce((earliest, current) => { 95 | const currentDate = new Date(current.createdAt); 96 | const earliestDate = new Date(earliest.createdAt); 97 | return currentDate < earliestDate ? current : earliest; 98 | }); 99 | 100 | return new Date(earliestRepo.createdAt).getFullYear(); 101 | } 102 | 103 | function calculateSeniorityIndicators(repos: any[], language: string) { 104 | const languageRepos = repos.filter(repo => 105 | getLanguageFromRepo(repo).includes(language) 106 | ); 107 | 108 | if (languageRepos.length === 0) { 109 | return { 110 | hasComplexProjects: false, 111 | hasCollaborativeProjects: false, 112 | hasWellDocumentedProjects: false, 113 | avgStarsPerRepo: 0 114 | }; 115 | } 116 | 117 | const totalStars = languageRepos.reduce((sum, repo) => sum + (repo.stargazerCount || 0), 0); 118 | const avgStarsPerRepo = totalStars / languageRepos.length; 119 | 120 | // Projetos complexos: repositórios com muitas estrelas ou contributors 121 | const hasComplexProjects = languageRepos.some(repo => 122 | (repo.stargazerCount || 0) > 10 || 123 | (repo.mentionableUsers?.totalCount || 0) > 5 124 | ); 125 | 126 | // Projetos colaborativos: repositórios com múltiplos contributors 127 | const hasCollaborativeProjects = languageRepos.some(repo => 128 | (repo.mentionableUsers?.totalCount || 0) > 2 129 | ); 130 | 131 | // Projetos bem documentados 132 | const hasWellDocumentedProjects = languageRepos.some(repo => 133 | repo.description && 134 | repo.description.length > 50 && 135 | (repo.homepageUrl || repo.hasIssuesEnabled) 136 | ); 137 | 138 | return { 139 | hasComplexProjects, 140 | hasCollaborativeProjects, 141 | hasWellDocumentedProjects, 142 | avgStarsPerRepo: Math.round(avgStarsPerRepo * 100) / 100 143 | }; 144 | } 145 | 146 | function calculateLanguageCommits(repos: any[], language: string): number { 147 | const languageRepos = repos.filter(repo => 148 | getLanguageFromRepo(repo).includes(language) 149 | ); 150 | 151 | return languageRepos.reduce((total, repo) => { 152 | return total + (repo.defaultBranchRef?.target?.history?.totalCount || 0); 153 | }, 0); 154 | } 155 | 156 | function calculateSeniorityScore(experience: StackAnalysis['stackExperience'][0]): number { 157 | const { 158 | yearsOfExperience, 159 | repositories, 160 | totalCommits, 161 | seniorityIndicators 162 | } = experience; 163 | 164 | // Normalizar valores para escala 0-100 165 | const normalizedYears = Math.min(yearsOfExperience / 8, 1) * 100; 166 | const normalizedRepos = Math.min(repositories / 20, 1) * 100; 167 | const normalizedCommits = Math.min(totalCommits / 1000, 1) * 100; 168 | const normalizedStars = Math.min(seniorityIndicators.avgStarsPerRepo / 10, 1) * 100; 169 | 170 | // Indicadores de complexidade 171 | const complexityScore = ( 172 | (seniorityIndicators.hasComplexProjects ? 25 : 0) + 173 | (seniorityIndicators.hasCollaborativeProjects ? 25 : 0) + 174 | (seniorityIndicators.hasWellDocumentedProjects ? 25 : 0) + 175 | (normalizedStars > 50 ? 25 : 0) 176 | ); 177 | 178 | const score = ( 179 | normalizedYears * SENIORITY_WEIGHTS.yearsOfExperience + 180 | normalizedRepos * SENIORITY_WEIGHTS.repositoryCount + 181 | normalizedStars * SENIORITY_WEIGHTS.avgStarsPerRepo + 182 | normalizedCommits * SENIORITY_WEIGHTS.totalCommits + 183 | complexityScore * SENIORITY_WEIGHTS.complexityIndicators 184 | ); 185 | 186 | return Math.round(score); 187 | } 188 | 189 | function determineSeniorityLevel(score: number): StackAnalysis['seniorityLevel'] { 190 | if (score >= 80) return 'Tech Lead'; 191 | if (score >= 60) return 'Senior'; 192 | if (score >= 35) return 'Pleno'; 193 | return 'Junior'; 194 | } 195 | 196 | export function analyzeStackAndSeniority(data: GitHubStatsResponse): StackAnalysis { 197 | const repos = data.user.repositories.nodes.filter(repo => !repo.isFork); 198 | const currentYear = new Date().getFullYear(); 199 | 200 | // Contar linguagens e seus repositórios 201 | const languageStats = new Map(); 202 | 203 | repos.forEach(repo => { 204 | const languages = getLanguageFromRepo(repo); 205 | languages.forEach(lang => { 206 | languageStats.set(lang, (languageStats.get(lang) || 0) + 1); 207 | }); 208 | }); 209 | 210 | // Criar análise de experiência para cada linguagem 211 | const stackExperience = Array.from(languageStats.entries()) 212 | .map(([language, repositories]) => { 213 | const firstUseYear = calculateFirstUseYear(repos, language); 214 | const yearsOfExperience = Math.max(currentYear - firstUseYear + 1, 1); 215 | const totalCommits = calculateLanguageCommits(repos, language); 216 | const seniorityIndicators = calculateSeniorityIndicators(repos, language); 217 | 218 | return { 219 | language, 220 | repositories, 221 | totalCommits, 222 | yearOfFirstUse: firstUseYear, 223 | yearsOfExperience, 224 | seniorityIndicators 225 | }; 226 | }) 227 | .sort((a, b) => b.repositories - a.repositories); // Ordenar por número de repositórios 228 | 229 | // Determinar stack principal (linguagem mais usada) 230 | const primaryLanguage = stackExperience[0]?.language || 'Indefinido'; 231 | const primaryStack = STACK_MAPPING[primaryLanguage] || primaryLanguage; 232 | 233 | // Calcular senioridade baseada na linguagem principal 234 | const primaryExperience = stackExperience[0]; 235 | let seniorityScore = 0; 236 | let seniorityLevel: StackAnalysis['seniorityLevel'] = 'Junior'; 237 | 238 | if (primaryExperience) { 239 | seniorityScore = calculateSeniorityScore(primaryExperience); 240 | seniorityLevel = determineSeniorityLevel(seniorityScore); 241 | } 242 | 243 | // Calcular resumo da stack 244 | const totalRepositories = repos.length; 245 | const primaryLanguageRepos = primaryExperience?.repositories || 0; 246 | const primaryLanguagePercentage = totalRepositories > 0 247 | ? Math.round((primaryLanguageRepos / totalRepositories) * 100) 248 | : 0; 249 | 250 | const experienceYears = stackExperience.length > 0 251 | ? Math.max(...stackExperience.map(exp => exp.yearsOfExperience)) 252 | : 0; 253 | 254 | let experienceRange = 'Iniciante'; 255 | if (experienceYears >= 8) experienceRange = 'Muito Experiente (8+ anos)'; 256 | else if (experienceYears >= 5) experienceRange = 'Experiente (5-8 anos)'; 257 | else if (experienceYears >= 3) experienceRange = 'Intermediário (3-5 anos)'; 258 | else if (experienceYears >= 1) experienceRange = 'Iniciante (1-3 anos)'; 259 | 260 | return { 261 | primaryStack, 262 | seniorityLevel, 263 | seniorityScore, 264 | stackExperience, 265 | stackSummary: { 266 | totalLanguages: stackExperience.length, 267 | primaryLanguagePercentage, 268 | experienceRange 269 | } 270 | }; 271 | } 272 | -------------------------------------------------------------------------------- /src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from "@/components/container"; 2 | import { Header } from "@/components/header"; 3 | 4 | export default function About() { 5 | return ( 6 | <> 7 |
8 | 9 |
17 |

Sobre o CommitWorth

18 |
19 |

Visão Geral do Projeto

20 |

21 | O CommitWorth é uma plataforma gamificada que calcula o "valor agregado" e outras informações do trabalho de desenvolvedores a partir de dados públicos do GitHub. Basta informar um username válido para acessar um dashboard exclusivo com métricas, análise de stack, detecção de senioridade e conquistas. 22 |

23 |
24 |
25 |

Objetivo

26 |

27 | O objetivo do CommitWorth é valorizar e dar visibilidade ao esforço de desenvolvedores, traduzindo sua atividade no GitHub em números, conquistas e um card compartilhável. A plataforma incentiva o crescimento técnico, a colaboração e a construção de um portfólio público relevante. 28 |

29 |
30 |
31 |

Funcionamento

32 |
    33 |
  1. O usuário informa seu username do GitHub.
  2. 34 |
  3. O sistema coleta dados públicos via API do GitHub.
  4. 35 |
  5. As métricas são processadas e a análise de stack é realizada: 36 |
      37 |
    • Identificação da linguagem/stack principal
    • 38 |
    • Cálculo do score de senioridade
    • 39 |
    • Análise de indicadores de complexidade
    • 40 |
    • Determinação do nível de experiência
    • 41 |
    42 |
  6. 43 |
  7. Os dados são exibidos no dashboard com gráficos e análises.
  8. 44 |
  9. O usuário pode gerar um card personalizado com suas conquistas e stack.
  10. 45 |
46 |
47 | Atenção: Todos os cálculos são realizados a partir de apenas 1 requisição que busca os 100 primeiros repositórios públicos do usuário no GitHub. Se você possui mais de 100 repositórios, os dados exibidos podem não refletir todo o seu histórico. Estamos trabalhando para ampliar esse limite em versões futuras! 48 |
49 |
50 |
51 |

Dados Consultados e Calculados

52 |
    53 |
  • Total de Estrelas: Soma das estrelas em todos os repositórios públicos.
  • 54 |
  • Total de Repositórios: Quantidade de repositórios públicos (excluindo forks).
  • 55 |
  • Total de Commits: Soma dos commits em todos os repositórios.
  • 56 |
  • Valor Agregado: Valor fictício calculado com base em commits, estrelas e forks:
  • 57 |
58 |
59 | Commit: R$2,00 | Estrela: R$0,50 | Fork: R$1,00 60 | Valor Agregado = (Commits x 2,00) + (Estrelas x 0,50) + (Forks x 1,00) 61 |
62 |
    63 |
  • Pontos do Usuário: Sistema de pontuação para gamificação:
  • 64 |
65 |
66 | Commit: 1 ponto | Estrela: 5 pontos | Fork: 3 pontos | Repo bem estruturado: 10 pontos 67 |
68 |
    69 |
  • Linguagens mais utilizadas: Top 5 linguagens predominantes.
  • 70 |
  • Repositórios bem estruturados: Repositórios com descrição com mais de 50 caracteres, homepage e issues habilitadas.
  • 71 |
  • Popularidade: Top 5 repositórios mais populares por estrelas.
  • 72 |
73 |
74 |
75 |

Diferença entre "Top Linguagens" e "Linguagens mais utilizadas"

76 |
77 |

78 | Top Linguagens (no card Análise de Stack & Senioridade) mostra as linguagens em que você possui mais experiência e tempo de uso, considerando: 79 |

80 |
    81 |
  • Anos de experiência em cada linguagem (desde o primeiro repositório)
  • 82 |
  • Quantidade de repositórios por linguagem
  • 83 |
  • Commits e indicadores de senioridade
  • 84 |
85 |

86 | Já Linguagens mais utilizadas exibe as linguagens mais frequentes nos seus repositórios públicos, baseada apenas na contagem de repositórios que usam cada linguagem. 87 |

88 |

89 | Por isso, os resultados podem ser diferentes: você pode ter muitos repositórios em uma linguagem (mais utilizada), mas ter mais experiência e tempo em outra (top linguagens da stack/senioridade). 90 |

91 |
92 |
93 |
94 |

Distintivos de Conquista

95 |

96 | O sistema desbloqueia distintivos conforme critérios como número de commits, repositórios, linguagens, estrelas, tempo de conta, senioridade e stack. Exemplos: 97 |

98 |
    99 |
  • Code Warrior: Mais de 1.000 commits
  • 100 |
  • Império do Código: 50+ repositórios
  • 101 |
  • Arquiteto do GitHub: 10+ linguagens
  • 102 |
  • Estrela do GitHub: 100+ estrelas
  • 103 |
  • Projeto de Ouro: 1 repo com 500+ estrelas
  • 104 |
  • Veterano do Código: 10+ anos de GitHub
  • 105 |
  • Especialista de Stack: Senioridade 70+ na stack principal
  • 106 |
  • Líder Técnico: Nível Tech Lead
  • 107 |
  • Poliglota: 15+ linguagens dominadas
  • 108 |
109 |
110 |
111 |

Geração e Compartilhamento do Card

112 |

113 | O usuário pode gerar uma imagem personalizada com seu nome, foto, valor agregado, total de commits, pontos, distintivos, stack principal, nível de senioridade, tempo de experiência e número de linguagens. O card pode ser baixado ou compartilhado em suas redes sociais. 114 |

115 |
116 |
117 |
118 | 119 | ) 120 | } -------------------------------------------------------------------------------- /src/app/dashboard/[user]/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import { 3 | Star, 4 | Box, 5 | GitCommit, 6 | DollarSign, 7 | GitFork, 8 | } from 'lucide-react' 9 | import { redirect, notFound } from "next/navigation" 10 | import { cache } from 'react' 11 | 12 | import { getGitHubStatsGraphQL } from "@/lib/getGithubData" 13 | 14 | import { Footer } from "../components/footer" 15 | import { CardInfoUserSmall } from "../components/cardInfoUserSmall" 16 | import { CardInfoUserBigNumber } from "../components/cardInfoUserBigNumber" 17 | import { CardLanguageChart } from "../components/charts/cardLanguageChart" 18 | import { CardPopularReposChart } from "../components/charts/cardPopularReposChart" 19 | import { WellStructuredRepoScoresChart } from "../components/charts/wellStructuredRepoScoresChart" 20 | import { RateLimitModal } from "../components/rateLimitModal" 21 | import { ConquestModal } from "../components/conquestCard" 22 | import { StackAnalysisCard } from "../components/stackAnalysisCard" 23 | 24 | import { Container } from "@/components/container" 25 | import LightRays from '@/components/LightRaysBG' 26 | import { Header } from "@/components/header" 27 | import { Metadata } from "next" 28 | import { GenerateMetadataModel } from "../utils/generateMetadata" 29 | import { GitHubCompleteData } from "@/lib/types" 30 | 31 | // Função com cache para buscar TODOS os dados com UMA única requisição 32 | const getCompleteGitHubData = cache(async (user: string): Promise => { 33 | try { 34 | const data = await getGitHubStatsGraphQL(user) 35 | return data 36 | 37 | } catch (error: any) { 38 | if (error?.response?.errors) { 39 | const errors = error.response.errors 40 | // usuário não encontrado 41 | if (errors.some((err: any) => err.type === 'NOT_FOUND')) { 42 | notFound() 43 | } 44 | } 45 | 46 | if (error?.response?.status === 403) { 47 | redirect('/') 48 | } 49 | 50 | throw error 51 | } 52 | }) 53 | 54 | interface PageProps { 55 | params: Promise<{ user: string }> 56 | } 57 | 58 | // Função para gerar metadata usando os dados em cache 59 | export async function generateMetadata({ params }: PageProps): Promise { 60 | const { user } = await params 61 | 62 | const { 63 | userData, 64 | totalStars, 65 | totalCommits, 66 | valorAgregado, 67 | achievements, 68 | pontosTotais, 69 | repoCountExcludingForks 70 | } = await getCompleteGitHubData(user) 71 | 72 | return GenerateMetadataModel({ 73 | totalCommits, 74 | totalStars, 75 | userData, 76 | valorAgregado, 77 | achievements, 78 | totalPoints: pontosTotais, 79 | totalRepos: repoCountExcludingForks 80 | }) 81 | } 82 | 83 | export default async function UserDetails({ params }: PageProps) { 84 | const { user } = await params 85 | 86 | const { 87 | userData, 88 | totalStars, 89 | totalForks, 90 | repoCountExcludingForks, 91 | popularContributions, 92 | wellStructuredRepoScores, 93 | totalCommits, 94 | valorAgregado, 95 | pontosTotais, 96 | languageRepoCount, 97 | rateLimitInfo, 98 | achievements, 99 | stackAnalysis 100 | } = await getCompleteGitHubData(user) // esses dados ele pega do cache da req já feita para os metadata 101 | 102 | return ( 103 |
104 |
105 | 117 |
118 | 119 |
120 |
121 | 122 | 127 | 128 |
129 |
130 |
{/* Aqui terá um botão futuramente */} 131 |
132 |
133 |

134 | {userData.name} 135 |

136 |

137 | @{userData.login} 138 |

139 |
140 | 141 | {`Avatar 148 |
149 |
150 | 151 |
152 | 157 | 162 | 167 |
168 | 169 |
170 | 171 |
172 | 173 |
174 | 180 | 181 | {languageRepoCount.length > 1 && ( 182 | 187 | )} 188 | 189 | 195 | 196 | {popularContributions.length > 1 && ( 197 | 201 | )} 202 | 203 | {wellStructuredRepoScores.length > 1 && ( 204 | 209 | )} 210 | 211 | 217 |
218 |
219 | 220 |

Distintivos de Conquista

221 |
222 | {achievements.map(item => ( 223 | 229 | ))} 230 |
231 | 232 |

Gere seu card!

233 |
243 | 244 |
245 |
246 | ) 247 | } -------------------------------------------------------------------------------- /src/components/ui/chart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as RechartsPrimitive from "recharts" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | // Format: { THEME_NAME: CSS_SELECTOR } 9 | const THEMES = { light: "", dark: ".dark" } as const 10 | 11 | export type ChartConfig = { 12 | [k in string]: { 13 | label?: React.ReactNode 14 | icon?: React.ComponentType 15 | } & ( 16 | | { color?: string; theme?: never } 17 | | { color?: never; theme: Record } 18 | ) 19 | } 20 | 21 | type ChartContextProps = { 22 | config: ChartConfig 23 | } 24 | 25 | const ChartContext = React.createContext(null) 26 | 27 | function useChart() { 28 | const context = React.useContext(ChartContext) 29 | 30 | if (!context) { 31 | throw new Error("useChart must be used within a ") 32 | } 33 | 34 | return context 35 | } 36 | 37 | function ChartContainer({ 38 | id, 39 | className, 40 | children, 41 | config, 42 | ...props 43 | }: React.ComponentProps<"div"> & { 44 | config: ChartConfig 45 | children: React.ComponentProps< 46 | typeof RechartsPrimitive.ResponsiveContainer 47 | >["children"] 48 | }) { 49 | const uniqueId = React.useId() 50 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` 51 | 52 | return ( 53 | 54 |
63 | 64 | 65 | {children} 66 | 67 |
68 |
69 | ) 70 | } 71 | 72 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 73 | const colorConfig = Object.entries(config).filter( 74 | ([, config]) => config.theme || config.color 75 | ) 76 | 77 | if (!colorConfig.length) { 78 | return null 79 | } 80 | 81 | return ( 82 |