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

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.

Hey, eu atualizei esta postagem no meu blog pessoal, veja agora!

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

Top 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!

We are hiring! Do you want to be our Senior Platform Engineer? Are you capable of chipping in across sysadmin, ops, and site reliability work, while supporting the open source stack that runs DEV and other communities?

This role might just be for you!

Apply now