Planejamento e Arquitetura do Projeto

Módulo 5: Projeto Final - Lista de Tarefas

Aula 1
1

Visão Geral do Projeto Final

20:00

Entenda o que vamos construir e como planejar

2

Estrutura de Pastas e Componentes

Organize o projeto de forma profissional e escalável

Estrutura de Pastas e Componentes

Estrutura de Pastas Recomendada

todo-app/
├── public/
│   ├── index.html
│   └── favicon.ico
├── src/
│   ├── components/
│   │   ├── common/
│   │   │   ├── Header/
│   │   │   │   ├── Header.jsx
│   │   │   │   └── Header.module.css
│   │   │   ├── Button/
│   │   │   ├── Modal/
│   │   │   └── Input/
│   │   ├── todo/
│   │   │   ├── TodoList/
│   │   │   ├── TodoItem/
│   │   │   ├── TodoForm/
│   │   │   ├── TodoFilters/
│   │   │   └── TodoStats/
│   │   └── layout/
│   │       ├── Sidebar/
│   │       └── MainContent/
│   ├── hooks/
│   │   ├── useTodos.js
│   │   ├── useLocalStorage.js
│   │   ├── useTheme.js
│   │   └── useFilter.js
│   ├── utils/
│   │   ├── dateHelpers.js
│   │   ├── storage.js
│   │   ├── constants.js
│   │   └── validators.js
│   ├── contexts/
│   │   ├── TodoContext.js
│   │   └── ThemeContext.js
│   ├── styles/
│   │   ├── globals.css
│   │   ├── variables.css
│   │   └── themes.css
│   ├── App.jsx
│   ├── App.module.css
│   └── index.js
└── package.json

Arquitetura de Componentes

1. App Component (Container Principal)

// App.jsx
import React from 'react';
import { TodoProvider } from './contexts/TodoContext';
import { ThemeProvider } from './contexts/ThemeContext';
import Header from './components/common/Header/Header';
import Sidebar from './components/layout/Sidebar/Sidebar';
import MainContent from './components/layout/MainContent/MainContent';
import styles from './App.module.css';

function App() {
  return (
    <ThemeProvider>
      <TodoProvider>
        <div className={styles.app}>
          <Header />
          <div className={styles.container}>
            <Sidebar />
            <MainContent />
          </div>
        </div>
      </TodoProvider>
    </ThemeProvider>
  );
}

export default App;

2. Context API para Estado Global

// contexts/TodoContext.js
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { loadTodos, saveTodos } from '../utils/storage';

const TodoContext = createContext();

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload];
    case 'UPDATE_TODO':
      return state.map(todo => 
        todo.id === action.payload.id ? action.payload : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case 'SET_TODOS':
      return action.payload;
    default:
      return state;
  }
};

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, [], loadTodos);

  useEffect(() => {
    saveTodos(todos);
  }, [todos]);

  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
}

export const useTodos = () => {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodos must be used within TodoProvider');
  }
  return context;
};

3. Estrutura de Dados

// Modelo de uma Tarefa
const todoSchema = {
  id: 'uuid-v4',
  title: 'Título da tarefa',
  description: 'Descrição opcional',
  completed: false,
  priority: 'medium', // low, medium, high
  category: 'personal', // personal, work, shopping, health
  tags: ['tag1', 'tag2'],
  dueDate: '2024-12-31',
  createdAt: '2024-01-01T00:00:00.000Z',
  updatedAt: '2024-01-01T00:00:00.000Z'
};

// Categorias
const categories = {
  personal: {
    name: 'Pessoal',
    color: '#6366f1',
    icon: '🏠'
  },
  work: {
    name: 'Trabalho',
    color: '#3b82f6',
    icon: '💼'
  },
  shopping: {
    name: 'Compras',
    color: '#10b981',
    icon: '🛍️'
  },
  health: {
    name: 'Saúde',
    color: '#ef4444',
    icon: '🏥'
  },
  study: {
    name: 'Estudos',
    color: '#f59e0b',
    icon: '📚'
  }
};

4. Componentes Principais

TodoForm

// components/todo/TodoForm/TodoForm.jsx
import React, { useState } from 'react';
import { useTodos } from '../../../contexts/TodoContext';
import { v4 as uuidv4 } from 'uuid';
import styles from './TodoForm.module.css';

function TodoForm({ onClose }) {
  const { dispatch } = useTodos();
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    priority: 'medium',
    category: 'personal',
    dueDate: '',
    tags: []
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    
    const newTodo = {
      ...formData,
      id: uuidv4(),
      completed: false,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };

    dispatch({ type: 'ADD_TODO', payload: newTodo });
    onClose();
  };

  // ... resto do componente
}

