Como criar um clone da funcionalidade de pesquisa de arquivos do GitHub

Funcionalidade de pesquisa de arquivos no Github

Ao criar este aplicativo, você aprenderá o seguinte:

  • Como criar uma interface do usuário semelhante a um repositório do GitHub
  • Como trabalhar com eventos do teclado no React
  • Como trabalhar com a navegação usando as setas do teclado
  • Como destacar o texto correspondente durante a pesquisa
  • Como adicionar ícones no React
  • Como renderizar conteúdo HTML em uma expressão JSX

E muito mais.

Você pode ver a demonstração ao vivo do aplicativo aqui.

Vamos começar

Crie um novo projeto usando create-react-app:

create-react-app github-file-search-react

Após a criação do projeto, exclua todos os arquivos do diretório src pasta e criar index.js, App.js e styles.scss arquivos dentro do src pasta. Crie também components e utils pastas dentro do src pasta.

Instale as dependências necessárias:

yarn add [email protected] [email protected] [email protected] [email protected]

Abrir styles.scss e adicione o conteúdo de aqui dentro dele.

Crie um novo arquivo Header.js dentro de components pasta com o seguinte conteúdo:

import React from 'react';

const Header = () => 

GitHub File Search

; export default Header;

Crie um novo arquivo api.js dentro de utils pasta e adicione o conteúdo de aqui dentro dele.

Neste arquivo, criamos dados estáticos para serem exibidos na interface do usuário para manter o aplicativo simples e fácil de entender.

Crie um novo arquivo ListItem.js dentro de components pasta com o seguinte conteúdo:

import React from 'react';
import moment from 'moment';
import { AiFillFolder, AiOutlineFile } from 'react-icons/ai';

const ListItem = ({ type, name, comment, modified_time }) => {
  return (
    
      
{type === 'folder' ? ( ) : ( )} {name}
{comment}
{moment(modified_time).fromNow()}
); }; export default ListItem;

Nesse arquivo, coletamos os dados de cada arquivo que queremos exibir e exibimos o ícone da pasta / arquivo, o nome do arquivo, os comentários e a última vez que o arquivo foi modificado.

Para exibir os ícones, usaremos o react-icons biblioteca npm. Ele tem um site muito bom que permite pesquisar e usar com facilidade os ícones que você precisa. Confira aqui.

O componente de ícones aceita o color e size adereços para personalizar o ícone que usamos no código acima.

Crie um novo arquivo chamado FilesList.js dentro de components pasta com o seguinte conteúdo:

import React from 'react';
import ListItem from './ListItem';

const FilesList = ({ files }) => {
  return (
    
{files.length > 0 ? ( files.map((file, index) => { return ; }) ) : (

No matching files found

)}
); }; export default FilesList;

Neste arquivo, lemos os dados estáticos do api.js e, em seguida, exiba cada elemento da matriz de arquivos usando o método de mapa de matriz.

Agora abra o src/App.js e adicione o seguinte código dentro dele:

import React from 'react';
import Header from './components/Header';
import FilesList from './components/FilesList';
import files from './utils/api';

export default class App extends React.Component {
  state = {
    filesList: files
  };

  render() {
    const { counter, filesList } = this.state;

    return (
      
); } }

Nesse arquivo, adicionamos um estado para armazenar os dados dos arquivos estáticos, que podemos modificar sempre que necessário. Então nós passamos para o FilesList componente a ser exibido na interface do usuário.

Agora, abra o index.js e adicione o seguinte código dentro dele:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './styles.scss';

ReactDOM.render(, document.getElementById('root'));

Agora, inicie seu aplicativo executando o yarn start comando no terminal ou prompt de comando e você verá a seguinte tela inicial:

Tela inicial

Você pode encontrar o código até este ponto em este ramo.

Adicionar funcionalidade básica de pesquisa

Agora, vamos adicionar a funcionalidade que altera a interface do usuário e nos permite pesquisar arquivos quando pressionamos a letra t no nosso teclado.

Dentro de utils pasta crie um novo arquivo chamado keyCodes.js com o seguinte conteúdo:

