diff --git a/README.md b/README.md index fb54762..078906b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ corepack pnpm@9.15.9 start | `pnpm test:watch` | Executa Jest em modo watch. | | `pnpm check:projects` | Verifica URLs de demonstração e substitui atomicamente `data/project-health.json`. | +## Contagem de projetos do GitHub + +O número "Mais de X projetos desenvolvidos" no topo da home vem da API do GitHub (`lib/github-stats.ts`), consultada só no servidor e renovada uma vez por dia (`revalidate = 86400` em `app/page.tsx`). Conta repositórios próprios, públicos e privados, sem forks e sem arquivados. + +Para incluir os privados, defina `GITHUB_STATS_TOKEN` no ambiente de build e de execução (não use o prefixo `NEXT_PUBLIC_`). O token deve ser um [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) com acesso a todos os repositórios da conta e uma única permissão, **Metadata: Read-only**. Sem o token, ou se a consulta falhar, o número simplesmente não aparece. + ## Stack | Área | Pacotes usados | diff --git a/app/components/Hero.test.tsx b/app/components/Hero.test.tsx index 20aa627..fb6e099 100644 --- a/app/components/Hero.test.tsx +++ b/app/components/Hero.test.tsx @@ -81,4 +81,23 @@ describe("Hero", () => { expect(link).toHaveAttribute("rel", "noopener noreferrer"); } }); + + it("shows the real GitHub repo count with the public/private breakdown", () => { + render(); + + expect(screen.getByText("projetos desenvolvidos")).toBeInTheDocument(); + expect(screen.getByText("Mais de")).toBeInTheDocument(); + expect(screen.getByText("68")).toBeInTheDocument(); + expect(screen.getByText("25 públicos · 43 privados")).toBeInTheDocument(); + expect(screen.queryByText(/projetos publicados/i)).not.toBeInTheDocument(); + }); + + it("omits the repo count when the GitHub lookup is unavailable", () => { + render(); + + expect(screen.getByText("anos de experiência")).toBeInTheDocument(); + expect( + screen.queryByText(/projetos desenvolvidos/i), + ).not.toBeInTheDocument(); + }); }); diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 847ca27..36b9f6d 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -17,20 +17,43 @@ const SOCIAL_LINKS = [ }, ]; -/** Números reais, calculados em app/page.tsx a partir de public/portfolio-data.json (nada digitado à mão). */ +/** + * Números reais, calculados em app/page.tsx (nada digitado à mão): anos e demos vêm de public/portfolio-data.json; + * repos vem da API do GitHub (lib/github-stats.ts) e some quando a consulta não está disponível. + */ export type HeroStats = { years?: number; - projects?: number; + repos?: { total: number; public: number; private: number }; demos?: number; }; -function statItems(stats?: HeroStats) { +type StatItem = { + value: string; + label: string; + prefix?: string; + detail?: string; +}; + +function statItems(stats?: HeroStats): StatItem[] { if (!stats) return []; - return [ - stats.years ? [String(stats.years), "anos de experiência"] : null, - stats.projects ? [String(stats.projects), "projetos publicados"] : null, - stats.demos ? [String(stats.demos), "com demonstração online"] : null, - ].filter((item): item is [string, string] => item !== null); + const { repos } = stats; + const items: Array = [ + stats.years + ? { value: String(stats.years), label: "anos de experiência" } + : null, + repos && repos.total > 0 + ? { + prefix: "Mais de", + value: String(repos.total), + label: "projetos desenvolvidos", + detail: `${repos.public} públicos · ${repos.private} privados`, + } + : null, + stats.demos + ? { value: String(stats.demos), label: "com demonstração online" } + : null, + ]; + return items.filter((item): item is StatItem => item !== null); } /** Título em palavras para a entrada em cascata; o texto segue inteiro para leitores de tela e buscadores. */ @@ -125,12 +148,22 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
{items.length > 0 && (
- {items.map(([value, label]) => ( + {items.map(({ prefix, value, label, detail }) => (
{label} + {detail && ( + + {detail} + + )}
+ {prefix && ( + + {prefix} + + )} {value}
diff --git a/app/page.tsx b/app/page.tsx index d8667eb..2976d9c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,10 @@ +import { loadGithubRepoCounts } from "@/lib/github-stats"; import { loadFeaturedProjects, portfolio } from "@/lib/portfolio-data"; import CinematicHome from "./components/cinematic/CinematicHome"; +/** A contagem do GitHub é consultada no servidor e renovada uma vez por dia. */ +export const revalidate = 86_400; + /** Anos de experiência vêm do próprio texto do portfólio ("5 anos de experiência"); sem o trecho, o número some. */ function yearsOfExperience() { const match = portfolio.personal.summary.match(/(\d+)\s+anos de experi/i); @@ -8,12 +12,15 @@ function yearsOfExperience() { } export default async function Home() { - const projects = await loadFeaturedProjects(); + const [projects, repos] = await Promise.all([ + loadFeaturedProjects(), + loadGithubRepoCounts(), + ]); return ( project.demoUrl && project.demoAvailability !== "unavailable",