Recentemente, enquanto assistia minha filha 🧒🏻 jogar jogos de memória gratuitos em seu tablet, notei que ela estava tendo dificuldades com uma quantidade avassaladora de anúncios e banners pop-up irritantes.

Isso me inspirou a construir um jogo semelhante para ela. Como ela está atualmente interessada em animes, decidi criar o jogo usando imagens fofas no estilo anime.

Neste artigo, vou orientá-lo pelo processo de construção do jogo para você ou seus filhos 🎮.

Começaremos explorando as características do jogo, e depois abordaremos a pilha de tecnologia e a estrutura do projeto — ambas são simples. Por fim, discutiremos otimizações e garantiremos uma jogabilidade suave em dispositivos móveis 📱.

Se você quiser pular a leitura, aqui 💁 está o repositório do GitHub 🙌. E aqui você pode ver a demonstração ao vivo.

Índice

Descrição do Projeto

Neste tutorial, vamos construir um desafiador jogo de memória de cartas com React que testa suas habilidades de recordação. Seu objetivo é clicar em imagens de anime únicas sem clicar na mesma duas vezes. Cada clique único ganha pontos, mas cuidado – clicar em uma imagem duas vezes redefine seu progresso.

Recursos do jogo:

  • 🎯 Jogabilidade dinâmica que desafia sua memória

  • 🔄 As cartas são embaralhadas após cada clique para aumentar a dificuldade

  • 🏆 Rastreamento de pontuação com persistência da melhor pontuação

  • 😺 Imagens de anime adoráveis da API The Nekosia

  • ✨ Transições e animações de carregamento suaves

  • 📱 Design responsivo para todos os dispositivos

  • 🎨 UI limpa e moderna

O jogo ajudará você a testar suas habilidades de memória enquanto desfruta de lindas imagens de anime. Você consegue alcançar a pontuação perfeita?

Como Jogar

  1. Clique em qualquer carta para começar

  2. Lembre-se de quais cartas você clicou

  3. Tente clicar em todas as cartas exatamente uma vez

  4. Veja sua pontuação crescer a cada seleção única

  5. Depois continue jogando para tentar superar sua melhor pontuação

A Pilha de Tecnologias

Aqui está uma lista das principais tecnologias que usaremos:

  • NPM – Um gerenciador de pacotes para JavaScript que ajuda a gerenciar dependências e scripts para o projeto.

  • Vite – Uma ferramenta de construção que fornece um ambiente de desenvolvimento rápido, especialmente otimizado para projetos web modernos.

  • React – Uma biblioteca JavaScript popular para a construção de interfaces de usuário, permitindo renderização eficiente e gerenciamento de estado.

  • Módulos CSS – Uma solução de estilização que delimita o CSS para componentes individuais, evitando conflitos de estilo e garantindo a manutenibilidade.

Vamos Construir o Jogo

De agora em diante, vou orientá-lo pelo processo que segui ao construir este jogo.

Arquitetura e Estrutura do Projeto

Ao construir este jogo de cartas de memória, organizei cuidadosamente a base de código para garantir manutenibilidade, escalabilidade e clara separação de preocupações. Vamos explorar a estrutura e o raciocínio por trás de cada decisão:

Arquitetura Baseada em Componentes

Optei por uma arquitetura baseada em componentes por várias razões:

  • Modularidade: Cada componente é autônomo, com sua própria lógica e estilos

  • Reutilização: Componentes como Card e Loader podem ser reutilizados em toda a aplicação

  • Manutenabilidade: Mais fácil de depurar e modificar componentes individuais

  • Teste: Componentes podem ser testados em isolamento