export const ESCAPE_CODE = 27;
export const HOTKEY_CODE = 84; // key code of letter t
export const UP_ARROW_CODE = 38;
export const DOWN_ARROW_CODE = 40;

Crie um novo arquivo chamado SearchView.js dentro de components pasta com o seguinte conteúdo:

import React, { useState, useEffect, useRef } from 'react';

const SearchView = ({ onSearch }) => {
  const [input, setInput] = useState('');
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  const onInputChange = (event) => {
    const input = event.target.value;
    setInput(input);
    onSearch(input);
  };

  return (
    
My Repository /
); }; export default SearchView;

Estamos usando o React Hooks aqui para nossos métodos de estado e ciclo de vida. Se você é novo no React Hooks, confira Este artigo para uma introdução.

Neste arquivo, declaramos primeiro um estado para armazenar a entrada digitada pelo usuário. Então nós adicionamos um ref usando o useRef Gancho para que possamos focar no campo de entrada quando o componente estiver montado.

const inputRef = useRef();

useEffect(() => {
  inputRef.current.focus();
}, []);

...

Nesse código, passando a matriz Vazia [] como o segundo argumento para o useEffect gancho, o código dentro do useEffect O gancho será executado apenas uma vez quando o componente estiver montado. Isso atua como o componentDidMount método de ciclo de vida nos componentes da classe.

Então atribuímos o ref para o campo de entrada como ref={inputRef}. Na alteração do campo de entrada dentro do onInputChange manipulador, estamos chamando o onSearch método passado como suporte ao componente do App.js Arquivo.

Agora abra App.js e substitua seu conteúdo pelo seguinte código:

import React from 'react';
import Header from './components/Header';
import FilesList from './components/FilesList';
import SearchView from './components/SearchView';
import { ESCAPE_CODE, HOTKEY_CODE } from './utils/keyCodes';
import files from './utils/api';

export default class App extends React.Component {
  state = {
    isSearchView: false,
    filesList: files
  };

  componentDidMount() {
    window.addEventListener('keydown', this.handleEvent);
  }

  componentWillUnmount() {
    window.removeEventListener('keydown', this.handleEvent);
  }

  handleEvent = (event) => {
    const keyCode = event.keyCode || event.which;

    switch (keyCode) {
      case HOTKEY_CODE:
        this.setState((prevState) => ({
          isSearchView: true,
          filesList: prevState.filesList.filter((file) => file.type === 'file')
        }));
        break;
      case ESCAPE_CODE:
        this.setState({ isSearchView: false, filesList: files });
        break;
      default:
        break;
    }
  };

  handleSearch = (searchTerm) => {
    let list;
    if (searchTerm) {
      list = files.filter(
        (file) =>
          file.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 &&
          file.type === 'file'
      );
    } else {
      list = files.filter((file) => file.type === 'file');
    }

    this.setState({
      filesList: list
    });
  };

  render() {
    const { isSearchView, filesList } = this.state;

    return (
      
{isSearchView ? (
) : ( )}
); } }

Agora, reinicie o aplicativo executando o yarn start comando novamente e verifique sua funcionalidade.

Funcionalidade de pesquisa inicial de trabalho

Como você pode ver, inicialmente todas as pastas e arquivos são exibidos. Então, quando pressionamos a letra t no teclado, a visualização muda para permitir a pesquisa nos arquivos exibidos.

Agora, vamos entender o código do App.js Arquivo.

Neste arquivo, declaramos primeiro isSearchView como uma variável de estado. Então dentro do componentDidMount e componentWillUnmount métodos de ciclo de vida que estamos adicionando e removendo o keydown manipulador de eventos, respectivamente.

Então dentro do handleEvent , estamos verificando qual tecla é pressionada pelo usuário.

  • Se o usuário pressionar a tecla t, definiremos o isSearchView estado para true e atualize o filesList matriz de estado para incluir apenas arquivos e excluir as pastas.
  • Se o uso pressionar a tecla Escape, definiremos o isSearchView estado para false e atualize o filesList matriz de estados para incluir todos os arquivos e pastas.

