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
@@ -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(<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 = {
|
||||
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<StatItem | null> = [
|
||||
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 }) {
|
||||
<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 && (
|
||||
<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">
|
||||
<dt className="text-[13px] leading-[1.4] text-[rgba(244,242,237,.64)]">
|
||||
{label}
|
||||
{detail && (
|
||||
<span className="block text-[12px] text-[rgba(244,242,237,.55)]">
|
||||
{detail}
|
||||
</span>
|
||||
)}
|
||||
</dt>
|
||||
<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}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
+9
-2
@@ -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 (
|
||||
<CinematicHome
|
||||
heroStats={{
|
||||
years: yearsOfExperience(),
|
||||
projects: projects.length,
|
||||
repos: repos ?? undefined,
|
||||
demos: projects.filter(
|
||||
(project) =>
|
||||
project.demoUrl && project.demoAvailability !== "unavailable",
|
||||
|
||||
Reference in new issue
Block a user