Planejamento e Arquitetura do Projeto
Módulo 5: Projeto Final - Lista de Tarefas
Aula 1
1
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
- Execute
npm start - Acesse http://localhost:3000
- 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
- Implementar Context API
- Criar componentes base
- Configurar sistema de temas
- 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