DEV Community

Cover image for App Desktop com React + Electron + Sqlite | MS TO-DO clone
Tulio Calil
Tulio Calil

Posted on • Updated on • Originally published at tuliocalil.com

App Desktop com React + Electron + Sqlite | MS TO-DO clone

Eu acho incrível toda essa ideia de podermos utilizar tecnologias web em diversos lugares, e é super interessante a versatilidade que essas coisas nos proporciona também.
Caso você ainda não saiba, é possível criar aplicações para dekstop com tecnologias web (html, js e css), e isso é tão comum que acaba passando despercebido por muita gente, temos apps no nosso dia a dia que são com essas tecnologias e nem parece, como é o caso do Visual Studio Code, Skype, Discord e vários outros.

Esse post foi atualizado e finalizado (a parte que falta do SQLite) e está disponível aqui.

Boa parte desses apps utilizam o Electron um framework focado em criação de apps desktop multiplataforma com tecnologias web. E como eu disse antes, todo esse ecossistema é super flexível, a ponto de conseguir usar o React junto ao electron, e é isso que vamos ver agora!

Overview do projeto

Nesse post, eu queria sintetizar algumas vontades que já tenho a um tempo e não consigo trazer: Um post de "React iniciante" e um sobre o Electron. Por isso, esse post vai ser dividido em partes e será o mais detalhado que eu conseguir.

Um aplicativo que eu tenho usado bastante ultimamente é o Microsoft To Do, então pensei que seria uma boa tentarmos criar um clone funcional dele durante essa serie de postagens.
MS Todo

Vamos utilizar como banco de dados o Sqlite, que é um banco SQL relacional super leve e portátil, perfeito para nosso projeto.

Iniciando com o projeto

Podemos iniciar o projeto como um projeto Electron normal e depois adicionar o React, porém, essa não é a melhor forma nem a mais performática de fazer isso.
O Electron não é exatamente elogiado pelos seus Builds enxutos ou seu baixo consumo de memória ram, então, vamos utilizar um Boilerplate, que nada mais é do que um esqueleto pronto com essa integração que precisamos e com varias configurações que vão nos ajudar e muito a poupar recursos da maquina.

Vamos iniciar clonando o repositório do boilerplate e dando um nome para o nosso projeto:

git clone --depth 1 --branch main https://github.com/electron-react-boilerplate/electron-react-boilerplate.git ms-todo-clone
Enter fullscreen mode Exit fullscreen mode

Após clonado, vamos navegar até a pasta do nosso projeto e instalar as dependências dele, aqui, estou levando em conta que você já tenha um ambiente Nodejs configurado, e que já tenha o yarn instalado, caso ainda não tenha, instale com npm install -g yarn e agora podemos continuar:

cd ms-todo-clone
yarn
Enter fullscreen mode Exit fullscreen mode

Dependências instaladas, podemos rodar o nosso projeto:

yarn start
Enter fullscreen mode Exit fullscreen mode

Electron React

No nosso projeto, temos a seguinte estrutura:
React Electron

O que podemos observar logo de cara é que temos dois package.json em pastas diferentes, e isso é por que o boilerplate separa dependências de desenvolvimento na raiz e dependências da aplicação dentro do app. Você pode ler detalhadamente sobre essa escolha aqui.
Outro ponto interessante e que temos o CSS Modules, React Router Dom e o testing-library já configurado no projeto.

Iniciando a UI

Para iniciar nosso projeto, vamos criar uma pasta views, styles e components dentro de src/renderer, vamos copiar as rotas dentro do arquivo App.tsx e criar um arquivo chamado routes.tsx e colar o conteúdo das rotas(a função que esta sendo exportada e os imports do react router dom), em seguida podemos deletar o arquivo App.tsx, agora mova o arquivo App.global.css para a pasta de styles. Dentro da pasta de views vamos criar uma pasta chamada Home, dentro dela vamos criar um arquivo chamado index.tsx e um home.module.tsx, importe esse componente para o seu arquivo de rota e use na rota "/".
Vamos precisar fazer algumas alterações no nosso index.tsx que fica em renderer, primeiro vamos corrigir a importação do componente App para apontar para nossas rotas, em seguida vamos importar nosso CSS global que movemos para a pasta styles.
Nossa estrutura ficará dessa forma:
Estrutura de pastas
Nosso arquivo de rotas:
Componentes de rota
Nosso arquivo index.tsx:
Componente da tela