Organização de Componentes

  1. Componente Card
  • Separado em seu próprio diretório porque é um elemento central do jogo

  • Contém módulos JSX e SCSS para encapsulamento

  • Gerencia a renderização individual de cartas, estados de carregamento e eventos de clique

  1. Componente CardsGrid
  • Gerencia o layout do tabuleiro do jogo

  • Gerencia a mistura e distribuição das cartas

  • Controla o layout de grade responsivo para diferentes tamanhos de tela

  1. Componente de Carregamento
  • Indicador de carregamento reutilizável

  • Melhora a experiência do usuário durante o carregamento de imagens

  • Pode ser usado por qualquer componente que necessita de estados de carregamento

  1. Componentes de Cabeçalho/Rodapé/Subtítulo
  • Componentes estruturais para o layout do aplicativo

  • O cabeçalho exibe o título do jogo e pontuações

  • O rodapé mostra informações de direitos autorais e versão

  • O subtítulo fornece instruções do jogo

Abordagem de Módulos CSS

Utilizei Módulos CSS (arquivos .module.scss) por diversos benefícios:

  • Estilização Limitada: Evita vazamentos de estilos entre componentes

  • Colisões de Nomes: Gera automaticamente nomes de classes únicos

  • Manutenibilidade: Estilos estão co-localizados com seus componentes

  • Recursos SCSS: Aproveita recursos SCSS enquanto mantém os estilos modulares

Hooks Personalizados

O diretório hooks contém hooks personalizados como useFetch:

  • Separação de Preocupações: Isola a lógica de busca de dados

  • Reutilizabilidade: Pode ser usado por qualquer componente que necessite de dados de imagem

  • Gerenciamento de Estado: Lida com estados de carregamento, erro e dados

  • Desempenho: Implementa otimizações como controle de tamanho de imagem

Arquivos de Nível Raiz

App.jsx:

  • Atua como o ponto de entrada da aplicação

  • Gerencia o estado global e o roteamento (se necessário)

  • Coordena a composição dos componentes

  • Trata dos layouts de alto nível

Considerações de Desempenho

A estrutura suporta otimizações de desempenho:

  • Divisão de Código: Os componentes podem ser carregados sob demanda, se necessário

  • Memoização: Os componentes podem ser memoizados de forma eficaz

  • Carregamento de Estilos: Os Módulos CSS possibilitam um carregamento eficiente de estilos

  • Gerenciamento de Ativos: Imagens e recursos são organizados adequadamente

Escalabilidade

Esta estrutura permite uma escalabilidade fácil:

  • Novos recursos podem ser adicionados como novos componentes

  • Novos hooks podem ser criados para novas funcionalidades

  • Os estilos permanecem mantíveis conforme o aplicativo cresce

  • Os testes podem ser implementados em qualquer nível

Experiência de Desenvolvimento

A estrutura aprimora a experiência do desenvolvedor:

  • Organização clara dos arquivos

  • Locais de componentes intuitivos

  • Fácil encontrar e modificar funcionalidades específicas

  • Suporta colaboração eficiente

Esta arquitetura se mostrou particularmente valiosa ao otimizar o jogo para uso em tablets, pois me permitiu:

  1. Identificar e otimizar facilmente gargalos de desempenho

  2. Adicionar estilos específicos para tablet sem afetar outros dispositivos

  3. Implementar estados de carregamento para uma melhor experiência móvel

  4. Manter uma separação clara entre a lógica do jogo e os componentes da interface do usuário

Ok, agora vamos começar a codificar.

Guia de Construção Passo a Passo

1. Configuração do Projeto

Configurar o Ambiente de Desenvolvimento

Para começar um projeto React limpo, abra o seu aplicativo de terminal e execute os seguintes comandos (você pode nomear a pasta do seu projeto como desejar – no meu caso, o nome é ‘memory-card’):

npm create vite@latest memory-card -- --template react
cd memory-card
npm install

Instalar as Dependências Necessárias

As únicas dependências que iremos utilizar neste projeto são o pacote hook da UI.dev (aliás, aqui você pode encontrar um artigo bem explicado sobre como o rendering no React funciona).

A outra dependência é o famoso pré-processador CSS, SASS, que precisaremos para escrever nossos módulos CSS em SASS em vez de CSS regular.