TodoItem

// components/todo/TodoItem/TodoItem.jsx
import React from 'react';
import { useTodos } from '../../../contexts/TodoContext';
import { formatDistanceToNow } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import styles from './TodoItem.module.css';

function TodoItem({ todo }) {
  const { dispatch } = useTodos();

  const handleToggle = () => {
    dispatch({ type: 'TOGGLE_TODO', payload: todo.id });
  };

  const handleDelete = () => {
    dispatch({ type: 'DELETE_TODO', payload: todo.id });
  };

  const priorityClass = styles[`priority-${todo.priority}`];
  const categoryStyle = {
    borderLeftColor: categories[todo.category].color
  };

  return (
    <div 
      className={`${styles.todoItem} ${todo.completed ? styles.completed : ''}`}
      style={categoryStyle}
    >
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={handleToggle}
        className={styles.checkbox}
      />
      
      <div className={styles.content}>
        <h3 className={styles.title}>{todo.title}</h3>
        {todo.description && (
          <p className={styles.description}>{todo.description}</p>
        )}
        
        <div className={styles.meta}>
          <span className={`${styles.priority} ${priorityClass}`}>
            {todo.priority}
          </span>
          
          <span className={styles.category}>
            {categories[todo.category].icon} {categories[todo.category].name}
          </span>
          
          {todo.dueDate && (
            <span className={styles.dueDate}>
              📅 {formatDistanceToNow(new Date(todo.dueDate), { 
                locale: ptBR,
                addSuffix: true 
              })}
            </span>
          )}
        </div>
      </div>
      
      <div className={styles.actions}>
        <button onClick={handleEdit}>✏️</button>
        <button onClick={handleDelete}>🗑️</button>
      </div>
    </div>
  );
}

5. Hooks Customizados

useLocalStorage

// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error loading ${key} from localStorage:`, error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`Error saving ${key} to localStorage:`, error);
    }
  };

  return [storedValue, setValue];
}

export default useLocalStorage;

useFilter

// hooks/useFilter.js
import { useState, useMemo } from 'react';

function useFilter(items, initialFilters = {}) {
  const [filters, setFilters] = useState({
    search: '',
    category: 'all',
    priority: 'all',
    status: 'all',
    sortBy: 'createdAt',
    sortOrder: 'desc',
    ...initialFilters
  });

  const filteredItems = useMemo(() => {
    let result = [...items];

    // Busca
    if (filters.search) {
      const search = filters.search.toLowerCase();
      result = result.filter(item => 
        item.title.toLowerCase().includes(search) ||
        item.description?.toLowerCase().includes(search) ||
        item.tags?.some(tag => tag.toLowerCase().includes(search))
      );
    }

    // Categoria
    if (filters.category !== 'all') {
      result = result.filter(item => item.category === filters.category);
    }

    // Prioridade
    if (filters.priority !== 'all') {
      result = result.filter(item => item.priority === filters.priority);
    }

    // Status
    if (filters.status !== 'all') {
      result = result.filter(item => 
        filters.status === 'completed' ? item.completed : !item.completed
      );
    }

    // Ordenação
    result.sort((a, b) => {
      const aValue = a[filters.sortBy];
      const bValue = b[filters.sortBy];
      
      if (filters.sortOrder === 'asc') {
        return aValue > bValue ? 1 : -1;
      } else {
        return aValue < bValue ? 1 : -1;
      }
    });

    return result;
  }, [items, filters]);

  return {
    filters,
    setFilters,
    filteredItems,
    updateFilter: (key, value) => setFilters(prev => ({ ...prev, [key]: value }))
  };
}

export default useFilter;

CSS Modules e Temas

Variáveis CSS

/* styles/variables.css */
:root {
  /* Cores */
  --color-primary: #6366f1;
  --color-secondary: #8b5cf6;
  --color-success: #10b981;
  --color-warning: #f59e0b;
  --color-danger: #ef4444;
  
  /* Tema Claro */
  --bg-primary: #ffffff;
  --bg-secondary: #f3f4f6;
  --text-primary: #111827;
  --text-secondary: #6b7280;
  --border-color: #e5e7eb;
  
  /* Espaçamentos */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  
  /* Bordas */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;
  
  /* Sombras */
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}

/* Tema Escuro */
[data-theme='dark'] {
  --bg-primary: #1f2937;
  --bg-secondary: #111827;
  --text-primary: #f9fafb;
  --text-secondary: #d1d5db;
  --border-color: #374151;
}

Performance e Otimizações

1. Memo e Callbacks

import React, { memo, useCallback, useMemo } from 'react';

const TodoItem = memo(({ todo, onToggle, onDelete }) => {
  // Componente só re-renderiza se props mudarem
  return <div>...</div>;
});

