5 jogos melhorados — o dia que o portfólio virou vitrine
🚀 Portfólio·

5 jogos melhorados — o dia que o portfólio virou vitrine

📖 7 min de leitura← Voltar para timeline

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: requestAnimationFrame em 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

  1. UX invisível é a melhor UX. Antes os jogos existiam mas ninguém sabia. Um carrossel + capa + play button mudou completamente o engajamento.

  2. Iframe bem feito não é gambiarra. Com moldura temática, loading state e limpeza de memória, a experiência fica integrada.

  3. API route como proxy é subestimado. Serve arquivos estáticos com headers controlados, sem precisar de um servidor extra.

  4. Dados falam. Umami analytics mostrou padrões claros — code-typing é o favorito, asteroid-dodge tem o maior tempo médio por sessão.

  5. 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.


~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$