npm install @uidotdev/usehooks sass

Configurar o Vite e as Configurações do Projeto

Ao configurar nosso projeto, precisamos fazer alguns ajustes específicos de configuração para lidar com avisos do SASS e melhorar nossa experiência de desenvolvimento. Veja como você pode configurar o Vitest:

// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/setupTests.js'],
    css: {
      modules: {
        classNameStrategy: 'non-scoped'
      }
    },
    preprocessors: {
      '**/*.scss': 'sass'
    },
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/setupTests.js',
        'src/main.jsx',
        'src/vite-env.d.ts',
      ],
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        quietDeps: true,  // Silencia os avisos de dependência do SASS
        charset: false    // Impede o aviso de charset em versões recentes do SASS
      }
    }
  }
});

Tenha em mente que a maioria dessas configurações são geradas automaticamente para você ao criar o projeto com o Vite. Aqui está o que está acontecendo:

  1. Configuração SASS:

    • quietDeps: true: Isso silencia os avisos sobre dependências obsoletas em módulos SASS. Particularmente útil ao trabalhar com arquivos SASS/SCSS de terceiros.

    • charset: false: Impede o aviso “@charset” que aparece nas versões mais recentes do SASS ao usar caracteres especiais em suas folhas de estilo.

  2. Configuração de Teste:

    • globals: true: Torna as funções de teste globalmente disponíveis em arquivos de teste

    • environment: 'jsdom': Fornece um ambiente DOM para testes

    • setupFiles: Aponta para nosso arquivo de configuração de teste

Essas configurações ajudam a criar uma experiência de desenvolvimento mais limpa, removendo mensagens de aviso desnecessárias no console, configurando adequadamente o ambiente de teste e garantindo que o processamento SASS/SCSS funcione sem problemas.

Você pode ver avisos no seu console sem essas configurações quando:

  • Usar recursos SASS/SCSS ou importar arquivos SASS

  • Executar testes que requerem manipulação do DOM

  • Usar caracteres especiais em suas folhas de estilo

2. Construindo os Componentes

Criar o Componente Card

Primeiro, vamos criar nosso componente de cartão básico que exibirá imagens individuais:

// src/components/Card/Card.jsx
import React, { useState, useCallback } from "react";
import Loader from "../Loader";
import styles from "./Card.module.scss";

const Card = React.memo(function Card({ imgUrl, imageId, categoryName, processTurn }) {
  const [isLoading, setIsLoading] = useState(true);

  const handleImageLoad = useCallback(() => {
    setIsLoading(false);
  }, []);

  const handleClick = useCallback(() => {
    processTurn(imageId);
  }, [processTurn, imageId]);

  return (
    <div className={styles.container} onClick={handleClick}>
      {isLoading && (
        <div className={styles.loaderContainer}>
          <Loader message="Loading..." />
        </div>
      )}
      <img
        src={imgUrl}
        alt={categoryName}
        onLoad={handleImageLoad}
        className={`${styles.image} ${isLoading ? styles.hidden : ''}`}
      />
    </div>
  );
});

export default Card;

O componente Card é um bloco de construção fundamental do nosso jogo. Ele é responsável por exibir imagens individuais e lidar com interações dos jogadores. Vamos detalhar sua implementação:

Quebra de propriedades:

  1. imagem: (string)

    • A URL da imagem a ser exibida que é recebida do nosso serviço de API.

    • Ela é usada diretamente no atributo src da tag img.

  2. id: (string)

    • Identificador único para cada carta que é crítico para rastrear quais cartas foram clicadas.

    • Ele é passado para o callback processTurn quando uma carta é clicada.

  3. categoria: (string)

    • Descreve o tipo de imagem (por exemplo, “anime”, “neko”), e é usado no atributo alt para melhor acessibilidade.

    • Ajuda com SEO e leitores de tela.

  4. processTurn: (função)

    • Função de retorno passada do componente pai que lida com a lógica do jogo quando uma carta é clicada.

    • Também gerencia atualizações de pontuação e mudanças de estado do jogo e determina se uma carta já foi clicada antes.

  5. isLoading: (booleano)

    • Controla se mostrar um estado de carregamento. Quando verdadeiro, exibe um componente Loader em vez da imagem.

    • Melhora a experiência do usuário durante o carregamento da imagem.