function TodoList() {
  const { todos, dispatch } = useTodos();
  
  const handleToggle = useCallback((id) => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  }, [dispatch]);
  
  const stats = useMemo(() => {
    return {
      total: todos.length,
      completed: todos.filter(t => t.completed).length,
      pending: todos.filter(t => !t.completed).length
    };
  }, [todos]);
  
  return <div>...</div>;
}

2. Lazy Loading

import React, { lazy, Suspense } from 'react';

const TodoStats = lazy(() => import('./components/todo/TodoStats/TodoStats'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <TodoStats />
    </Suspense>
  );
}

Checklist de Desenvolvimento

  • Estrutura de pastas criada
  • Componentes base implementados
  • Context API configurado
  • Hooks customizados criados
  • Sistema de temas implementado
  • CSS Modules configurados
  • LocalStorage integrado
  • Performance otimizada
3

Atividade: Setup Inicial do Projeto

Configure o ambiente e estrutura inicial

Activity

Atividade: Setup Inicial do Projeto

Objetivo

Configurar o ambiente de desenvolvimento e criar a estrutura inicial do projeto.

Passo 1: Criar o Projeto

# Criar novo projeto React
npx create-react-app todo-app
cd todo-app

# Instalar dependências adicionais
npm install uuid date-fns

# Iniciar o servidor de desenvolvimento
npm start

Passo 2: Limpar Estrutura Inicial

Remova os arquivos desnecessários:

# No diretório src/
rm App.test.js logo.svg setupTests.js reportWebVitals.js

Passo 3: Criar Estrutura de Pastas

# Criar diretórios
mkdir -p src/components/common
mkdir -p src/components/todo
mkdir -p src/components/layout
mkdir -p src/hooks
mkdir -p src/utils
mkdir -p src/contexts
mkdir -p src/styles

Passo 4: Arquivos Base

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './styles/globals.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

src/styles/globals.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-size: 16px;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 
    'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 
    'Helvetica Neue', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: var(--bg-primary);
  color: var(--text-primary);
  line-height: 1.6;
}

button {
  cursor: pointer;
  border: none;
  font-family: inherit;
  font-size: inherit;
}

input, textarea, select {
  font-family: inherit;
  font-size: inherit;
}

a {
  color: inherit;
  text-decoration: none;
}

ul {
  list-style: none;
}

src/styles/variables.css

:root {
  /* Cores principais */
  --color-primary: #6366f1;
  --color-primary-dark: #4f46e5;
  --color-primary-light: #818cf8;
  
  --color-secondary: #8b5cf6;
  --color-success: #10b981;
  --color-warning: #f59e0b;
  --color-danger: #ef4444;
  --color-info: #3b82f6;
  
  /* Tema claro */
  --bg-primary: #ffffff;
  --bg-secondary: #f3f4f6;
  --bg-tertiary: #e5e7eb;
  
  --text-primary: #111827;
  --text-secondary: #6b7280;
  --text-tertiary: #9ca3af;
  
  --border-color: #d1d5db;
  --border-color-light: #e5e7eb;
  
  /* Espaçamentos */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  --spacing-2xl: 3rem;
  
  /* Tamanhos de fonte */
  --font-xs: 0.75rem;
  --font-sm: 0.875rem;
  --font-md: 1rem;
  --font-lg: 1.125rem;
  --font-xl: 1.25rem;
  --font-2xl: 1.5rem;
  --font-3xl: 2rem;
  
  /* Bordas */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;
  --radius-full: 9999px;
  
  /* Sombras */
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
  --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  
  /* Transições */
  --transition-fast: 150ms ease-in-out;
  --transition-normal: 250ms ease-in-out;
  --transition-slow: 350ms ease-in-out;
  
  /* Z-index */
  --z-dropdown: 100;
  --z-modal: 200;
  --z-tooltip: 300;
}

/* Tema escuro */
[data-theme='dark'] {
  --bg-primary: #1f2937;
  --bg-secondary: #111827;
  --bg-tertiary: #030712;
  
  --text-primary: #f9fafb;
  --text-secondary: #d1d5db;
  --text-tertiary: #9ca3af;
  
  --border-color: #374151;
  --border-color-light: #1f2937;
}

src/App.jsx

import React from 'react';
import './styles/variables.css';
import styles from './App.module.css';

function App() {
  return (
    <div className={styles.app}>
      <header className={styles.header}>
        <h1>Todo App</h1>
      </header>
      
      <main className={styles.main}>
        <p>Projeto configurado e pronto para desenvolvimento!</p>
      </main>
    </div>
  );
}

export default App;

