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
@@ -21,6 +21,7 @@ describe("public/portfolio-data.json", () => {
|
||||
["sc-plus", "cliente"],
|
||||
["gcc-installer", "laboratorio"],
|
||||
["as-servicos", "cliente"],
|
||||
["julia-beauty", "cliente"],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,13 @@ import {
|
||||
const MAX_BYTES = 3 * 1024 * 1024;
|
||||
const publicFile = (path: string) => join(process.cwd(), "public", path);
|
||||
/** 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", () => {
|
||||
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
|
||||
// somam ~468 px (0,52 tela de 900 px) fora dela, e o cabeçalho fixo tem 68 px
|
||||
const SCREEN = 900;
|
||||
@@ -48,7 +54,7 @@ describe("showcase length", () => {
|
||||
true,
|
||||
);
|
||||
const section = SCREEN - HEADER + total * SCREEN + OUTSIDE;
|
||||
expect(section / SCREEN).toBeLessThanOrEqual(3.5);
|
||||
expect(section / SCREEN).toBeLessThanOrEqual(4);
|
||||
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,
|
||||
* 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
|
||||
* cliente da home (sev-modaintima, lavacao-047, sc-plus e as-servicos) prendem 2 telas de rolagem e a seção Projetos
|
||||
* 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 é
|
||||
* 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, as-servicos e julia-beauty) prendem 2,5 telas de rolagem e
|
||||
* 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.
|
||||
*/
|
||||
export const SHOWCASE: readonly ShowcaseConfig[] = [
|
||||
@@ -95,6 +95,16 @@ export const SHOWCASE: readonly ShowcaseConfig[] = [
|
||||
{ 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);
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ it("keeps profile data timeless and sitemap output deterministic", () => {
|
||||
|
||||
expect(first.map(({ url }) => url)).toEqual(expectedUrls);
|
||||
expect(second).toEqual(first);
|
||||
expect(first).toHaveLength(20);
|
||||
expect(first).toHaveLength(21);
|
||||
expect(
|
||||
first.filter(({ url }) => url === `${siteUrl}/privacidade`),
|
||||
).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
|
||||
* `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") {
|
||||
let loaded: typeof import("./site-content") | undefined;
|
||||
|
||||
@@ -17,7 +17,13 @@ import {
|
||||
servicePages,
|
||||
} 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"];
|
||||
|
||||
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", () => {
|
||||
expect(clientCases.map(({ id }) => id)).toEqual(clientIds);
|
||||
expect(labCases.map(({ id }) => id)).toEqual(labIds);
|
||||
expect(caseStudies).toHaveLength(8);
|
||||
expect(caseStudies).toHaveLength(9);
|
||||
expect(
|
||||
caseStudies.every(
|
||||
(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",
|
||||
"sc-plus",
|
||||
"as-servicos",
|
||||
"julia-beauty",
|
||||
]);
|
||||
expect(clientCasesForListing).toHaveLength(clientCases.length);
|
||||
});
|
||||
|
||||
+42
-2
@@ -821,6 +821,45 @@ export const caseStudies: CaseStudyContent[] = [
|
||||
demoUrl: null,
|
||||
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(
|
||||
@@ -846,6 +885,7 @@ const clientListingOrder = [
|
||||
"lavacao-047",
|
||||
"sc-plus",
|
||||
"as-servicos",
|
||||
"julia-beauty",
|
||||
];
|
||||
|
||||
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. */
|
||||
export const pageUpdatedAt = {
|
||||
home: "2026-10-14",
|
||||
home: "2026-10-24",
|
||||
servicos: "2026-10-14",
|
||||
cases: "2026-10-14",
|
||||
cases: "2026-10-24",
|
||||
ia: "2026-10-14",
|
||||
curriculo: "2026-10-14",
|
||||
// a própria página informa a vigência (13 de setembro de 2026)
|
||||
|
||||
Reference in new issue
Block a user