Estilização do componente:

// src/components/Card/Card.module.scss
.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(255, 255, 255, 0.8);
  border: 1px solid rgba(0, 0, 0, 0.8);
  padding: 20px;
  font-size: 30px;
  text-align: center;
  min-height: 200px;
  position: relative;
  cursor: pointer;
  transition: transform 0.2s ease;

  &:hover {
    transform: scale(1.02);
  }

  .image {
    width: 10rem;
    height: auto;
    opacity: 1;
    transition: opacity 0.3s ease;

    &.hidden {
      opacity: 0;
    }
  }

  .loaderContainer {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
}

Uso no componente:

<Card
    key={getKey()}
    imgUrl={item?.image?.original?.url || ""}
    imageId={item?.id}
    categoryName={item?.category}
    processTurn={(imageId) => processTurn(imageId)} 
/>

Principais características:

  1. Otimização de Desempenho:

    • Utiliza React.memo para evitar re-renderizações desnecessárias

    • Implementa useCallback para manipuladores de eventos

    • Gerencia o estado de carregamento internamente para melhor UX

  2. Gerenciamento de Estado de Carregamento:

    • Estado interno isLoading rastreia o carregamento da imagem

    • Exibe um componente Loader com uma mensagem enquanto carrega

    • Oculta a imagem até que esteja totalmente carregada usando classes CSS

  3. Manipulação de Eventos:

    • handleImageLoad: Gerencia a transição do estado de carregamento

    • handleClick: Processa os movimentos do jogador através do callback processTurn

Construa o Componente CardsGrid

Este é o nosso componente principal do jogo que gerencia o estado do jogo, a lógica de pontuação e as interações com as cartas. Vamos detalhar sua implementação:


// src/components/CardsGrid/CardsGrid.jsx
import React, { useState, useEffect } from "react";
import { useLocalStorage } from "@uidotdev/usehooks";
import Card from "../Card";
import Loader from "../Loader";
import styles from "./CardsGrid.module.scss";
import useFetch from "../../hooks/useFetch";

function CardsGrid(data) {
  // Gerenciamento de Estado
  const [images, setImages] = useState(data?.data?.images || []);
  const [clickedImages, setClickedImages] = useLocalStorage("clickedImages", []);
  const [score, setScore] = useLocalStorage("score", 0);
  const [bestScore, setBestScore] = useLocalStorage("bestScore", 0);
  const [isLoading, setIsLoading] = useState(!data?.data?.images?.length);

  // Hook personalizado para buscar imagens
  const { data: fetchedData, fetchData, error } = useFetch();

  // Atualizar imagens quando novos dados são buscados
  useEffect(() => {
    if (fetchedData?.images) {
      setImages(fetchedData.images);
      setIsLoading(false);
      // Redefinir imagens clicadas quando um novo lote é carregado
      setClickedImages([]);
    }
  }, [fetchedData]);

  // Função auxiliar para atualizar a melhor pontuação
  function updateBestScore(currentScore) {
    if (currentScore > bestScore) {
      setBestScore(currentScore);
    }
  }

  // Lógica principal do jogo
  function processTurn(imageId) {
    const newClickedImages = [...clickedImages, imageId];
    setClickedImages(newClickedImages);

    // Se clicar na mesma imagem duas vezes, redefinir tudo
    if (clickedImages.includes(imageId)) {
      // Atualizar a melhor pontuação, se necessário
      updateBestScore(score);

      setClickedImages([]);
      setScore(0);
    } else {
      // Lidar com a seleção bem-sucedida de cartas
      const newScore = score + 1;
      setScore(newScore);

      // Verificar se a pontuação perfeita foi alcançada (todas as cartas clicadas uma vez)
       if (newClickedImages.length === images.length) {
        updateBestScore(newScore);
        fetchData();
        setClickedImages([]);
      } else {
        // Embaralhar as imagens
        const shuffled = [...images].sort(() => Math.random() - 0.5);
        setImages(shuffled);
      }
    }
  }

 if (error) {
    return <p>Failed to fetch data</p>;
  }

  if (isLoading) {
    return <Loader message="Loading new images..." />;
  }

  return (
    <div className={styles.container}>
      {images.map((item) => (
        <Card
          key={getKey()}
          imgUrl={item?.image?.original?.url || ""}
          imageId={item?.id}
          categoryName={item?.category}
          processTurn={(imageId) => processTurn(imageId)}
        />
      ))}
    </div>
  );
}

