260 lines
6.6 KiB
Markdown
260 lines
6.6 KiB
Markdown
# Arquitetura Frontend
|
|
|
|
## Arquitetura de Componentes
|
|
|
|
### Organização de Componentes
|
|
|
|
```
|
|
src/
|
|
├── components/
|
|
│ ├── ui/ # Shadcn/ui components
|
|
│ ├── layout/
|
|
│ │ ├── Header.tsx
|
|
│ │ ├── Footer.tsx
|
|
│ │ └── DashboardLayout.tsx
|
|
│ ├── whatsapp/
|
|
│ │ ├── WhatsAppInstanceCard.tsx
|
|
│ │ ├── WhatsAppInstanceList.tsx
|
|
│ │ └── QRCodeModal.tsx
|
|
│ └── google-calendar/
|
|
│ └── GoogleCalendarCard.tsx
|
|
```
|
|
|
|
### Template de Componente
|
|
|
|
```typescript
|
|
// components/whatsapp/WhatsAppInstanceCard.tsx
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { WhatsAppInstance } from '@/types/whatsapp';
|
|
|
|
interface WhatsAppInstanceCardProps {
|
|
instance: WhatsAppInstance;
|
|
onGenerateQRCode: (instanceName: string) => void;
|
|
onDisconnect: (instanceName: string) => void;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
export function WhatsAppInstanceCard({
|
|
instance,
|
|
onGenerateQRCode,
|
|
onDisconnect,
|
|
isLoading = false,
|
|
}: WhatsAppInstanceCardProps) {
|
|
const isConnected = instance.status === 'connected';
|
|
|
|
return (
|
|
<Card className="bg-gray-800 border-gray-700">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span className="text-white">{instance.instanceName}</span>
|
|
<Badge variant={isConnected ? 'success' : 'destructive'}>
|
|
{isConnected ? 'Conectado' : 'Desconectado'}
|
|
</Badge>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{!isConnected && (
|
|
<Button
|
|
onClick={() => onGenerateQRCode(instance.instanceName)}
|
|
disabled={isLoading}
|
|
className="w-full bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
Gerar QR Code
|
|
</Button>
|
|
)}
|
|
{isConnected && (
|
|
<Button
|
|
onClick={() => onDisconnect(instance.instanceName)}
|
|
disabled={isLoading}
|
|
variant="outline"
|
|
className="w-full"
|
|
>
|
|
Desconectar
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Gerenciamento de Estado
|
|
|
|
### Estrutura de Estado
|
|
|
|
```typescript
|
|
// stores/auth.ts
|
|
import { createContext, useContext, useState, useEffect } from 'react';
|
|
import { User } from '@supabase/supabase-js';
|
|
import { supabase } from '@/lib/supabase';
|
|
|
|
interface AuthContextType {
|
|
user: User | null;
|
|
loading: boolean;
|
|
signOut: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
supabase.auth.getSession().then(({ data: { session } }) => {
|
|
setUser(session?.user ?? null);
|
|
setLoading(false);
|
|
});
|
|
|
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
|
setUser(session?.user ?? null);
|
|
});
|
|
|
|
return () => subscription.unsubscribe();
|
|
}, []);
|
|
|
|
const signOut = async () => {
|
|
await supabase.auth.signOut();
|
|
setUser(null);
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, loading, signOut }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
if (!context) throw new Error('useAuth must be used within AuthProvider');
|
|
return context;
|
|
}
|
|
```
|
|
|
|
### Padrões de Gerenciamento de Estado
|
|
|
|
- **Global auth state:** React Context (`AuthContext`) para sessão
|
|
- **Component-local state:** `useState` para UI state
|
|
- **Server state:** Custom hooks com `fetch` e polling
|
|
- **Form state:** `useState` ou React Hook Form
|
|
|
|
## Arquitetura de Roteamento
|
|
|
|
### Organização de Rotas
|
|
|
|
```
|
|
app/
|
|
├── (auth)/
|
|
│ ├── login/page.tsx
|
|
│ └── reset-password/page.tsx
|
|
├── (dashboard)/
|
|
│ ├── layout.tsx # Protected layout
|
|
│ └── dashboard/page.tsx
|
|
├── api/
|
|
│ ├── whatsapp/
|
|
│ └── google-calendar/
|
|
└── layout.tsx # Root layout
|
|
```
|
|
|
|
### Padrão de Rota Protegida
|
|
|
|
```typescript
|
|
// app/(dashboard)/layout.tsx
|
|
'use client';
|
|
|
|
import { useAuth } from '@/stores/auth';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useEffect } from 'react';
|
|
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
|
|
|
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
|
|
const { user, loading } = useAuth();
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
if (!loading && !user) {
|
|
router.push('/login');
|
|
}
|
|
}, [user, loading, router]);
|
|
|
|
if (loading) return <div>Loading...</div>;
|
|
if (!user) return null;
|
|
|
|
return <DashboardLayout>{children}</DashboardLayout>;
|
|
}
|
|
```
|
|
|
|
## Camada de Serviços Frontend
|
|
|
|
### Setup do Cliente API
|
|
|
|
```typescript
|
|
// lib/api-client.ts
|
|
import { supabase } from '@/lib/supabase';
|
|
|
|
interface FetchOptions extends RequestInit {
|
|
requireAuth?: boolean;
|
|
}
|
|
|
|
export async function apiClient<T>(
|
|
endpoint: string,
|
|
options: FetchOptions = {}
|
|
): Promise<T> {
|
|
const { requireAuth = true, ...fetchOptions } = options;
|
|
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
...fetchOptions.headers,
|
|
};
|
|
|
|
if (requireAuth) {
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
if (!session) throw new Error('Unauthorized');
|
|
headers['Authorization'] = `Bearer ${session.access_token}`;
|
|
}
|
|
|
|
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}${endpoint}`, {
|
|
...fetchOptions,
|
|
headers,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errorData = await res.json().catch(() => ({ error: { message: 'Unknown error' } }));
|
|
throw new Error(errorData.error?.message || `Request failed: ${res.status}`);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
```
|
|
|
|
### Exemplo de Serviço
|
|
|
|
```typescript
|
|
// services/whatsapp.service.ts
|
|
import { apiClient } from '@/lib/api-client';
|
|
import { WhatsAppInstance } from '@/types/whatsapp';
|
|
|
|
export const whatsappService = {
|
|
async getInstances(): Promise<WhatsAppInstance[]> {
|
|
return apiClient<WhatsAppInstance[]>('/api/whatsapp/instances');
|
|
},
|
|
|
|
async generateQRCode(instanceName: string): Promise<{ qrCode: string }> {
|
|
return apiClient<{ qrCode: string }>(`/api/whatsapp/instances/${instanceName}/qrcode`, {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
async disconnectInstance(instanceName: string): Promise<{ success: boolean }> {
|
|
return apiClient(`/api/whatsapp/instances/${instanceName}/disconnect`, {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
};
|
|
```
|
|
|
|
---
|