A razão pela qual declaramos HOTKEY_CODE e ESCAPE_CODE em arquivos separados (keyCodes.js em vez de usar diretamente o código-chave como 84) é que mais tarde, se quisermos mudar a tecla de atalho de t para s, basta alterar o código da chave nesse arquivo. Ele refletirá a alteração em todos os arquivos em que é usada sem precisar alterá-la em todos os arquivos.

Agora, vamos entender o handleSearch função. Nesta função, verificamos se o usuário inseriu algo na caixa de pesquisa de entrada e depois filtramos o (s) nome (s) do arquivo correspondente (s) que inclui esse termo de pesquisa. Em seguida, atualizamos o estado com os resultados filtrados.

Em seguida, dentro do método render, com base no isSearchView valor, exibimos a exibição da lista de arquivos ou a pesquisa do usuário.

Você pode encontrar código até este ponto em este ramo.

Adicione funcionalidade para navegar entre arquivos

Agora, vamos adicionar a funcionalidade para exibir uma seta na frente do arquivo atualmente selecionado enquanto navega na lista de arquivos.

Crie um novo arquivo chamado InfoMessage.js dentro de components pasta com o seguinte conteúdo:

import React from 'react';

const InfoMessage = () => {
  return (
    
You've activated the file finder. Start typing to filter the file list. Use and{' '} to navigate,{' '} esc to exit.
); }; export default InfoMessage;

Agora, abra o App.js arquivo e importe o InfoMessage componente para usá-lo:

import InfoMessage from './components/InfoMessage';

Adicione uma nova variável de estado chamada counter com o valor inicial de 0. Isso é para acompanhar o índice da seta.

Dentro de handleEvent manipulador, obtenha o filesList e counter valores do estado:

const { filesList, counter } = this.state;

Adicione dois novos casos de switch:

case UP_ARROW_CODE:
  if (counter > 0) {
    this.setState({ counter: counter - 1 });
  }
  break;
case DOWN_ARROW_CODE:
  if (counter < filesList.length - 1) {
    this.setState({ counter: counter + 1 });
  }
  break;

Aqui, diminuímos o counter valor do estado quando pressionamos a seta para cima no teclado e incrementamos quando pressionamos a seta para baixo.

Importe também as constantes da matriz para cima e para baixo na parte superior do arquivo:

import {
  ESCAPE_CODE,
  HOTKEY_CODE,
  UP_ARROW_CODE,
  DOWN_ARROW_CODE
} from './utils/keyCodes';

Dentro de handleSearch função, redefina o counter estado para 0 no final da função, para que a seta sempre seja exibida para o primeiro arquivo da lista enquanto estiver filtrando a lista de arquivos.

this.setState({
  filesList: list,
  counter: 0
});

Altere o método de renderização para exibir o InfoMessage componente e passe counter e isSearchView como adereços para o FilesList componente:

render() {
  const { isSearchView, counter, filesList } = this.state;

  return (
    
{isSearchView ? (
) : ( )}
); }

Agora, abra o FilesList.js arquivo e aceite o isSearchView e counter adereços e passá-los para o ListItem componente.

Seu FilesList.js O arquivo ficará assim agora:

import React from 'react';
import ListItem from './ListItem';

const FilesList = ({ files, isSearchView, counter }) => {
  return (
    
{files.length > 0 ? ( files.map((file, index) => { return ( ); }) ) : (

No matching files found

)}
); }; export default FilesList;

Agora abra ListItem.js e substitua seu conteúdo pelo seguinte conteúdo:

import React from 'react';
import moment from 'moment';
import { AiFillFolder, AiOutlineFile, AiOutlineRight } from 'react-icons/ai';

const ListItem = ({
  index,
  type,
  name,
  comment,
  modified_time,
  isSearchView,
  counter
}) => {
  const isSelected = counter === index;

  return (
    
      
{isSearchView && ( )} {type === 'folder' ? ( ) : ( )} {name}
{!isSearchView && (
{comment}
{moment(modified_time).fromNow()}
)}
); }; export default ListItem;

