
5 jogos melhorados — o dia que o portfólio virou vitrine
O portfólio já tinha estrutura, animações, i18n — mas faltava alma. Os 5 mini-games estavam lá, funcionais, mas meio esquecidos. Cada um feito num momento diferente, com estilos e qualidades diferentes.
Chegou a hora de tratar eles como vitrine, não como enfeite.
Onde estavam
Os jogos nasceram como arquivos HTML estáticos em public/games/, cada um num subdiretório:
public/games/
├── simon-game/ # Jogo da memória sequencial (Simon Says)
├── asteroid-dodge/ # Desvio de asteroides em espaço 2D
├── code-typing/ # Teste de velocidade de digitação
├── memory-matrix/ # Memorização de grade luminosa
└── terminal/ # Simulador de terminal interativo
Funcionavam — mas eram acessados só por URL direta. Não tinham presença na home. Não tinham capa. Não tinham contexto.
O que mudei
GameShowcase — a vitrine horizontal
O primeiro passo foi criar um componente carrossel com scroll horizontal que exibisse cada jogo com:
- Imagem de capa (webp 180×110)
- Nome e descrição puxados do repositório
- Botão de play que carrega o jogo inline
- Embed dedicado com loading state e botão de fechar
// GameShowcase.tsx — a vitrine que faltava
const GAME_IMAGES: Record<string, string> = {
"simon-game": "/games/simon-game.webp",
"asteroid-dodge": "/games/asteroid-dodge.webp",
"code-typing": "/games/code-typing.webp",
"memory-matrix": "/games/memory-matrix.webp",
"terminal": "/games/terminal.webp",
};
O scroll é nativo (overflow-x: auto), com setas de navegação e snap-points pra não deixar o card pela metade. Funciona no celular arrastando, no desktop com scroll wheel.
API route — proxy inteligente
Criei uma API route /api/game/[slug] que serve o HTML estático de cada jogo. Isso me deu controle sobre headers de cache, CSP e logging sem precisar modificar os HTMLs originais.
// app/api/game/[slug]/route.ts
export async function GET(
_request: Request,
{ params }: { params: { slug: string } }
) {
const gamePath = path.join(process.cwd(), 'public', 'games', params.slug, 'index.html')
if (!fs.existsSync(gamePath)) {
return new Response('Game not found', { status: 404 })
}
const html = fs.readFileSync(gamePath, 'utf-8')
return new Response(html, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
},
})
}
Analytics em cada jogada
Cada clique em “Play” dispara um evento no Umami com o nome do jogo. Em duas semanas já vi padrão: code-typing é o mais jogado, seguido de asteroid-dodge.
const playGame = useCallback((name: string, url: string | null) => {
track({ type: "game_play", game: name })
// ... carrega o iframe
}, [track])
Melhorias individuais
Cada jogo recebeu atenção específica:
Simon Game:
- Cores mais vibrantes com glow no hover
- Animação de escala no clique (110% → 100%)
- Score persistente durante a sessão
Asteroid Dodge:
- Touch events habilitados (
touch-action: none) - Performance:
requestAnimationFrameem vez de setInterval - Mobile-first: tela cheia no celular
Memory Matrix:
- Grid responsivo (3×3 no mobile, 4×4 no desktop)
- Timer visual com barra de progresso
- Dificuldade progressiva a cada rodada
Code Typing:
- Banco de snippets expandido (30+ trechos reais de código)
- WPM + precisão em tempo real
- Destaque syntax do trecho atual
Terminal:
- 15 comandos temáticos do portfólio
- Efeito de digitação com velocidade variável
- Histórico de comandos via sessionStorage
O embed que virou moldura
A parte mais legal tecnicamente foi o sistema de embed. Quando o usuário clica em “Play”, um iframe é criado dinamicamente dentro de uma moldura com tema do portfólio:
┌─ 🟢 simon-game ───────────────────── [✕] ─┐
│ │
│ [jogo carregado aqui] │
│ │
└─────────────────────────────────────────────┘
A moldura tem:
- Status indicator verde (como um terminal real)
- Título dinâmico do jogo
- Botão de fechar que limpa o iframe (libera memória)
- Loading spinner enquanto o HTML não carrega
{loading && (
<div className="flex items-center justify-center h-[450px]">
<div className="w-8 h-8 border-2 border-[var(--accent)]
border-t-transparent rounded-full animate-spin" />
<span className="text-xs font-mono animate-pulse">
Carregando jogo...
</span>
</div>
)}
Resultado
Antes da reforma, os 5 jogos existiam mas ninguém via. Depois:
- +240% de interação na seção de jogos (Umami)
- ~45s de tempo médio por sessão de jogo
- Code Typing lidera com 38% das jogadas
- Zero reclamação de performance (tudo vanilla ou React production)
O portfólio virou vitrine de verdade — não só mostra código, mostra o que esse código faz funcionar.
O que aprendi
-
UX invisível é a melhor UX. Antes os jogos existiam mas ninguém sabia. Um carrossel + capa + play button mudou completamente o engajamento.
-
Iframe bem feito não é gambiarra. Com moldura temática, loading state e limpeza de memória, a experiência fica integrada.
-
API route como proxy é subestimado. Serve arquivos estáticos com headers controlados, sem precisar de um servidor extra.
-
Dados falam. Umami analytics mostrou padrões claros — code-typing é o favorito, asteroid-dodge tem o maior tempo médio por sessão.
-
Mini-games não são só enfeite. São a parte mais interativa do portfólio. As pessoas passam mais tempo jogando do que lendo sobre habilidades.