Dashboard-Automatizase/docs/architecture/arquitetura-frontend.md
2025-10-05 21:17:43 -03:00

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: 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

// 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',
    });
  },
};