Estrutura do Frontend
O frontend segue uma arquitetura em camadas inspirada em Clean Architecture, separando claramente as responsabilidades de apresentação, lógica de negócio e infraestrutura.
Camadas da Aplicação
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Pages │ │ Components │ │ Theme │ │
│ │ (Rotas) │ │ (UI Blocks) │ │ (Layouts) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ APPLICATION LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Queries │ │ Hooks │ │ Schemas │ │
│ │(React Query)│ │ (Custom) │ │ (Zod) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ INFRASTRUCTURE LAYER │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ API │ │ Axios │ │
│ │ (Clients) │ │ (Config) │ │
│ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ STORE LAYER │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Zustand (Global State) │ │
│ │ auth | user | onboarding | solicitation │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Presentation Layer
Pages
Páginas organizadas por tipo de usuário e funcionalidade:
pages/
├── login/
│ ├── LoginPage.tsx
│ ├── EmailNotVerified.tsx
│ └── ResendVerification.tsx
│
├── register/
│ ├── index.tsx # Wizard de registro
│ ├── steps/
│ │ ├── EntrepreneurStep.tsx
│ │ └── StructuringAgentStep.tsx
│ └── schemas/
│
└── private/
├── admin/ # Gestor
│ ├── dashboard/
│ ├── business-plan/
│ ├── mentor-requests/
│ └── profile/
│
├── agent/ # Agente Estruturador
│ ├── Page.tsx # Kanban Board
│ ├── metrics/
│ ├── profile/
│ └── solicitacoes/
│
└── user/ # Empreendedor
├── solicitations/
├── credit-lines/
└── profile/
Components
Componentes reutilizáveis organizados por categoria:
components/
├── common/
│ ├── Alert/
│ ├── DialogComponent/
│ ├── FileUpload/
│ ├── InputComponent/
│ ├── Loading/
│ ├── PaginationWithInfo/
│ ├── ProfilePage/
│ ├── RichTextEditor/ # Editor Tiptap
│ ├── Search/
│ ├── SelectComponent/
│ ├── Table/
│ ├── TextAreaComponent/
│ └── Toast/
│
├── graphs/ # Componentes Recharts
│ ├── BarChart/
│ ├── PieChart/
│ └── LineChart/
│
├── KanbanBoard/ # Kanban com dnd-kit
│ ├── Board.tsx
│ ├── Column.tsx
│ └── Card.tsx
│
└── Onboarding/ # Wizard de registro
├── Stepper.tsx
└── StepContent.tsx
Theme
Configuração de tema e layouts:
theme/
├── colors/
│ └── index.ts # Paleta de cores
│
├── layout/
│ ├── Header/
│ ├── AdminLayout/ # Layout do Gestor
│ ├── AgentLayout/ # Layout do Agente
│ └── UserLayout/ # Layout do Empreendedor
│
└── index.ts # MUI Theme config
Application Layer
Queries (React Query)
queries/
├── agent/
│ ├── useAgentMetrics.ts
│ └── useAgentBoard.ts
│
├── business-plans/
│ ├── useBusinessPlans.ts
│ ├── useBusinessPlan.ts
│ └── useCreateBusinessPlan.ts
│
├── manager/
│ ├── useApprovalRequests.ts
│ └── useManagerMetrics.ts
│
├── structuring-agent/
│ └── useStructuringAgents.ts
│
└── schemas/
├── businessPlanSchema.ts
└── userSchema.ts
Exemplo de Query Hook
// queries/business-plans/useBusinessPlans.ts
import { useQuery } from "@tanstack/react-query";
import { businessPlansApi } from "@/infrastructure/api/business-plans";
export const useBusinessPlans = (filters?: BusinessPlanFilters) => {
return useQuery({
queryKey: ["business-plans", filters],
queryFn: () => businessPlansApi.list(filters),
staleTime: 5 * 60 * 1000, // 5 minutos
});
};
Infrastructure Layer
API Clients
api/
├── auth/
│ └── auth-api.ts # Login, register, verify
│
├── business-plans/
│ └── business-plans-api.ts
│
├── profile/
│ └── profile-api.ts
│
├── register/
│ └── register-api.ts
│
├── agent/
│ └── metrics/
│ └── metrics-api.ts
│
├── manager/
│ └── metrics/
│ └── metrics-api.ts
│
├── structuring-agent/
│ └── structuring-agent-api.ts
│
└── util/
└── http.ts # Axios config
Configuração do Axios
// lib/axios.ts
import axios from "axios";
import { useAuthStore } from "@/store/auth/authStore";
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
"Content-Type": "application/json",
},
});
// Interceptor de request para adicionar token
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Interceptor de response para refresh token
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Lógica de refresh token
}
return Promise.reject(error);
}
);
export default api;
Store Layer (Zustand)
store/
├── auth/
│ └── authStore.ts # Tokens JWT
│
├── user/
│ └── userStore.ts # Dados do usuário
│
├── onboarding/
│ └── onboardingStore.ts # Estado do wizard
│
├── agentBoardStore.ts # Estado do Kanban
├── solicitationStore.ts # Solicitações
├── pendingCorrectionStore.ts
│
└── utils/
└── formatCurrency.ts
Exemplo de Store
// store/user/userStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface UserState {
user: User | null;
role: "entrepreneur" | "agent" | "manager" | null;
setUser: (user: User) => void;
clearUser: () => void;
}
export const useUserStore = create<UserState>()(
persist(
(set) => ({
user: null,
role: null,
setUser: (user) => set({ user, role: user.role }),
clearUser: () => set({ user: null, role: null }),
}),
{
name: "user-storage",
}
)
);
Routes
routes/
├── index.tsx # Router principal
├── adminRoutes.tsx # /admin/*
├── agentRoutes.tsx # /agent/*
├── userRoutes.tsx # /user/*
├── loginRoutes.tsx # /login, /register
└── auth/
└── PrivateRoute.tsx # Proteção de rotas
Proteção de Rotas
// routes/auth/PrivateRoute.tsx
import { Navigate } from "react-router-dom";
import { useUserStore } from "@/store/user/userStore";
interface PrivateRouteProps {
children: React.ReactNode;
allowedRoles?: ("entrepreneur" | "agent" | "manager")[];
}
export const PrivateRoute: FC<PrivateRouteProps> = ({ children, allowedRoles }) => {
const { user, role } = useUserStore();
if (!user) {
return <Navigate to="/login" />;
}
if (allowedRoles && !allowedRoles.includes(role!)) {
return <Navigate to="/unauthorized" />;
}
return <>{children}</>;
};