export default React.memo(CardsGrid);

Estilização do Componente:

.container {
  display: grid;
  gap: 1rem 1rem;
  grid-template-columns: auto; /* Padrão: uma coluna para dispositivos móveis */
  background-color: #2196f3;
  padding: 0.7rem;
  cursor: pointer;
}

@media (min-width: 481px) {
  .container {
    grid-template-columns: auto auto; /* Duas colunas para tablets e acima */
  }
}

@media (min-width: 769px) {
  .container {
    grid-template-columns: auto auto auto; /* Três colunas para desktops e maiores */
  }
}

Detalhes das Principais Funcionalidades:

  1. Gerenciamento de Estado:

    • Usa useState para estado a nível de componente

    • Implementa useLocalStorage para dados persistentes do jogo:

      • clickedImages: Rastreia quais cartas foram clicadas

      • score: Pontuação atual do jogo

      • bestScore: Maior pontuação alcançada

    • Gerencia o estado de carregamento para busca de imagens

    • Embaralha as cartas

  2. Lógica do Jogo:

    • processTurn: Lida com as jogadas dos jogadores

      • Rastreia cliques duplicados

      • Atualiza pontuações

      • Gerencia cenários de pontuação perfeita

    • updateBestScore: Atualiza a pontuação máxima quando necessário

    • Busca automaticamente novas imagens quando uma rodada é concluída

  3. Busca de Dados:

    • Usa o hook customizado useFetch para dados de imagem

    • Gerencia estados de carregamento e erro

    • Atualiza imagens quando novos dados são buscados

  4. Otimização de Performance:

    • Componente envolto em React.memo

    • Atualizações de estado eficientes

    • Layout de grade responsivo

  5. Persistência:

    • O estado do jogo persiste entre recarregamentos de página

    • Rastreamento da melhor pontuação

    • Salvamento do progresso atual do jogo

Exemplo de Uso:

...
...

function App() {
  const { data, loading, error } = useFetch();

  if (loading) return <Loader />;
  if (error) return <p>Error: {error}</p>;

  return (
    <div className={styles.container}>
      <Header />
      <Subtitle />
      <CardsGrid data={data} />
      <Footer />
    </div>
  );
}
export default App;

O componente CardsGrid serve como o coração do nosso jogo de cartas de memória, gerenciando:

  • Estado e lógica do jogo

  • Rastreamento de pontuação

  • Interações de cartas

  • Carregamento e exibição de imagens

  • Layout responsivo

  • Persistência de dados

Esta implementação fornece uma experiência de jogo suave, mantendo a legibilidade e a manutenibilidade do código por meio de uma clara separação de preocupações e um gerenciamento adequado do estado.

3. Implementando a Camada de API

Nosso jogo utiliza uma camada de API robusta com várias opções de fallback para garantir a entrega confiável de imagens. Vamos implementar cada serviço e o mecanismo de fallback.

Configurar o Serviço de API Primário:

// src/services/api/nekosiaApi.js
const NEKOSIA_API_URL = "https://api.nekosia.cat/api/v1/images/catgirl";

