feat(home): trocar o texto fixo de projetos publicados pela contagem real do GitHub
O hero mostra 'Mais de X projetos desenvolvidos' com o detalhamento de publicos e privados; sem a contagem (token ausente ou API fora) o item some. A home passa a revalidar uma vez por dia. README documenta o token. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
4ba955e2c3
commit
0350e7e883
4 files changed
+76
-11
No files matched your search
@@ -41,6 +41,12 @@ corepack [email protected] start
|
|||||||
| `pnpm test:watch` | Executa Jest em modo watch. |
|
| `pnpm test:watch` | Executa Jest em modo watch. |
|
||||||
| `pnpm check:projects` | Verifica URLs de demonstração e substitui atomicamente `data/project-health.json`. |
|
| `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
|
## Stack
|
||||||
|
|
||||||
| Área | Pacotes usados |
|
| Área | Pacotes usados |
|
||||||
|
|||||||
@@ -81,4 +81,23 @@ describe("Hero", () => {
|
|||||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the real GitHub repo count with the public/private breakdown", () => {
|
||||||
|
render(<Hero stats={{ repos: { total: 68, public: 25, private: 43 } }} />);
|
||||||
|
|
||||||
|
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(<Hero stats={{ years: 5 }} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("anos de experiência")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(/projetos desenvolvidos/i),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+42
-9
@@ -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 = {
|
export type HeroStats = {
|
||||||
years?: number;
|
years?: number;
|
||||||
projects?: number;
|
repos?: { total: number; public: number; private: number };
|
||||||
demos?: 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 [];
|
if (!stats) return [];
|
||||||
return [
|
const { repos } = stats;
|
||||||
stats.years ? [String(stats.years), "anos de experiência"] : null,
|
const items: Array<StatItem | null> = [
|
||||||
stats.projects ? [String(stats.projects), "projetos publicados"] : null,
|
stats.years
|
||||||
stats.demos ? [String(stats.demos), "com demonstração online"] : null,
|
? { value: String(stats.years), label: "anos de experiência" }
|
||||||
].filter((item): item is [string, string] => item !== null);
|
: 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. */
|
/** 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 }) {
|
|||||||
<div className="flex flex-wrap items-end gap-x-sp-8 gap-y-sp-4 border-t border-white/10 pt-sp-4">
|
<div className="flex flex-wrap items-end gap-x-sp-8 gap-y-sp-4 border-t border-white/10 pt-sp-4">
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
<dl className="flex gap-x-sp-5 gap-y-sp-4 sm:flex-wrap sm:gap-x-sp-8">
|
<dl className="flex gap-x-sp-5 gap-y-sp-4 sm:flex-wrap sm:gap-x-sp-8">
|
||||||
{items.map(([value, label]) => (
|
{items.map(({ prefix, value, label, detail }) => (
|
||||||
<div key={label} className="flex flex-col-reverse">
|
<div key={label} className="flex flex-col-reverse">
|
||||||
<dt className="text-[13px] leading-[1.4] text-[rgba(244,242,237,.64)]">
|
<dt className="text-[13px] leading-[1.4] text-[rgba(244,242,237,.64)]">
|
||||||
{label}
|
{label}
|
||||||
|
{detail && (
|
||||||
|
<span className="block text-[12px] text-[rgba(244,242,237,.55)]">
|
||||||
|
{detail}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="font-mono text-[clamp(22px,1.1rem+0.8vw,36px)] font-semibold leading-none tracking-[-0.03em] text-[var(--slash)]">
|
<dd className="font-mono text-[clamp(22px,1.1rem+0.8vw,36px)] font-semibold leading-none tracking-[-0.03em] text-[var(--slash)]">
|
||||||
|
{prefix && (
|
||||||
|
<span className="mr-sp-2 text-[13px] font-normal tracking-normal text-[rgba(244,242,237,.64)]">
|
||||||
|
{prefix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{value}
|
{value}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+9
-2
@@ -1,6 +1,10 @@
|
|||||||
|
import { loadGithubRepoCounts } from "@/lib/github-stats";
|
||||||
import { loadFeaturedProjects, portfolio } from "@/lib/portfolio-data";
|
import { loadFeaturedProjects, portfolio } from "@/lib/portfolio-data";
|
||||||
import CinematicHome from "./components/cinematic/CinematicHome";
|
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. */
|
/** 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() {
|
function yearsOfExperience() {
|
||||||
const match = portfolio.personal.summary.match(/(\d+)\s+anos de experi/i);
|
const match = portfolio.personal.summary.match(/(\d+)\s+anos de experi/i);
|
||||||
@@ -8,12 +12,15 @@ function yearsOfExperience() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const projects = await loadFeaturedProjects();
|
const [projects, repos] = await Promise.all([
|
||||||
|
loadFeaturedProjects(),
|
||||||
|
loadGithubRepoCounts(),
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<CinematicHome
|
<CinematicHome
|
||||||
heroStats={{
|
heroStats={{
|
||||||
years: yearsOfExperience(),
|
years: yearsOfExperience(),
|
||||||
projects: projects.length,
|
repos: repos ?? undefined,
|
||||||
demos: projects.filter(
|
demos: projects.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.demoUrl && project.demoAvailability !== "unavailable",
|
project.demoUrl && project.demoAvailability !== "unavailable",
|
||||||
|
|||||||
Reference in new issue
Block a user