Com toda nossa estrutura configurada, vamos começar nossa interface, vamos começar pela sidebar, então dentro da pasta components cria uma pasta chamada Sidebar com os arquivos index.tsx e Sidebar.module.css dentro dela, vamos inicialmente adicionar o seguinte código a esse componente:

import React from 'react';

import styles from './Sidebar.module.css';

export default function Sidebar() {
  return (
    <div>
      <a href="#">Meu dia</a>
      <a href="#">Importante</a>
      <a href="#">Planejado</a>
      <a href="#">Trabalho</a>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Importe o componente na Home, ficando dessa forma por enquanto:

import React from 'react';

import Sidebar from '../../components/Sidebar';

export default function Home() {
  return <Sidebar />;
}

Enter fullscreen mode Exit fullscreen mode

Vamos criar um arquivo de tema, para centralizar nossos estilos. Na pasta styles crie uma pasta chamada themes e crie um arquivo chamado default.css e nele vamos por o seguinte conteúdo:

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300&display=swap');

:root {
  --primary-color: #788cde;
  --secondary-color: #323232;
  --background-color: #282828;
  --alternate-background-color: #1e1e1e;

  --text-color: #e1e1e1;
  --text-color-light: #777676bb;
  --font: Roboto;

  --text-cancel-color: #dd2a2c;

  --link-color: #e1e1e1;
  --link-color--hover: #543fd7;
}

Enter fullscreen mode Exit fullscreen mode

Agora vamos estilizar nossa Sidebar, para isso abra o arquivo Sidebar.module.css e vamos colocar o seguinte:

@import '../../styles/themes/default.css';

.sidenav {
  width: 240px;
  height: 100vh;
  background: var(--background-color);
  overflow-x: hidden;
  padding-left: 10px;
}

.sidenav a {
  padding: 10px;
  text-decoration: none;
  font-family: var(--font);
  font-size: 1.1rem;
  color: var(--link-color);
  display: block;
}

.sidenav a:hover {
  background-color: var(--alternate-background-color);
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos criar nosso componente de logo. Na pasta components crie a pasta Logo e dentro dela index.tsx e Logo.module.css:

import React from 'react';

import styles from './Logo.module.css';

export default function Logo() {
  return <h1 className={styles.logo}>TODO Clone</h1>;
}
Enter fullscreen mode Exit fullscreen mode
@import '../../styles/themes/default.css';

.logo {
  color: var(--primary-color);
  margin: 20px 0px;
  font-family: var(--font);
  font-weight: 800;
}

Enter fullscreen mode Exit fullscreen mode

Importe o componente de logo na nossa Sidebar:

import React from 'react';

import Logo from '../Logo';

import styles from './Sidebar.module.css';

export default function Sidebar() {
  return (
    <div className={styles.sidenav}>
      <Logo />
      <a href="#">Meu dia</a>
      <a href="#">Importante</a>
      <a href="#">Planejado</a>
      <a href="#">Trabalho</a>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Como resultado, teremos o seguinte até agora:
Previa

Crie duas novas pastas em components: TaskArea e TaskItem.
TaskItem é o componente que representará nossa tarefa, no arquivo index.tsx vamos incluir o seguinte:

import React from 'react';
import { format } from 'date-fns';
import styles from './TaskItem.module.css';

export type TaskItemType = {
  label: string;
  date: string;
  id: number;
  checked: boolean;
  onChange: (id: number) => void;
};

export default function TaskItem({
  date,
  label,
  id,
  checked,
  onChange,
}: TaskItemType) {
  function handleCheck() {
    onChange(id);
  }

  return (
    <div
      className={`${styles.container} ${checked ? styles['task-finish'] : ''}`}
      id={`${id}`}
    >
      <input
        className={styles.checkbox}
        type="checkbox"
        checked={checked}
        onChange={handleCheck}
      />
      <div className="col">
        <div className="row">
          <p className={styles['task-label']}>{label}</p>
        </div>
        <div className="row">
          <p className={styles['task-date']}>
            {format(new Date(date), "E., dd 'de' MMM")}
          </p>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Perceba que você precisará instalar o date-fns.
E o CSS dele fica da seguinte forma:

@import '../../styles/themes/default.css';

.container {
  display: flex;
  align-items: center;
  background-color: var(--secondary-color);
  padding: 10px 20px;
  margin: 1px 0px;
  color: var(--text-color);
  font-family: var(--font);
  border-radius: 6px;
}

.container > :nth-child(1) {
  margin-right: 15px;
}

.task-label {
  font-size: 0.85rem;
  color: var(--text-color);
}

.task-date {
  font-size: 0.85rem;
  color: var(--text-cancel-color);
  font-weight: bold;
}

.task-finish .task-label {
  text-decoration: line-through;
}

input[type='checkbox'] {
  -webkit-appearance: none;
  appearance: none;
  background-color: var(--alternate-background-color);
  margin: 0;
  font: inherit;
  color: currentColor;
  width: 1.35em;
  height: 1.35em;
  border: 0.15em solid var(--background-color);
  border-radius: 50px;
  transform: translateY(-0.075em);
  display: grid;
  place-content: center;
}

input[type='checkbox']::before {
  content: '';
  width: 0.55em;
  height: 0.55em;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
  border-radius: 50px;
  transform: scale(0);
  transform-origin: bottom left;
  transition: 120ms transform ease-in-out;
  box-shadow: inset 1em 1em var(--background-color);
  background-color: var(--background-color);
}

input[type='checkbox']:checked::before {
  transform: scale(1);
}

input[type='checkbox']:checked {
  background-color: var(--primary-color);
}

input[type='checkbox']:focus {
  outline: max(2px, 0.15em) solid currentColor;
  outline-offset: max(2px, 0.15em);
}

input[type='checkbox']:disabled {
  color: var(--primary-color);
  cursor: not-allowed;
}
Enter fullscreen mode Exit fullscreen mode

TaskArea será o container que irá gerenciar quais tasks serão exibidas. O código dele fica assim:

import React, { useState } from 'react';

import TaskItem from '../TaskItem';

import styles from './TaskArea.module.css';

export default function TaskArea() {
  const [tasks, setTasks] = useState([
    {
      id: 1,
      label: 'Teste de task',
      date: new Date().toDateString(),
      checked: false,
    },
    {
      id: 2,
      label: 'Teste de task',
      date: new Date().toDateString(),
      checked: false,
    },
    {
      id: 3,
      label: 'Teste de task',
      date: new Date().toDateString(),
      checked: false,
    },
  ]);

  const handleCheckTask = (id: number) => {
    const newState = tasks.map((task) => {
      if (task.id === id) {
        return {
          ...task,
          checked: !task.checked,
        };
      }

      return task;
    });

    setTasks(newState);
  };

  return (
    <div className={styles.container}>
      {tasks.map((task) => (
        <TaskItem
          checked={task.checked}
          date={task.date}
          label={task.label}
          key={task.id}
          id={task.id}
          onChange={handleCheckTask}
        />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

E o CSS:

@import '../../styles/themes/default.css';

.container {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 10px;
  background-color: var(--alternate-background-color);
}
Enter fullscreen mode Exit fullscreen mode

Com isso, já podemos voltar na nossa view Home e importar o componente TaskArea e vamos importar os estilos dela também:

import React from 'react';

import Sidebar from '../../components/Sidebar';
import TaskArea from '../../components/TaskArea';

import styles from './Home.module.css';

export default function Home() {
  return (
    <div className={styles.container}>
      <Sidebar />
      <TaskArea />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

CSS da Home:

.container {
  display: flex;
  flex-direction: row;
}
Enter fullscreen mode Exit fullscreen mode

Com isso, já temos nossa UI exibindo as tarefas e marcando ou desmarcando como "feita".
Prévia

Nosso próximo passo será:

  • Criar novas tarefas
  • Editar tarefas
  • Deletar tarefas
  • Adicionar data a tarefa
  • Verificar se a tarefa está fora do prazo
  • Navegar entre os menus laterais
  • Conectar com a base de dados

Post atualizado com a parte final

Post sobre Electron + SQLite e Autenticação

Latest comments (2)

Collapse
 
andrellgrillo profile image
André Grillo

Muito bom... gostaria de ver os próximos passos.

Collapse
 
kaiquelopes profile image
Kaique Lopes

Show demais manooow!