export async function fetchNekosiaImages() {
  const response = await fetch(
    `${NEKOSIA_API_URL}?count=21&additionalTags=white-hair,uniform&blacklistedTags=short-hair,sad,maid&width=300`
  );

  if (!response.ok) {
    throw new Error(`Nekosia API error: ${response.status}`);
  }

  const result = await response.json();

  if (!result.images || !Array.isArray(result.images)) {
    throw new Error('Invalid response format from Nekosia API');
  }

  const validImages = result.images.filter(item => item?.image?.original?.url);

  if (validImages.length === 0) {
    throw new Error('No valid images received from Nekosia API');
  }

  return { ...result, images: validImages };
}

Criar o Primeiro Serviço de API de Fallback:

// src/services/api/nekosBestApi.js
const NEKOS_BEST_API_URL = "https://nekos.best/api/v2/neko?amount=21";

export async function fetchNekosBestImages() {
  const response = await fetch(NEKOS_BEST_API_URL, {
    method: "GET",
    mode: "no-cors"
  });

  if (!response.ok) {
    throw new Error(`Nekos Best API error: ${response.status}`);
  }

  const result = await response.json();

  // Transformar a resposta para corresponder ao nosso formato esperado
  const transformedImages = result.results.map(item => ({
    id: item.url.split('/').pop().split('.')[0], // Extrair UUID do URL
    image: {
      original: {
        url: item.url
      }
    },
    artist: {
      name: item.artist_name,
      href: item.artist_href
    },
    source: item.source_url
  }));

  return { images: transformedImages };
}

Criar o Segundo Serviço de API de Fallback:

// src/services/api/nekosApi.js
const NEKOS_API_URL = "https://api.nekosapi.com/v3/images/random?limit=21&rating=safe";

export async function fetchNekosImages() {
  const response = await fetch(NEKOS_API_URL, {
    method: "GET",
  });

  if (!response.ok) {
    throw new Error(`Nekos API error: ${response.status}`);
  }

  const result = await response.json();

  // Transformar a resposta para corresponder ao nosso formato esperado
  const transformedImages = result.items.map(item => ({
    id: item.id,
    image: {
      original: {
        url: item.image_url
      }
    }
  }));

  return { images: transformedImages };
}

Construa o Mecanismo de Reserva da API:

// src/services/api/imageService.js
import { fetchNekosiaImages } from "./nekosiaApi";
import { fetchNekosImages } from "./nekosApi";
import { fetchNekosBestImages } from "./nekosBestApi";

export async function fetchImages() {
  try {
    // Tente primeiro a API principal
    return await fetchNekosiaImages();
  } catch (error) {
    console.warn("Primary API failed, trying fallback:", error);

    // Tente a primeira API de reserva
    try {
      return await fetchNekosBestImages();
    } catch (fallbackError) {
      console.warn("First fallback API failed, trying second fallback:", fallbackError);

      // Tente a segunda API de reserva
      try {
        return await fetchNekosImages();
      } catch (secondFallbackError) {
        console.error("All image APIs failed:", secondFallbackError);
        throw new Error("All image APIs failed");
      }
    }
  }
}

Use o Serviço de Imagens:

// src/hooks/useFetch.js
import { useState, useEffect } from "react";
import { fetchImages } from "../services/api/imageService";