src/App.module.css

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.header {
  background-color: var(--bg-secondary);
  padding: var(--spacing-lg);
  border-bottom: 1px solid var(--border-color);
  box-shadow: var(--shadow-sm);
}

.header h1 {
  color: var(--color-primary);
  font-size: var(--font-2xl);
  text-align: center;
}

.main {
  flex: 1;
  padding: var(--spacing-xl);
  max-width: 1200px;
  margin: 0 auto;
  width: 100%;
}

Passo 5: Utils Básicas

src/utils/constants.js

export const PRIORITIES = {
  LOW: 'low',
  MEDIUM: 'medium',
  HIGH: 'high'
};

export const CATEGORIES = {
  personal: {
    id: 'personal',
    name: 'Pessoal',
    color: '#6366f1',
    icon: '🏠'
  },
  work: {
    id: 'work',
    name: 'Trabalho',
    color: '#3b82f6',
    icon: '💼'
  },
  shopping: {
    id: 'shopping',
    name: 'Compras',
    color: '#10b981',
    icon: '🛍️'
  },
  health: {
    id: 'health',
    name: 'Saúde',
    color: '#ef4444',
    icon: '🏥'
  },
  study: {
    id: 'study',
    name: 'Estudos',
    color: '#f59e0b',
    icon: '📚'
  },
  other: {
    id: 'other',
    name: 'Outros',
    color: '#8b5cf6',
    icon: '📌'
  }
};

export const SORT_OPTIONS = {
  CREATED_AT_DESC: { field: 'createdAt', order: 'desc', label: 'Mais recentes' },
  CREATED_AT_ASC: { field: 'createdAt', order: 'asc', label: 'Mais antigas' },
  DUE_DATE_ASC: { field: 'dueDate', order: 'asc', label: 'Prazo (mais próximas)' },
  DUE_DATE_DESC: { field: 'dueDate', order: 'desc', label: 'Prazo (mais distantes)' },
  PRIORITY_DESC: { field: 'priority', order: 'desc', label: 'Prioridade (alta primeiro)' },
  PRIORITY_ASC: { field: 'priority', order: 'asc', label: 'Prioridade (baixa primeiro)' },
  TITLE_ASC: { field: 'title', order: 'asc', label: 'Título (A-Z)' },
  TITLE_DESC: { field: 'title', order: 'desc', label: 'Título (Z-A)' }
};

export const FILTER_OPTIONS = {
  STATUS: {
    ALL: 'all',
    ACTIVE: 'active',
    COMPLETED: 'completed'
  },
  TIME: {
    ALL: 'all',
    TODAY: 'today',
    WEEK: 'week',
    MONTH: 'month',
    OVERDUE: 'overdue'
  }
};

src/utils/storage.js

const STORAGE_KEY = 'todo-app-data';

export const loadTodos = () => {
  try {
    const data = localStorage.getItem(STORAGE_KEY);
    return data ? JSON.parse(data) : [];
  } catch (error) {
    console.error('Error loading todos from localStorage:', error);
    return [];
  }
};

export const saveTodos = (todos) => {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
  } catch (error) {
    console.error('Error saving todos to localStorage:', error);
  }
};

export const exportData = (todos) => {
  const dataStr = JSON.stringify(todos, null, 2);
  const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
  
  const exportFileDefaultName = `todos-backup-${new Date().toISOString().slice(0, 10)}.json`;
  
  const linkElement = document.createElement('a');
  linkElement.setAttribute('href', dataUri);
  linkElement.setAttribute('download', exportFileDefaultName);
  linkElement.click();
};

export const importData = (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const todos = JSON.parse(e.target.result);
        resolve(todos);
      } catch (error) {
        reject(new Error('Invalid file format'));
      }
    };
    reader.onerror = () => reject(new Error('Error reading file'));
    reader.readAsText(file);
  });
};

Passo 6: Verificar Instalação

  1. Execute npm start
  2. Acesse http://localhost:3000
  3. Você deve ver a mensagem "Projeto configurado e pronto para desenvolvimento!"

Checklist

  • Projeto criado com create-react-app
  • Dependências instaladas (uuid, date-fns)
  • Estrutura de pastas criada
  • Arquivos desnecessários removidos
  • Estilos globais configurados
  • Variáveis CSS definidas
  • Utils básicas criadas
  • App inicial funcionando

Próximos Passos

  1. Implementar Context API
  2. Criar componentes base
  3. Configurar sistema de temas
  4. Implementar hooks customizados

Dicas

  • Use CSS Modules para evitar conflitos de estilos
  • Mantenha componentes pequenos e focados
  • Teste cada funcionalidade conforme desenvolve
  • Commite frequentemente no Git
3 content items