6.6 KiB
6.6 KiB
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
// 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
// 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:
useStatepara UI state - Server state: Custom hooks com
fetche polling - Form state:
useStateou 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
// 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
// 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
// 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',
});
},
};