export default function useFetch() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    setLoading(true);
    setError(null);

    try {
      const result = await fetchImages();
      setData(result);
    } catch (err) {
      setError(err.message || 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return {
    data,
    loading,
    error,
    fetchData,
  };
}

Principais Recursos de Nossa Implementação de API:

  1. Múltiplas Fontes de API:

    • API Principal (Nekosia): Fornece imagens de anime de alta qualidade

    • Primeira Reserva (Nekos Best): Inclui informações do artista

    • Segunda Reserva (Nekos): Backup simples e confiável

  2. Formato de Dados Consistente:

    • Todas as APIs transformam suas respostas para corresponder ao nosso formato esperado:
    {
      images: [
        {
          id: string,
          image: {
            original: {
              url: string
            }
          }
        }
      ]
    }
  1. Manuseio de Erros Robusto:

    • Valida as respostas da API

    • Verifica URLs de imagem válidas

    • Fornece mensagens de erro detalhadas

    • Mecanismo de fallback gracioso

  2. Recursos de Segurança:

    • Filtragem segura de conteúdo (classificação=seguro)

    • Limitação de contagem de imagens (21 imagens)

    • Validação de URL

    • Validação do formato de resposta

  3. Considerações de Desempenho:

    • Tamanhos de imagem otimizados

    • Tags de conteúdo filtradas

    • Transformação eficiente de dados

    • Chamadas mínimas à API

Esta implementação garante que nosso jogo tenha uma fonte confiável de imagens, lidando de forma adequada com possíveis falhas na API. O formato de dados consistente em todas as APIs facilita a troca entre elas sem afetar a funcionalidade do jogo.

Testando o Aplicativo

Testar é uma parte crucial do desenvolvimento de qualquer aplicativo, e para o nosso Memory Card Game, implementamos uma estratégia abrangente de testes usando ferramentas e práticas modernas. Vamos mergulhar na estrutura dos nossos testes e em alguns padrões-chave de teste que utilizamos.

Pilha de Testes

  • Vitest: Nosso framework de testes principal, escolhido pela sua rapidez e integração perfeita com o Vite

  • React Testing Library: Para testar componentes React com uma abordagem centrada no usuário

  • @testing-library/user-event: Para simular interações do usuário

  • jsdom: Para criar um ambiente DOM em nossos testes

Principais Padrões de Teste

Os testes foram uma parte crucial para garantir a confiabilidade e a manutenibilidade deste Jogo de Memória. Eu implementei uma estratégia abrangente de testes usando a React Testing Library e o Vitest, focando em várias áreas-chave:

1. Teste de Componentes

Escrevi testes extensivos para meus componentes React para garantir que eles renderizassem corretamente e se comportassem como esperado. Por exemplo, o componente CardsGrid, que é o coração do jogo, possui uma cobertura de testes completa, incluindo:

  • Estados de renderização inicial

  • Estados de carregamento

  • Tratamento de erros

  • Rastreamento de pontuação

  • Comportamento de interação das cartas

2. Mocking de Testes

Para garantir testes confiáveis e rápidos, implementei várias estratégias de mocking:

  • Operações de armazenamento local usando o hook useLocalStorage

  • Chamadas de API usando o hook useFetch

  • Manipuladores de eventos e atualizações de estado

3. Melhores Práticas de Testes

Ao longo da minha implementação de testes, segui várias melhores práticas:

  • Usando beforeEach e afterEach hooks para redefinir o estado entre os testes

  • Testando interações do usuário usando fireEvent da React Testing Library

  • Escrevendo testes que se assemelham a como os usuários interagem com o aplicativo

  • Testando tanto cenários de sucesso quanto de erro

  • Isolando testes usando simulação adequada

4. Ferramentas de Teste

O projeto aproveita ferramentas e bibliotecas modernas de teste:

  • Vitest: Como o executor de testes

  • React Testing Library: Para testar componentes React

  • @testing-library/jest-dom: Para afirmações de teste de DOM aprimoradas

  • @testing-library/user-event: Para simular interações do usuário

Essa abordagem abrangente de teste me ajudou a identificar bugs precocemente, garantiu a qualidade do código e tornou o refatoramento mais seguro e gerenciável.

Otimizações

Para garantir um desempenho suave, especialmente em dispositivos móveis, implementamos várias técnicas de otimização:

  1. Transformação de Resposta

    • Formato de dados padronizado em todas as APIs

    • Extração eficiente de IDs a partir de URLs

    • Metadados de imagem estruturados para acesso rápido

  2. Otimização de Rede

    • Usando o modo no-cors quando apropriado para lidar com problemas de CORS de forma eficiente

    • Tratamento de erros com códigos de status específicos para melhor depuração

    • Estrutura de resposta consistente em todas as implementações de API

  3. Considerações Mobile-First

    • Estratégia otimizada de carregamento de imagens

    • Tratamento de erros eficiente para evitar tentativas desnecessárias

    • Transformação de dados simplificada para reduzir a sobrecarga de processamento

Melhorias Futuras

Existem algumas maneiras de melhorar ainda mais este projeto:

  1. Cache de Respostas da API

    • Implementar cache de armazenamento local para imagens frequentemente usadas

    • Adicionar estratégia de invalidação de cache para conteúdo atualizado

    • Implementar carregamento progressivo de imagens

  2. Otimizações de Performance

    • Adicione carregamento preguiçoso de imagens para melhor tempo de carregamento inicial

    • Implemente o enfileiramento de requisições para melhor gerenciamento de largura de banda

    • Adicione compressão de resposta para transferência de dados mais rápida

  3. Aprimoramentos de Confiabilidade

    • Adicione verificação de saúde da API antes das tentativas

    • Implemente mecanismos de nova tentativa com aumento exponencial

    • Adicione o padrão de disjuntor para APIs que falham

  4. Analytics e Monitoramento

    • Acompanhe as taxas de sucesso da API

    • Monitore os tempos de resposta

    • Implemente a troca automática de API com base em métricas de desempenho

Essa implementação robusta garante que nosso jogo permaneça funcional e performático mesmo sob condições adversas de rede ou indisponibilidade da API, ao mesmo tempo que ainda mantém espaço para futuras melhorias e otimizações.

Conclusão

Construir este Jogo de Memória foi mais do que apenas criar uma alternativa divertida e sem anúncios para crianças—foi um exercício na implementação das melhores práticas de desenvolvimento web moderno enquanto resolvia um problema do mundo real.

O projeto demonstra como a combinação de uma arquitetura bem pensada, testes robustos e mecanismos de fallback confiáveis pode resultar em uma aplicação pronta para produção que é tanto entretenimento quanto educacional.

🗝️ Principais Conclusões

  1. Desenvolvimento Centrado no Usuário

    • Começou com um problema claro (jogos repletos de anúncios afetando a experiência do usuário)

    • Implementou recursos que melhoram a jogabilidade sem interrupções

    • Manteve o foco em desempenho e confiabilidade em todos os dispositivos

  2. Excelência Técnica

    • Utilizou padrões e hooks modernos do React para código limpo e de fácil manutenção

    • Implementou uma estratégia de testes abrangente garantindo confiabilidade

    • Criou um sistema robusto de fallback da API para jogabilidade ininterrupta

  3. Desempenho em Primeiro Lugar

    • Adotada a abordagem mobile-first com design responsivo

    • Otimizada a carga e o manuseio de imagens

    • Implementadas estratégias eficientes de gerenciamento de estado e caching

📚 Resultados de Aprendizado

Este projeto demonstra como jogos aparentemente simples podem ser excelentes veículos para implementar soluções técnicas complexas. Desde a arquitetura de componentes até os fallbacks de API, cada recurso foi construído com escalabilidade e manutenibilidade em mente, provando que até projetos de hobby podem manter uma qualidade de código profissional.

🔮 Avançando

Embora o jogo atinja com sucesso seu objetivo principal de fornecer uma experiência agradável e sem anúncios, as melhorias futuras documentadas fornecem um roteiro claro para evolução. Seja implementando otimizações adicionais ou adicionando novos recursos, a fundação é sólida e pronta para expansão.

O Jogo da Memória é um testemunho de como projetos pessoais podem tanto resolver problemas do mundo real quanto servir como plataformas para implementar as melhores práticas no desenvolvimento web moderno. Sinta-se à vontade para explorar o código, contribuir ou usá-lo como inspiração para seus próprios projetos!