feat(frontend): Setup Next.js with Zustand stores and API client - Phase 2 initialization

This commit is contained in:
Erik Silva
2025-12-01 01:43:21 -03:00
parent 5ff1a4a004
commit 888e4e4d60
7 changed files with 990 additions and 20 deletions

View File

@@ -0,0 +1,220 @@
/**
* 🔐 Auth Store (Zustand)
* Gerenciador de estado de autenticação
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import apiClient, { handleApiError } from '../api';
import {
AuthState,
AuthUser,
SignupPayload,
LoginPayload,
AuthResponse,
} from '../types';
interface AuthStore extends AuthState {
// ações
signup: (payload: SignupPayload) => Promise<void>;
login: (payload: LoginPayload) => Promise<void>;
logout: () => Promise<void>;
getProfile: () => Promise<AuthUser | null>;
setError: (error: string | null) => void;
setLoading: (loading: boolean) => void;
}
/**
* Decodificar JWT para pegar o userId
*/
const decodeToken = (token: string): { userId: string } | null => {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join(''),
);
return JSON.parse(jsonPayload);
} catch {
return null;
}
};
/**
* Criar store de autenticação
*/
export const useAuthStore = create<AuthStore>()(
persist(
(set, get) => ({
// Estado inicial
user: null,
token: null,
isAuthenticated: false,
isLoading: false,
error: null,
// ============================================================================
// AÇÕES
// ============================================================================
/**
* Registrar novo usuário
*/
signup: async (payload: SignupPayload) => {
try {
set({ isLoading: true, error: null });
const response = await apiClient.post<AuthResponse>(
'/auth/signup',
payload,
);
const { access_token, user } = response.data;
set({
token: access_token,
user,
isAuthenticated: true,
isLoading: false,
});
// Salvar token no localStorage
localStorage.setItem('auth_token', access_token);
localStorage.setItem('auth_user', JSON.stringify(user));
} catch (error) {
const message = handleApiError(error);
set({ error: message, isLoading: false });
throw new Error(message);
}
},
/**
* Fazer login
*/
login: async (payload: LoginPayload) => {
try {
set({ isLoading: true, error: null });
const response = await apiClient.post<AuthResponse>(
'/auth/login',
payload,
);
const { access_token, user } = response.data;
set({
token: access_token,
user,
isAuthenticated: true,
isLoading: false,
});
// Salvar no localStorage
localStorage.setItem('auth_token', access_token);
localStorage.setItem('auth_user', JSON.stringify(user));
} catch (error) {
const message = handleApiError(error);
set({ error: message, isLoading: false });
throw new Error(message);
}
},
/**
* Fazer logout
*/
logout: async () => {
try {
set({ isLoading: true });
// Chamar endpoint de logout (opcional)
await apiClient.post('/auth/logout');
} catch (error) {
console.error('Erro ao fazer logout:', error);
} finally {
// Limpar estado e localStorage
set({
user: null,
token: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
localStorage.removeItem('auth_store');
}
},
/**
* Obter perfil do usuário
*/
getProfile: async () => {
try {
const response = await apiClient.get<AuthUser>('/auth/me');
const user = response.data;
set({ user, isAuthenticated: true });
localStorage.setItem('auth_user', JSON.stringify(user));
return user;
} catch (error) {
const message = handleApiError(error);
set({ error: message, isAuthenticated: false });
return null;
}
},
/**
* Setar erro
*/
setError: (error: string | null) => {
set({ error });
},
/**
* Setar loading
*/
setLoading: (isLoading: boolean) => {
set({ isLoading });
},
}),
// Configurar persistência
{
name: 'auth_store',
partialize: (state) => ({
token: state.token,
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
// Validar token ao recuperar do storage
onRehydrateStorage: () => (state) => {
if (state?.token) {
const decoded = decodeToken(state.token);
if (!decoded) {
state.token = null;
state.isAuthenticated = false;
}
}
},
},
),
);
// Exportar hook separado para debugging
export const useAuth = () => {
const store = useAuthStore();
return {
user: store.user,
token: store.token,
isAuthenticated: store.isAuthenticated,
isLoading: store.isLoading,
error: store.error,
login: store.login,
signup: store.signup,
logout: store.logout,
getProfile: store.getProfile,
};
};

View File

@@ -0,0 +1,6 @@
/**
* 🛒 Store Exports
*/
export { useAuthStore, useAuth } from './auth.store';
export { useTasksStore, useTasks } from './tasks.store';

View File

@@ -0,0 +1,248 @@
/**
* 📝 Tasks Store (Zustand)
* Gerenciador de estado das tarefas
*/
import { create } from 'zustand';
import apiClient, { handleApiError } from '../api';
import {
TasksState,
Task,
CreateTaskPayload,
UpdateTaskPayload,
TaskFilters,
TasksListResponse,
TaskStatsResponse,
} from '../types';
interface TasksStore extends TasksState {
// Ações
fetchTasks: (filters?: TaskFilters) => Promise<void>;
fetchStats: () => Promise<void>;
createTask: (payload: CreateTaskPayload) => Promise<Task | null>;
updateTask: (id: string, payload: UpdateTaskPayload) => Promise<Task | null>;
deleteTask: (id: string) => Promise<boolean>;
setFilters: (filters: TaskFilters) => void;
setError: (error: string | null) => void;
setLoading: (loading: boolean) => void;
clear: () => void;
}
/**
* Criar store de tarefas
*/
export const useTasksStore = create<TasksStore>((set, get) => ({
// Estado inicial
tasks: [],
stats: null,
isLoading: false,
error: null,
filters: {
completed: undefined,
sortBy: 'created_at',
order: 'desc',
},
// ============================================================================
// AÇÕES
// ============================================================================
/**
* Buscar tarefas com filtros
*/
fetchTasks: async (filters?: TaskFilters) => {
try {
set({ isLoading: true, error: null });
if (filters) {
set({ filters });
}
const currentFilters = filters || get().filters;
const params = new URLSearchParams();
if (currentFilters.completed !== undefined) {
params.append('completed', String(currentFilters.completed));
}
if (currentFilters.category) {
params.append('category', currentFilters.category);
}
if (currentFilters.priority) {
params.append('priority', currentFilters.priority);
}
if (currentFilters.sortBy) {
params.append('sortBy', currentFilters.sortBy);
}
if (currentFilters.order) {
params.append('order', currentFilters.order);
}
const url = `/tasks${params.toString() ? '?' + params.toString() : ''}`;
const response = await apiClient.get<TasksListResponse>(url);
set({
tasks: response.data.data,
isLoading: false,
});
} catch (error) {
const message = handleApiError(error);
set({ error: message, isLoading: false, tasks: [] });
}
},
/**
* Buscar estatísticas
*/
fetchStats: async () => {
try {
const response = await apiClient.get<TaskStatsResponse>('/tasks/stats');
set({ stats: response.data.data });
} catch (error) {
console.error('Erro ao buscar estatísticas:', error);
}
},
/**
* Criar tarefa
*/
createTask: async (payload: CreateTaskPayload) => {
try {
set({ isLoading: true, error: null });
const response = await apiClient.post<{ data: Task }>('/tasks', payload);
const newTask = response.data.data;
// Adicionar à lista
set({ tasks: [newTask, ...get().tasks] });
// Atualizar stats se houver
if (get().stats) {
await get().fetchStats();
}
return newTask;
} catch (error) {
const message = handleApiError(error);
set({ error: message });
throw new Error(message);
} finally {
set({ isLoading: false });
}
},
/**
* Atualizar tarefa
*/
updateTask: async (id: string, payload: UpdateTaskPayload) => {
try {
set({ isLoading: true, error: null });
const response = await apiClient.patch<{ data: Task }>(
`/tasks/${id}`,
payload,
);
const updatedTask = response.data.data;
// Atualizar na lista
set({
tasks: get().tasks.map((t) => (t.id === id ? updatedTask : t)),
});
// Atualizar stats se houver
if (get().stats) {
await get().fetchStats();
}
return updatedTask;
} catch (error) {
const message = handleApiError(error);
set({ error: message });
throw new Error(message);
} finally {
set({ isLoading: false });
}
},
/**
* Deletar tarefa
*/
deleteTask: async (id: string) => {
try {
set({ isLoading: true, error: null });
await apiClient.delete(`/tasks/${id}`);
// Remover da lista
set({ tasks: get().tasks.filter((t) => t.id !== id) });
// Atualizar stats se houver
if (get().stats) {
await get().fetchStats();
}
return true;
} catch (error) {
const message = handleApiError(error);
set({ error: message });
return false;
} finally {
set({ isLoading: false });
}
},
/**
* Setar filtros
*/
setFilters: (filters: TaskFilters) => {
set({ filters });
},
/**
* Setar erro
*/
setError: (error: string | null) => {
set({ error });
},
/**
* Setar loading
*/
setLoading: (isLoading: boolean) => {
set({ isLoading });
},
/**
* Limpar estado
*/
clear: () => {
set({
tasks: [],
stats: null,
isLoading: false,
error: null,
filters: {
completed: undefined,
sortBy: 'created_at',
order: 'desc',
},
});
},
}));
// Hook customizado
export const useTasks = () => {
const store = useTasksStore();
return {
tasks: store.tasks,
stats: store.stats,
isLoading: store.isLoading,
error: store.error,
filters: store.filters,
fetchTasks: store.fetchTasks,
fetchStats: store.fetchStats,
createTask: store.createTask,
updateTask: store.updateTask,
deleteTask: store.deleteTask,
setFilters: store.setFilters,
};
};