Site pessoal de Bruno Amorim, Senior Software Engineer. Construído com Next.js, TypeScript e Tailwind CSS, com tema claro/escuro.
- Início — apresentação, links sociais (LinkedIn, GitHub, e-mail), projetos em destaque e a cena WebGL.
- Projetos — projetos mobile e web com imagem, descrição e stack de cada um.
- Sobre — experiência profissional, formação e tecnologias.
- Agendar — página
/schedule, onde qualquer pessoa reserva um horário direto na minha agenda do Google.
O movimento do site é todo GSAP, concentrado em poucos componentes reutilizáveis:
Reveal (entrada por rolagem), Headline
(título que sobe linha a linha, com SplitText), Counter,
Parallax, Magnetic e
ScrollProgress. Os plugins são registrados num
lugar só, em lib/gsap.ts.
O estado inicial dos blocos que entram por rolagem mora no CSS ([data-reveal]), e não
num gsap.set na montagem: assim o conteúdo já chega escondido no HTML do servidor, sem
piscar. Quem não executa JavaScript recebe tudo à vista pelo <noscript> do _document.
Quem pede prefers-reduced-motion recebe o estado final direto, sem transição.
No herói, a palavra “apps” do título carrega os apps de verdade:
AppSwarm ancora uma camada de ícones na palavra e os
lança numa fileira acima dela no hover — em tela de toque eles aparecem sozinhos, uma
vez, e voltam a aparecer no toque. A lista sai de shippedApps, em
lib/projects.ts: primeiro os produtos que têm case aqui, depois os
apps publicados dentro de time de produto, que existem só como ícone. Para incluir mais
um, basta o arquivo em public/projects/shots/ e uma linha em shippedElsewhere. A camada é irmã do título, e não filha: as linhas do
SplitText viram máscaras com overflow: clip e cortariam qualquer coisa que saísse da
palavra.
Cada ícone no ar é um link para a página do produto: o case daqui quando ele existe, a loja quando o app só existe lá. Dois detalhes fazem isso parar de pé. O clique só liga enquanto o leque está aberto — parados, os ícones se empilham sobre a própria palavra e roubariam dela o hover que abre tudo. E entre a palavra e a fileira existe uma ponte invisível, dimensionada junto com o arco, porque sem ela o ponteiro “sairia” do leque no vão que separa um do outro. Os links ficam fora da ordem de tabulação: o mesmo produto aparece logo abaixo, listado com nome e case.
A cena WebGL da home é o Console3D: a unidade de estúdio
com botões que giram de verdade, sem nenhum arquivo de modelo ou textura — o painel
inteiro é desenhado num canvas 2D em tempo de execução, e desse mesmo traçado sai o mapa
de normais, por isso os rótulos são gravados no metal em vez de impressos. As ferramentas
que fabricam esse alumínio (escovado, gravação e o Sobel que vira relevo) ficam em
lib/machining.ts.
O ScrollTrigger só escreve o progresso da seção num ref e o loop do R3F decide o que fazer com ele: a rolagem não passa pelo estado do React.
yarn install
yarn devAbra http://localhost:3000 no navegador.
Sem as variáveis do Google configuradas o site inteiro funciona normalmente; só a página /schedule mostra o aviso de agenda indisponível.
A página lê a disponibilidade real da agenda via freeBusy e cria o evento com convite e sala do Google Meet. Não há banco de dados: a agenda do Google é a fonte de verdade.
As regras de horário ficam todas em lib/schedule/config.ts: dias úteis, das 13h às 18h no horário de Brasília, blocos de 30 minutos, 30 dias de horizonte e 2 horas de antecedência mínima. Sexta fecha mais cedo, às 16h, pela exceção em DAY_END_HOUR_BY_WEEKDAY.
A conta é Gmail pessoal, então não dá para usar service account (isso exige domain-wide delegation, exclusivo do Workspace). O caminho é dar consentimento uma vez e guardar o refresh token.
- No Google Cloud Console, crie um projeto e habilite a Google Calendar API.
- Configure a tela de consentimento OAuth como External e clique em Publish app.
Enquanto o app ficar em modo Testing o refresh token expira em 7 dias e a página para de funcionar sozinha. Publicado, ele não expira por tempo.
Os escopos usados são dois, e os dois são necessários:
calendar.eventspara criar o evento ecalendar.freebusypara ler a ocupação. OfreeBusynão aceitacalendar.events, então só com ele a página de horários responde 403. - Crie uma credencial OAuth client ID do tipo Web application com a redirect URI
http://localhost:4455/oauth2callback. Ela serve só para o consentimento local: em produção o servidor troca o refresh token por access token direto, sem redirect. O domínio do site não entra aqui. - Copie
.env.local.examplepara.env.locale preenchaGOOGLE_CLIENT_IDeGOOGLE_CLIENT_SECRET. - Rode o script abaixo e aceite o consentimento. Ele grava o
GOOGLE_REFRESH_TOKENno.env.localsem imprimir o valor no terminal.
node scripts/google-oauth.mjsEm produção, as mesmas quatro variáveis precisam existir no projeto da Vercel.
O formulário tem honeypot, limite de tamanho nos campos e um freio por IP. A defesa principal, porém, é o servidor recalcular a grade de horários e recusar qualquer start que não seja um bloco válido, o que impede a rota de virar um criador de eventos arbitrários. Se aparecer abuso de verdade, o próximo passo é ligar o Vercel Firewall.
| Comando | Descrição |
|---|---|
yarn dev |
Servidor de desenvolvimento |
yarn build |
Build de produção |
yarn start |
Servir o build de produção |
yarn lint |
ESLint |
Next.js · React · TypeScript · Tailwind CSS · next-themes · GSAP (ScrollTrigger, SplitText) · three.js / react-three-fiber
Otimizado para deploy na Vercel.