Nesse arquivo, primeiro aceitamos o isSearchView e counter suporte. Em seguida, verificamos se o índice do arquivo atualmente exibido na lista corresponde ao counter valor.

Com base nisso, exibimos a seta na frente apenas para esse arquivo. Então, quando usamos a seta para baixo ou para cima para navegar pela lista, aumentamos ou diminuímos o valor do contador, respectivamente, no App.js Arquivo.

Com base no isSearchView valor que exibimos ou ocultamos a coluna de comentário e hora na exibição de pesquisa na interface do usuário.

Agora, reinicie o aplicativo executando o yarn start comando novamente e verifique sua funcionalidade:

Pesquisar e navegar

Você pode encontrar o código até este ponto em este ramo.

Adicione funcionalidade para destacar o texto correspondente

Agora, vamos adicionar a funcionalidade para destacar o texto correspondente do nome do arquivo quando filtramos o arquivo.

Abrir App.js e mude o handleSearch para o seguinte código:

handleSearch = (searchTerm) => {
  let list;
  if (searchTerm) {
    const pattern = new RegExp(searchTerm, 'gi');
    list = files
      .filter(
        (file) =>
          file.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 &&
          file.type === 'file'
      )
      .map((file) => {
        return {
          ...file,
          name: file.name.replace(pattern, (match) => {
            return `${match}`;
          })
        };
      });
  } else {
    list = files.filter((file) => file.type === 'file');
  }

  this.setState({
    filesList: list,
    counter: 0
  });
};

Nesse código, primeiro usamos o RegExp construtor para criar uma expressão regular dinâmica para pesquisa global e sem distinção entre maiúsculas e minúsculas:

const pattern = new RegExp(searchTerm, 'gi');

Em seguida, filtramos os arquivos que correspondem aos critérios de pesquisa:

files.filter(
  (file) =>
    file.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 &&
    file.type === 'file'
);

Em seguida, chamamos o método de mapa de matriz no resultado obtido da funcionalidade de filtro acima.

No método map, usamos a string replace método.
o replace O método aceita dois parâmetros:

  • padrão para procurar
  • função a ser executada para cada padrão correspondente

Nós usamos o replace método para encontrar todas as correspondências para o pattern e substitua-o pela corda ${match}. Aqui match conterá o texto correspondente do nome do arquivo.

Se você verificar a estrutura JSON no diretório utils/api.js arquivo, a estrutura de cada arquivo fica assim:

{
  id: 12,
  type: 'file',
  name: 'Search.js',
  comment: 'changes using react context',
  modified_time: '2020-06-30T07:55:33Z'
}

Como queremos substituir o texto apenas do campo de nome, espalhamos as propriedades do objeto de arquivo e apenas alteramos o nome, mantendo outros valores como estão.

{
  ...file,
  name: file.name.replace(pattern, (match) => {
    return `${match}`;
  })
}

Agora, reinicie o aplicativo executando o yarn start comando novamente e verifique sua funcionalidade.

Você verá que o HTML é exibido como na interface do usuário ao pesquisar:

HTML não renderizado corretamente

Isso ocorre porque estamos exibindo o nome do arquivo no diretório ListItem.js arquivo da seguinte maneira:

{name}

E para prevenir Cross-site scripting (XSS) ataques, o React escapa todo o conteúdo exibido usando a Expressão JSX (que está entre colchetes).

Portanto, se realmente queremos exibir o HTML correto, precisamos usar um suporte especial conhecido como dangerouslySetInnerHTML. Passa o __html nome com o HTML para exibir o valor como este:

Agora, reinicie o aplicativo executando o yarn start comando novamente e verifique sua funcionalidade:

Aplicação final de trabalho

Como você pode ver, o termo de pesquisa está sendo destacado corretamente no nome do arquivo.

É isso aí!

Você pode encontrar o código até este ponto em este ramo.

Código fonte completo do GitHub: aqui
Demonstração ao vivo: aqui

Confira meus outros artigos sobre React, Node.js e Javascript em Médio, dev.to e assine para receber atualizações semanais diretamente na sua caixa de entrada aqui.