feat(cases): adiciona o case Julia Beauty com vídeo de rolagem
This commit is contained in:
1 parent
594daecd2b
commit
bcdf0099c5
26 files changed
+159
-42
No files matched your search
@@ -132,7 +132,7 @@ describe("opengraph-image Image", () => {
|
|||||||
expect(all.map(({ id }) => id)).toEqual(caseStudies.map(({ id }) => id));
|
expect(all.map(({ id }) => id)).toEqual(caseStudies.map(({ id }) => id));
|
||||||
expect(
|
expect(
|
||||||
all.filter(({ contentType }) => contentType === "image/jpeg"),
|
all.filter(({ contentType }) => contentType === "image/jpeg"),
|
||||||
).toHaveLength(5);
|
).toHaveLength(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("path: unknown slug -> notFound() and no image", async () => {
|
it("path: unknown slug -> notFound() and no image", async () => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const clientCaseIds = [
|
|||||||
"lavacao-047",
|
"lavacao-047",
|
||||||
"sc-plus",
|
"sc-plus",
|
||||||
"as-servicos",
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
];
|
];
|
||||||
const labCaseIds = [
|
const labCaseIds = [
|
||||||
"nutriscan",
|
"nutriscan",
|
||||||
@@ -86,15 +87,21 @@ function expectContactAndNoDeadLinks(caseStudy: { id: string; title: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
it("exposes the eight public cases, the two new client cases among them", async () => {
|
it("exposes the nine public cases, the new client cases among them", async () => {
|
||||||
const staticSlugs = generateStaticParams().map(({ slug }) => slug);
|
const staticSlugs = generateStaticParams().map(({ slug }) => slug);
|
||||||
expect(staticSlugs).toHaveLength(8);
|
expect(staticSlugs).toHaveLength(9);
|
||||||
expect(new Set(staticSlugs).size).toBe(8);
|
expect(new Set(staticSlugs).size).toBe(9);
|
||||||
expect([...staticSlugs].sort()).toEqual([...publicCaseIds].sort());
|
expect([...staticSlugs].sort()).toEqual([...publicCaseIds].sort());
|
||||||
expect(staticSlugs).toContain("sev-modaintima");
|
expect(staticSlugs).toContain("sev-modaintima");
|
||||||
expect(staticSlugs).toContain("lavacao-047");
|
expect(staticSlugs).toContain("lavacao-047");
|
||||||
|
expect(staticSlugs).toContain("julia-beauty");
|
||||||
|
|
||||||
for (const id of ["sev-modaintima", "lavacao-047", "as-servicos"]) {
|
for (const id of [
|
||||||
|
"sev-modaintima",
|
||||||
|
"lavacao-047",
|
||||||
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
|
]) {
|
||||||
const caseStudy = getCase(id);
|
const caseStudy = getCase(id);
|
||||||
const url = `${siteUrl}/cases/${caseStudy.id}`;
|
const url = `${siteUrl}/cases/${caseStudy.id}`;
|
||||||
const { metaTitle: title, metaDescription: description } = caseStudy;
|
const { metaTitle: title, metaDescription: description } = caseStudy;
|
||||||
@@ -680,6 +687,7 @@ it("lists client cases with a cover first, then the laboratory, and keeps the sc
|
|||||||
"lavacao-047",
|
"lavacao-047",
|
||||||
"sc-plus",
|
"sc-plus",
|
||||||
"as-servicos",
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
]);
|
]);
|
||||||
const clientLinks = listedIds.map((id) => {
|
const clientLinks = listedIds.map((id) => {
|
||||||
const card = container
|
const card = container
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ describe("CasesPage: cases de cliente", () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("path: contact -> 'Falar sobre meu projeto', and the list of clients follows the showcase: the SEV first, then Lavação 047, SC Plus and AS Serviços", () => {
|
it("path: contact -> 'Falar sobre meu projeto', and the list of clients follows the showcase: the SEV first, then Lavação 047, SC Plus, AS Serviços and Julia Beauty", () => {
|
||||||
const { container } = render(<CasesPage />);
|
const { container } = render(<CasesPage />);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -94,6 +94,7 @@ describe("CasesPage: cases de cliente", () => {
|
|||||||
"Lavação 047",
|
"Lavação 047",
|
||||||
"SC Plus Aquicultura",
|
"SC Plus Aquicultura",
|
||||||
"AS Serviços",
|
"AS Serviços",
|
||||||
|
"Julia Beauty",
|
||||||
]);
|
]);
|
||||||
// um botão de contato só na página: o do fecho
|
// um botão de contato só na página: o do fecho
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ describe("ProjectLightbox", () => {
|
|||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Fechar" }),
|
within(dialog).getByRole("button", { name: "Fechar" }),
|
||||||
).toHaveFocus();
|
).toHaveFocus();
|
||||||
expect(within(dialog).getByText("02 / 05")).toBeInTheDocument();
|
expect(within(dialog).getByText("02 / 06")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the desktop video in a browser frame by default and the phone version on request", () => {
|
it("shows the desktop video in a browser frame by default and the phone version on request", () => {
|
||||||
@@ -490,7 +490,7 @@ describe("ProjectLightbox: revisão", () => {
|
|||||||
it("announces which project is on screen", () => {
|
it("announces which project is on screen", () => {
|
||||||
const dialog = setup("nutriscan");
|
const dialog = setup("nutriscan");
|
||||||
expect(dialog.querySelector("[aria-live='polite']")).toHaveTextContent(
|
expect(dialog.querySelector("[aria-live='polite']")).toHaveTextContent(
|
||||||
"NutriScan, projeto 3 de 5",
|
"NutriScan, projeto 3 de 6",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ const WEIGHTS = [
|
|||||||
"nutriscan",
|
"nutriscan",
|
||||||
"sc-plus",
|
"sc-plus",
|
||||||
"as-servicos",
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
].map((id) => getShowcaseConfig(id)?.weight ?? 1);
|
].map((id) => getShowcaseConfig(id)?.weight ?? 1);
|
||||||
|
|
||||||
const originalRect = Element.prototype.getBoundingClientRect;
|
const originalRect = Element.prototype.getBoundingClientRect;
|
||||||
@@ -272,7 +273,7 @@ describe("ProjectReel: qual versão entra", () => {
|
|||||||
expect(reelEl()).toBeNull();
|
expect(reelEl()).toBeNull();
|
||||||
const list = listEl() as HTMLElement;
|
const list = listEl() as HTMLElement;
|
||||||
expect(list).not.toBeNull();
|
expect(list).not.toBeNull();
|
||||||
expect(list.querySelectorAll("li[data-device='desktop']")).toHaveLength(5);
|
expect(list.querySelectorAll("li[data-device='desktop']")).toHaveLength(6);
|
||||||
expect(list.querySelector(".ds-phone")).toBeNull();
|
expect(list.querySelector(".ds-phone")).toBeNull();
|
||||||
expect(list).not.toHaveAttribute("data-pending");
|
expect(list).not.toHaveAttribute("data-pending");
|
||||||
});
|
});
|
||||||
@@ -285,7 +286,7 @@ describe("ProjectReel: qual versão entra", () => {
|
|||||||
expect(reelEl()).toBeNull();
|
expect(reelEl()).toBeNull();
|
||||||
expect(
|
expect(
|
||||||
(listEl() as HTMLElement).querySelectorAll("li[data-device='mobile']"),
|
(listEl() as HTMLElement).querySelectorAll("li[data-device='mobile']"),
|
||||||
).toHaveLength(5);
|
).toHaveLength(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("path: phone width and reduced motion at once -> still the list", () => {
|
it("path: phone width and reduced motion at once -> still the list", () => {
|
||||||
@@ -330,13 +331,13 @@ describe("ProjectReel: qual versão entra", () => {
|
|||||||
renderReel();
|
renderReel();
|
||||||
expect(
|
expect(
|
||||||
(listEl() as HTMLElement).querySelectorAll("li[data-device='desktop']"),
|
(listEl() as HTMLElement).querySelectorAll("li[data-device='desktop']"),
|
||||||
).toHaveLength(5);
|
).toHaveLength(6);
|
||||||
|
|
||||||
changeMedia(MOBILE_QUERY, true);
|
changeMedia(MOBILE_QUERY, true);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
(listEl() as HTMLElement).querySelectorAll("li[data-device='mobile']"),
|
(listEl() as HTMLElement).querySelectorAll("li[data-device='mobile']"),
|
||||||
).toHaveLength(5);
|
).toHaveLength(6);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -780,7 +781,7 @@ describe("ProjectReel: rolagem suave até um projeto", () => {
|
|||||||
([options]) => options.top as number,
|
([options]) => options.top as number,
|
||||||
);
|
);
|
||||||
const finalTop = tops[tops.length - 1];
|
const finalTop = tops[tops.length - 1];
|
||||||
// o destino da Lavação (2º projeto) fica bem antes do da AS Serviços (último)
|
// o destino da Lavação (2º projeto) fica bem antes do da AS Serviços (5º)
|
||||||
expect(finalTop).toBeLessThan(reelTopFor(4));
|
expect(finalTop).toBeLessThan(reelTopFor(4));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const weights = [
|
|||||||
"nutriscan",
|
"nutriscan",
|
||||||
"sc-plus",
|
"sc-plus",
|
||||||
"as-servicos",
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
].map((id) => getShowcaseConfig(id)?.weight ?? 1);
|
].map((id) => getShowcaseConfig(id)?.weight ?? 1);
|
||||||
|
|
||||||
const originalRect = Element.prototype.getBoundingClientRect;
|
const originalRect = Element.prototype.getBoundingClientRect;
|
||||||
@@ -210,7 +211,7 @@ describe("ProjectReel", () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
expect(document.querySelector(".ds-reel")).toBeNull();
|
expect(document.querySelector(".ds-reel")).toBeNull();
|
||||||
const list = screen.getByRole("region", { name: "Projetos de clientes" });
|
const list = screen.getByRole("region", { name: "Projetos de clientes" });
|
||||||
expect(within(list).getAllByRole("heading", { level: 3 })).toHaveLength(5);
|
expect(within(list).getAllByRole("heading", { level: 3 })).toHaveLength(6);
|
||||||
expect(document.querySelector("video")).toBeNull();
|
expect(document.querySelector("video")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,7 +232,7 @@ describe("ProjectReel", () => {
|
|||||||
);
|
);
|
||||||
expect(posters.length).toBeGreaterThan(0);
|
expect(posters.length).toBeGreaterThan(0);
|
||||||
for (const poster of posters) expect(poster).toContain("mobile.webp");
|
for (const poster of posters) expect(poster).toContain("mobile.webp");
|
||||||
expect(list.querySelectorAll(".ds-phone")).toHaveLength(5);
|
expect(list.querySelectorAll(".ds-phone")).toHaveLength(6);
|
||||||
expect(list.querySelector(".ds-bframe")).toBeNull();
|
expect(list.querySelector(".ds-bframe")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,6 +259,7 @@ describe("ProjectReel", () => {
|
|||||||
"NutriScan",
|
"NutriScan",
|
||||||
"SC Plus Aquicultura",
|
"SC Plus Aquicultura",
|
||||||
"AS Serviços",
|
"AS Serviços",
|
||||||
|
"Julia Beauty",
|
||||||
])
|
])
|
||||||
expect(within(nav).getByRole("button", { name })).toBeInTheDocument();
|
expect(within(nav).getByRole("button", { name })).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
@@ -272,7 +274,7 @@ describe("ProjectReel", () => {
|
|||||||
it("hides the layers that are not on screen from assistive technology and from the keyboard", () => {
|
it("hides the layers that are not on screen from assistive technology and from the keyboard", () => {
|
||||||
renderReel();
|
renderReel();
|
||||||
const layers = [...document.querySelectorAll(".ds-reel-layer")];
|
const layers = [...document.querySelectorAll(".ds-reel-layer")];
|
||||||
expect(layers).toHaveLength(5);
|
expect(layers).toHaveLength(6);
|
||||||
for (const layer of layers.slice(1)) {
|
for (const layer of layers.slice(1)) {
|
||||||
expect(layer).toHaveAttribute("aria-hidden", "true");
|
expect(layer).toHaveAttribute("aria-hidden", "true");
|
||||||
expect(layer).toHaveAttribute("inert");
|
expect(layer).toHaveAttribute("inert");
|
||||||
@@ -338,7 +340,7 @@ describe("ProjectReel", () => {
|
|||||||
expect(first.currentTime).toBe(0);
|
expect(first.currentTime).toBe(0);
|
||||||
|
|
||||||
scrollTo(1);
|
scrollTo(1);
|
||||||
expect(caption()).toHaveTextContent("AS Serviços");
|
expect(caption()).toHaveTextContent("Julia Beauty");
|
||||||
const last = activeLayer().querySelector(
|
const last = activeLayer().querySelector(
|
||||||
".ds-reel-screen video",
|
".ds-reel-screen video",
|
||||||
) as HTMLVideoElement;
|
) as HTMLVideoElement;
|
||||||
@@ -381,7 +383,7 @@ describe("ProjectReel", () => {
|
|||||||
});
|
});
|
||||||
loaded = sources().filter(Boolean);
|
loaded = sources().filter(Boolean);
|
||||||
expect(
|
expect(
|
||||||
loaded.some((src) => src?.includes("as-servicos/scrub-desktop.mp4")),
|
loaded.some((src) => src?.includes("julia-beauty/scrub-desktop.mp4")),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(loaded.some((src) => src?.includes("sev-modaintima"))).toBe(false);
|
expect(loaded.some((src) => src?.includes("sev-modaintima"))).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -558,8 +560,8 @@ describe("ProjectReel", () => {
|
|||||||
).toHaveClass("ds-link-muted");
|
).toHaveClass("ds-link-muted");
|
||||||
expect(document.querySelector(".ds-reel .ds-link-accent")).toBeNull();
|
expect(document.querySelector(".ds-reel .ds-link-accent")).toBeNull();
|
||||||
|
|
||||||
// sem demoUrl não há "Abrir site"
|
// sem demoUrl não há "Abrir site" (AS Serviços, 5º projeto)
|
||||||
scrollTo(1);
|
scrollTo(reelOffset(weights, 4, 0.5));
|
||||||
expect(caption()).toHaveTextContent("AS Serviços");
|
expect(caption()).toHaveTextContent("AS Serviços");
|
||||||
expect(
|
expect(
|
||||||
within(caption()).queryByRole("link", { name: /Abrir site/ }),
|
within(caption()).queryByRole("link", { name: /Abrir site/ }),
|
||||||
@@ -635,7 +637,7 @@ describe("ProjectReel: a troca de projeto não duplica nada na tela", () => {
|
|||||||
expect(document.querySelectorAll(".ds-reel-title")).toHaveLength(1);
|
expect(document.querySelectorAll(".ds-reel-title")).toHaveLength(1);
|
||||||
expect(document.querySelectorAll(".ds-reel-chapter-count")).toHaveLength(1);
|
expect(document.querySelectorAll(".ds-reel-chapter-count")).toHaveLength(1);
|
||||||
expect(document.querySelectorAll(".ds-reel-actions")).toHaveLength(1);
|
expect(document.querySelectorAll(".ds-reel-actions")).toHaveLength(1);
|
||||||
expect(document.querySelectorAll(".ds-reel-layer")).toHaveLength(5);
|
expect(document.querySelectorAll(".ds-reel-layer")).toHaveLength(6);
|
||||||
for (const layer of document.querySelectorAll(".ds-reel-layer")) {
|
for (const layer of document.querySelectorAll(".ds-reel-layer")) {
|
||||||
expect(layer.closest(".ds-reel-caption")).toBeNull();
|
expect(layer.closest(".ds-reel-caption")).toBeNull();
|
||||||
expect(layer.querySelector(".ds-reel-caption")).toBeNull();
|
expect(layer.querySelector(".ds-reel-caption")).toBeNull();
|
||||||
@@ -782,8 +784,8 @@ describe("ProjectReel: teclado do índice (roving tabindex)", () => {
|
|||||||
it("makes the whole index ONE tab stop (the current project), however many projects and chapters it lists", () => {
|
it("makes the whole index ONE tab stop (the current project), however many projects and chapters it lists", () => {
|
||||||
renderReel();
|
renderReel();
|
||||||
const buttons = within(index()).getAllByRole("button");
|
const buttons = within(index()).getAllByRole("button");
|
||||||
// 5 projetos e os 2 trechos do projeto em cena: 7 botões, uma parada
|
// 6 projetos e os 2 trechos do projeto em cena: 8 botões, uma parada
|
||||||
expect(buttons).toHaveLength(5 + 2);
|
expect(buttons).toHaveLength(6 + 2);
|
||||||
expect(stops()).toHaveLength(1);
|
expect(stops()).toHaveLength(1);
|
||||||
expect(stops()[0]).toHaveAttribute("aria-current", "true");
|
expect(stops()[0]).toHaveAttribute("aria-current", "true");
|
||||||
expect(stops()[0]).toHaveAccessibleName("SEV Moda Íntima");
|
expect(stops()[0]).toHaveAccessibleName("SEV Moda Íntima");
|
||||||
@@ -895,7 +897,7 @@ describe("ProjectReel: teclado do índice (roving tabindex)", () => {
|
|||||||
fireEvent.keyDown(document.activeElement as Element, { key: "End" });
|
fireEvent.keyDown(document.activeElement as Element, { key: "End" });
|
||||||
expect(document.activeElement).toHaveAccessibleName(/Abrir site/);
|
expect(document.activeElement).toHaveAccessibleName(/Abrir site/);
|
||||||
|
|
||||||
scrollTo(1); // AS Serviços: sem site
|
scrollTo(reelOffset(weights, 4, 0.5)); // AS Serviços: sem site
|
||||||
act(() => {
|
act(() => {
|
||||||
jest.advanceTimersByTime(CAPTION_OUT_MS + 40);
|
jest.advanceTimersByTime(CAPTION_OUT_MS + 40);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ describe("buildShowcaseItems", () => {
|
|||||||
"nutriscan",
|
"nutriscan",
|
||||||
"sc-plus",
|
"sc-plus",
|
||||||
"as-servicos",
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
buildShowcaseItems(projects.filter((p) => p.id === "twins-lab")),
|
buildShowcaseItems(projects.filter((p) => p.id === "twins-lab")),
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async function expectDotsOnlyBetweenItemsOfOneLine(page: Page, where: string) {
|
|||||||
expect(strip, `${where}: a faixa 'Já entregue para:' no hero`).not.toBeNull();
|
expect(strip, `${where}: a faixa 'Já entregue para:' no hero`).not.toBeNull();
|
||||||
if (!strip) return 0;
|
if (!strip) return 0;
|
||||||
const { items, listLeft, listRight, overflowX } = strip;
|
const { items, listLeft, listRight, overflowX } = strip;
|
||||||
// 4 clientes + os anos de experiência
|
// 5 clientes + os anos de experiência
|
||||||
expect(items.length, `${where}: itens da faixa`).toBeGreaterThanOrEqual(
|
expect(items.length, `${where}: itens da faixa`).toBeGreaterThanOrEqual(
|
||||||
clientProjectNames().length,
|
clientProjectNames().length,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ const CTA_LABEL = "Falar sobre meu projeto";
|
|||||||
const SHORTCUT_MAX_TABS_TO_CTA = 20;
|
const SHORTCUT_MAX_TABS_TO_CTA = 20;
|
||||||
const SHORTCUT_MAX_TABS_AFTER_ENTER = 4;
|
const SHORTCUT_MAX_TABS_AFTER_ENTER = 4;
|
||||||
/**
|
/**
|
||||||
* Limite do caminho linear: 36 toques. Inventário das paradas legítimas, do topo da home ao campo de nome (35 hoje):
|
* Limite do caminho linear: 36 toques. Inventário das paradas legítimas, do topo da home ao campo de nome (36 hoje):
|
||||||
* 1 link "Pular para o conteúdo";
|
* 1 link "Pular para o conteúdo";
|
||||||
* cabeçalho: logo, 5 links (Cases, Serviços, Como trabalho, Sobre, IA) e "Falar sobre meu projeto" (7);
|
* cabeçalho: logo, 5 links (Cases, Serviços, Como trabalho, Sobre, IA) e "Falar sobre meu projeto" (7);
|
||||||
* hero: "Falar sobre meu projeto", "Ver projetos entregues", os 4 clientes da faixa, LinkedIn e e-mail (8);
|
* hero: "Falar sobre meu projeto", "Ver projetos entregues", os 5 clientes da faixa, LinkedIn e e-mail (9);
|
||||||
* vitrine de projetos: o seletor, o link do case do projeto aberto e "Ver os cases" (3);
|
* vitrine de projetos: o seletor, o link do case do projeto aberto e "Ver os cases" (3);
|
||||||
* ofertas: 3 botões e 3 links de case/página, mais "Ver todos os serviços" (7);
|
* ofertas: 3 botões e 3 links de case/página, mais "Ver todos os serviços" (7);
|
||||||
* "Ver trajetória completa" (1) e as 5 perguntas da FAQ (5);
|
* "Ver trajetória completa" (1) e as 5 perguntas da FAQ (5);
|
||||||
|
|||||||
+2
-2
@@ -38,11 +38,11 @@ async function expectReadable(locator: Locator, what: string) {
|
|||||||
.toBeGreaterThan(MIN_OPACITY);
|
.toBeGreaterThan(MIN_OPACITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
test("sem JavaScript a home mostra o contato e os nomes dos 4 clientes, legíveis", async ({
|
test("sem JavaScript a home mostra o contato e os nomes dos 5 clientes, legíveis", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const clients = clientProjectNames();
|
const clients = clientProjectNames();
|
||||||
expect(clients, "clientes em public/portfolio-data.json").toHaveLength(4);
|
expect(clients, "clientes em public/portfolio-data.json").toHaveLength(5);
|
||||||
|
|
||||||
const response = await page.goto("/", { waitUntil: "load" });
|
const response = await page.goto("/", { waitUntil: "load" });
|
||||||
expect(response?.status()).toBe(200);
|
expect(response?.status()).toBe(200);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ describe("public/portfolio-data.json", () => {
|
|||||||
["sc-plus", "cliente"],
|
["sc-plus", "cliente"],
|
||||||
["gcc-installer", "laboratorio"],
|
["gcc-installer", "laboratorio"],
|
||||||
["as-servicos", "cliente"],
|
["as-servicos", "cliente"],
|
||||||
|
["julia-beauty", "cliente"],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ import {
|
|||||||
const MAX_BYTES = 3 * 1024 * 1024;
|
const MAX_BYTES = 3 * 1024 * 1024;
|
||||||
const publicFile = (path: string) => join(process.cwd(), "public", path);
|
const publicFile = (path: string) => join(process.cwd(), "public", path);
|
||||||
/** Os projetos de cliente que a home entrega à vitrine. */
|
/** Os projetos de cliente que a home entrega à vitrine. */
|
||||||
const HOME_IDS = ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"];
|
const HOME_IDS = [
|
||||||
|
"sev-modaintima",
|
||||||
|
"lavacao-047",
|
||||||
|
"sc-plus",
|
||||||
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
|
];
|
||||||
|
|
||||||
describe("showcase length", () => {
|
describe("showcase length", () => {
|
||||||
it.each(SHOWCASE)("$id has at most two chapters", (config) => {
|
it.each(SHOWCASE)("$id has at most two chapters", (config) => {
|
||||||
@@ -35,7 +41,7 @@ describe("showcase length", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the whole Projects section within about three and a half screens", () => {
|
it("keeps the whole Projects section within about four screens", () => {
|
||||||
// o casco da vitrine tem (pin: 1 tela menos o cabeçalho) + (soma dos pesos) telas; o título e o botão de Projetos
|
// o casco da vitrine tem (pin: 1 tela menos o cabeçalho) + (soma dos pesos) telas; o título e o botão de Projetos
|
||||||
// somam ~468 px (0,52 tela de 900 px) fora dela, e o cabeçalho fixo tem 68 px
|
// somam ~468 px (0,52 tela de 900 px) fora dela, e o cabeçalho fixo tem 68 px
|
||||||
const SCREEN = 900;
|
const SCREEN = 900;
|
||||||
@@ -48,7 +54,7 @@ describe("showcase length", () => {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const section = SCREEN - HEADER + total * SCREEN + OUTSIDE;
|
const section = SCREEN - HEADER + total * SCREEN + OUTSIDE;
|
||||||
expect(section / SCREEN).toBeLessThanOrEqual(3.5);
|
expect(section / SCREEN).toBeLessThanOrEqual(4);
|
||||||
expect(total).toBeGreaterThan(1.5);
|
expect(total).toBeGreaterThan(1.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+13
-3
@@ -38,9 +38,9 @@ export const MAX_CHAPTERS = 2;
|
|||||||
* dos próprios vídeos, no máximo MAX_CHAPTERS por projeto, com os tempos originais: a abertura e o trecho principal,
|
* dos próprios vídeos, no máximo MAX_CHAPTERS por projeto, com os tempos originais: a abertura e o trecho principal,
|
||||||
* o par que deixa menos segundos de vídeo sob o rótulo do outro (o último trecho vai até o fim do vídeo).
|
* o par que deixa menos segundos de vídeo sob o rótulo do outro (o último trecho vai até o fim do vídeo).
|
||||||
*
|
*
|
||||||
* O peso é o mesmo em todos, meia altura de tela por projeto, um quarto de tela por capítulo: os quatro projetos de
|
* O peso é o mesmo em todos, meia altura de tela por projeto, um quarto de tela por capítulo: os cinco projetos de
|
||||||
* cliente da home (sev-modaintima, lavacao-047, sc-plus e as-servicos) prendem 2 telas de rolagem e a seção Projetos
|
* cliente da home (sev-modaintima, lavacao-047, sc-plus, as-servicos e julia-beauty) prendem 2,5 telas de rolagem e
|
||||||
* inteira, com o título e o botão, fica em cerca de 3,4 telas (eram 4,9). Dentro do projeto a rolagem também é
|
* a seção Projetos inteira, com o título e o botão, fica em cerca de 3,9 telas. Dentro do projeto a rolagem também é
|
||||||
* repartida por igual entre os capítulos (ver `videoFractionAtScroll`), qualquer que seja a duração deles no vídeo.
|
* repartida por igual entre os capítulos (ver `videoFractionAtScroll`), qualquer que seja a duração deles no vídeo.
|
||||||
*/
|
*/
|
||||||
export const SHOWCASE: readonly ShowcaseConfig[] = [
|
export const SHOWCASE: readonly ShowcaseConfig[] = [
|
||||||
@@ -95,6 +95,16 @@ export const SHOWCASE: readonly ShowcaseConfig[] = [
|
|||||||
{ label: "Contato", desktop: 2.4, mobile: 4.4 },
|
{ label: "Contato", desktop: 2.4, mobile: 4.4 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "julia-beauty",
|
||||||
|
accent: "#c6f19d",
|
||||||
|
durations: { desktop: 23.1, mobile: 24.97 },
|
||||||
|
weight: 0.5,
|
||||||
|
chapters: [
|
||||||
|
{ label: "Abertura", desktop: 0, mobile: 0 },
|
||||||
|
{ label: "Simulador de efeito", desktop: 2.5, mobile: 3.5 },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PROJECT_VIDEO_IDS = SHOWCASE.map((config) => config.id);
|
export const PROJECT_VIDEO_IDS = SHOWCASE.map((config) => config.id);
|
||||||
|
|||||||
+1
-1
@@ -246,7 +246,7 @@ it("keeps profile data timeless and sitemap output deterministic", () => {
|
|||||||
|
|
||||||
expect(first.map(({ url }) => url)).toEqual(expectedUrls);
|
expect(first.map(({ url }) => url)).toEqual(expectedUrls);
|
||||||
expect(second).toEqual(first);
|
expect(second).toEqual(first);
|
||||||
expect(first).toHaveLength(20);
|
expect(first).toHaveLength(21);
|
||||||
expect(
|
expect(
|
||||||
first.filter(({ url }) => url === `${siteUrl}/privacidade`),
|
first.filter(({ url }) => url === `${siteUrl}/privacidade`),
|
||||||
).toHaveLength(1);
|
).toHaveLength(1);
|
||||||
|
|||||||
@@ -4,10 +4,16 @@
|
|||||||
*
|
*
|
||||||
* listingRank (CC 2): o id está em `clientListingOrder` (a posição dele) | não está (o fim, depois de todos). O
|
* listingRank (CC 2): o id está em `clientListingOrder` (a posição dele) | não está (o fim, depois de todos). O
|
||||||
* `sort` é estável: os que não estão na lista ficam na ordem em que `caseStudies` os traz.
|
* `sort` é estável: os que não estão na lista ficam na ordem em que `caseStudies` os traz.
|
||||||
* Os dados reais só têm os quatro clientes da lista; o caminho do cliente fora da lista só existe aqui.
|
* Os dados reais só têm os cinco clientes da lista; o caminho do cliente fora da lista só existe aqui.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const LISTED = ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"];
|
const LISTED = [
|
||||||
|
"sev-modaintima",
|
||||||
|
"lavacao-047",
|
||||||
|
"sc-plus",
|
||||||
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
|
];
|
||||||
|
|
||||||
function loadWithClients(clientIds: readonly string[] | "all") {
|
function loadWithClients(clientIds: readonly string[] | "all") {
|
||||||
let loaded: typeof import("./site-content") | undefined;
|
let loaded: typeof import("./site-content") | undefined;
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ import {
|
|||||||
servicePages,
|
servicePages,
|
||||||
} from "./site-content";
|
} from "./site-content";
|
||||||
|
|
||||||
const clientIds = ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"];
|
const clientIds = [
|
||||||
|
"sev-modaintima",
|
||||||
|
"lavacao-047",
|
||||||
|
"sc-plus",
|
||||||
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
|
];
|
||||||
const labIds = ["nutriscan", "twins-lab", "reddit-explorer", "gcc-installer"];
|
const labIds = ["nutriscan", "twins-lab", "reddit-explorer", "gcc-installer"];
|
||||||
|
|
||||||
function getService(slug: string) {
|
function getService(slug: string) {
|
||||||
@@ -29,7 +35,7 @@ function getService(slug: string) {
|
|||||||
it("separates client work from the laboratory using the kind in the portfolio data", () => {
|
it("separates client work from the laboratory using the kind in the portfolio data", () => {
|
||||||
expect(clientCases.map(({ id }) => id)).toEqual(clientIds);
|
expect(clientCases.map(({ id }) => id)).toEqual(clientIds);
|
||||||
expect(labCases.map(({ id }) => id)).toEqual(labIds);
|
expect(labCases.map(({ id }) => id)).toEqual(labIds);
|
||||||
expect(caseStudies).toHaveLength(8);
|
expect(caseStudies).toHaveLength(9);
|
||||||
expect(
|
expect(
|
||||||
caseStudies.every(
|
caseStudies.every(
|
||||||
(item) => isClientCase(item) === clientIds.includes(item.id),
|
(item) => isClientCase(item) === clientIds.includes(item.id),
|
||||||
@@ -214,6 +220,7 @@ it("lists the client cases for the mosaic and /cases in the order of the home sh
|
|||||||
"lavacao-047",
|
"lavacao-047",
|
||||||
"sc-plus",
|
"sc-plus",
|
||||||
"as-servicos",
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
]);
|
]);
|
||||||
expect(clientCasesForListing).toHaveLength(clientCases.length);
|
expect(clientCasesForListing).toHaveLength(clientCases.length);
|
||||||
});
|
});
|
||||||
|
|||||||
+42
-2
@@ -821,6 +821,45 @@ export const caseStudies: CaseStudyContent[] = [
|
|||||||
demoUrl: null,
|
demoUrl: null,
|
||||||
role: "Desenvolvimento da primeira landing page do site, com animações 3D, e de um painel para controle de projetos.",
|
role: "Desenvolvimento da primeira landing page do site, com animações 3D, e de um painel para controle de projetos.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "julia-beauty",
|
||||||
|
title: "Julia Beauty",
|
||||||
|
subtitle: "Site de estúdio de extensão de cílios",
|
||||||
|
metaTitle: "Julia Beauty: site de extensão de cílios",
|
||||||
|
metaDescription:
|
||||||
|
"Site do estúdio de extensão de cílios Julia Beauty, em Camboriú (SC): simulador de efeito no olho, SEO local e agendamento pelo WhatsApp.",
|
||||||
|
headline:
|
||||||
|
"Site com simulador de efeito e agendamento pelo WhatsApp para um estúdio de cílios em Camboriú",
|
||||||
|
summary:
|
||||||
|
"Site da Julia Beauty, o estúdio de extensão de cílios da lash designer Julia, em Camboriú-SC. A cliente testa o efeito no formato do próprio olho, conhece as técnicas e os cuidados e agenda pelo WhatsApp.",
|
||||||
|
impact:
|
||||||
|
"A cliente testa o efeito antes de marcar e chama o estúdio no WhatsApp com a mensagem já pronta.",
|
||||||
|
context:
|
||||||
|
"A Julia Beauty é o estúdio de extensão de cílios da lash designer Julia, em Camboriú, Santa Catarina. Ela atende uma cliente por vez, com hora marcada, e agenda as sessões pelo WhatsApp. O site apresenta as técnicas, os cuidados, o endereço e o horário, e leva a cliente à conversa.",
|
||||||
|
problem:
|
||||||
|
"Quem procura extensão de cílios chega com dúvidas antes de marcar: qual efeito combina com o meu olho, qual técnica escolher, como é a sessão e que cuidados vêm depois. O estúdio atende uma cliente por vez e marca tudo pelo WhatsApp, e precisa ser encontrado por quem busca o serviço em Camboriú. O site tinha de responder a essas perguntas, abrir rápido no celular e levar a cliente ao agendamento.",
|
||||||
|
deliveries: [
|
||||||
|
"Simulador de efeito: a cliente escolhe o formato do olho e o efeito e vê o desenho dos fios, com o tamanho de cada um em milímetros.",
|
||||||
|
"Agendamento pelo WhatsApp com mensagem pronta, com endereço, horário e link para o mapa na própria página.",
|
||||||
|
"Um painel de fotos que troca de imagem conforme a cliente rola a página: o ritual, a Julia, as técnicas e os cuidados.",
|
||||||
|
"As etapas da sessão, a tabela de técnicas e as perguntas frequentes, para a cliente chegar sabendo como tudo funciona.",
|
||||||
|
"Informações organizadas para o Google e para assistentes de IA entenderem o que o estúdio faz e onde fica.",
|
||||||
|
],
|
||||||
|
capabilities: [
|
||||||
|
"A cliente testa o efeito no formato do próprio olho antes de marcar a sessão.",
|
||||||
|
"A cliente chama o estúdio no WhatsApp com a mensagem pronta, já sabendo endereço, horário e cuidados.",
|
||||||
|
"Buscadores e assistentes de IA leem endereço, horário, técnicas e perguntas frequentes na própria página.",
|
||||||
|
],
|
||||||
|
updatedAt: "2026-10-24",
|
||||||
|
stack: ["HTML", "CSS", "JavaScript", "JSON-LD", "Vercel", "Gitea Actions"],
|
||||||
|
decisions: [
|
||||||
|
"HTML, CSS e JavaScript puros, sem build e sem dependências: a página inteira fica em um arquivo, com o estilo e as interações à parte.",
|
||||||
|
"Os dados estruturados (JSON-LD) descrevem o estúdio como BeautySalon, com endereço, horário e técnicas, e as perguntas frequentes como FAQPage; o site publica também Open Graph, sitemap, robots.txt e llms.txt.",
|
||||||
|
"Cada push dispara o Gitea Actions, que publica na Vercel: a branch principal vai para produção e as outras geram uma prévia.",
|
||||||
|
"Medido antes da publicação, o Lighthouse no celular deu 100 em desempenho, acessibilidade, boas práticas e SEO.",
|
||||||
|
],
|
||||||
|
demoUrl: "https://juliabeauty.com.br",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const clientProjectIds = new Set(
|
const clientProjectIds = new Set(
|
||||||
@@ -846,6 +885,7 @@ const clientListingOrder = [
|
|||||||
"lavacao-047",
|
"lavacao-047",
|
||||||
"sc-plus",
|
"sc-plus",
|
||||||
"as-servicos",
|
"as-servicos",
|
||||||
|
"julia-beauty",
|
||||||
];
|
];
|
||||||
|
|
||||||
function listingRank({ id }: { id: string }) {
|
function listingRank({ id }: { id: string }) {
|
||||||
@@ -859,9 +899,9 @@ export const clientCasesForListing = [...clientCases].sort(
|
|||||||
|
|
||||||
/** Data (AAAA-MM-DD) da última mudança no conteúdo de cada página fixa: vira o `lastmod` do mapa do site. */
|
/** Data (AAAA-MM-DD) da última mudança no conteúdo de cada página fixa: vira o `lastmod` do mapa do site. */
|
||||||
export const pageUpdatedAt = {
|
export const pageUpdatedAt = {
|
||||||
home: "2026-10-14",
|
home: "2026-10-24",
|
||||||
servicos: "2026-10-14",
|
servicos: "2026-10-14",
|
||||||
cases: "2026-10-14",
|
cases: "2026-10-24",
|
||||||
ia: "2026-10-14",
|
ia: "2026-10-14",
|
||||||
curriculo: "2026-10-14",
|
curriculo: "2026-10-14",
|
||||||
// a própria página informa a vigência (13 de setembro de 2026)
|
// a própria página informa a vigência (13 de setembro de 2026)
|
||||||
|
|||||||
@@ -301,6 +301,40 @@
|
|||||||
"complexity": 5,
|
"complexity": 5,
|
||||||
"teamSize": "Em equipe"
|
"teamSize": "Em equipe"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "julia-beauty",
|
||||||
|
"kind": "cliente",
|
||||||
|
"title": "Julia Beauty",
|
||||||
|
"subtitle": "Site de estúdio de extensão de cílios",
|
||||||
|
"description": "Site estático do estúdio de extensão de cílios da lash designer Julia, em Camboriú (SC), feito em HTML, CSS e JavaScript sem build: simulador de efeito no olho, painel de fotos que troca com a rolagem e agendamento pelo WhatsApp.",
|
||||||
|
"longDescription": "Página única com SEO local: Open Graph, dados estruturados JSON-LD (BeautySalon e FAQPage), sitemap, robots.txt e llms.txt. O deploy na Vercel roda pelo Gitea Actions a cada push. Medido antes da publicação, o Lighthouse no celular deu 100 em desempenho, acessibilidade, boas práticas e SEO.",
|
||||||
|
"technologies": [
|
||||||
|
"HTML",
|
||||||
|
"CSS",
|
||||||
|
"JavaScript",
|
||||||
|
"JSON-LD",
|
||||||
|
"Vercel",
|
||||||
|
"Gitea Actions"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"Simulador de efeito no olho",
|
||||||
|
"Painel de fotos que troca com a rolagem",
|
||||||
|
"Agendamento pelo WhatsApp com mensagem pronta",
|
||||||
|
"SEO local com JSON-LD, sitemap e llms.txt",
|
||||||
|
"Deploy na Vercel por Gitea Actions"
|
||||||
|
],
|
||||||
|
"category": "site",
|
||||||
|
"level": "pleno",
|
||||||
|
"status": "Em Produção",
|
||||||
|
"demoUrl": "https://juliabeauty.com.br",
|
||||||
|
"githubUrl": "",
|
||||||
|
"badges": ["Site estático", "Em Produção"],
|
||||||
|
"featured": true,
|
||||||
|
"metrics": {
|
||||||
|
"complexity": 3,
|
||||||
|
"teamSize": "Individual"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"skills": {
|
"skills": {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
Reference in new issue
Block a user