# 10. Orientações de Desenvolvimento ## 10.1 Configuração do Ambiente Local ### Pré-requisitos - **Node.js 20+** — instalar via [nvm](https://github.com/nvm-sh/nvm) (recomendado) - **pnpm 8+** — `npm install -g pnpm` - **Git 2.40+** - Conta **Supabase** (base de dados + storage) - Conta **Stripe** (modo teste disponível gratuitamente) - Conta **Resend** (emails) - Chave API **Anthropic** (IA) ### Setup Inicial ```bash # 1. Clonar o repositório git clone https://github.com/pawlink/pawlink.git cd pawlink # 2. Instalar dependências pnpm install # 3. Copiar template de variáveis de ambiente cp .env.example .env.local # Preencher .env.local com as tuas chaves # 4. Aplicar migrações da base de dados npx prisma migrate dev # 5. Popular a base de dados com dados de teste npx prisma db seed # 6. Iniciar o servidor de desenvolvimento pnpm dev # Aplicação disponível em http://localhost:3000 ``` --- ## 10.2 Convenções de Código ### TypeScript ```typescript // ✅ Usar tipos explícitos em funções públicas export async function getAnimalById(id: string): Promise { ... } // ✅ Interfaces para props de componentes interface AnimalCardProps { animal: Animal; onReserve?: (id: string) => void; } // ✅ Zod para validação — nunca "as" para fazer cast de dados externos const schema = z.object({ id: z.string().cuid() }); const data = schema.parse(body); // lança erro se inválido // ❌ Evitar const data = body as { id: string }; // não valida nada ``` ### Componentes React ```typescript // ✅ Server Components por defeito (sem 'use client') // Apenas adicionar 'use client' quando necessário (hooks, eventos, browser APIs) // ✅ Async Server Components para fetch de dados async function AnimalPage({ params }: { params: { id: string } }) { const animal = await getAnimalById(params.id); // fetch directo no servidor if (!animal) notFound(); return ; } // ✅ Separar lógica de UI — componente de apresentação vs. componente de dados // AnimalProfile.tsx — recebe dados como props, sem fetch // app/(main)/animals/[id]/page.tsx — faz fetch, passa para AnimalProfile ``` ### API Routes ```typescript // ✅ Padrão de API Route export async function GET(req: Request) { try { // 1. Autenticação (se necessário) const session = await getServerSession(authConfig); if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 }); // 2. Validação de inputs const { searchParams } = new URL(req.url); const params = FiltersSchema.safeParse(Object.fromEntries(searchParams)); if (!params.success) return Response.json({ error: params.error.issues }, { status: 400 }); // 3. Lógica de negócio const data = await getAnimals(params.data); // 4. Resposta return Response.json(data); } catch (error) { console.error('[GET /api/animals]', error); return Response.json({ error: 'Internal Server Error' }, { status: 500 }); } } ``` --- ## 10.3 Gestão de Estado | Tipo de Estado | Solução | Quando usar | |---|---|---| | Dados do servidor | TanStack Query | Listas de animais, detalhes, reservas — qualquer dado que vem da API | | Estado global do cliente | Zustand | Sessão do utilizador, preferências UI (tema), estado do menu | | Estado de formulários | React Hook Form | Todos os formulários — registo, doação, reserva | | Estado de UI local | `useState` | Toggles, modais, estados temporários dentro de um componente | ```typescript // ✅ Exemplo: store Zustand para preferências do utilizador import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface UIStore { darkMode: boolean; toggleDarkMode: () => void; } export const useUIStore = create()( persist( (set) => ({ darkMode: false, toggleDarkMode: () => set(state => ({ darkMode: !state.darkMode })), }), { name: 'ui-preferences' } ) ); ``` --- ## 10.4 Fluxo de Trabalho Git ### Branches ``` main → código estável em produção develop → integração contínua de funcionalidades feature/* → novas funcionalidades (ex: feature/donation-flow) fix/* → correcção de bugs (ex: fix/age-validation) docs/* → alterações de documentação ``` ### Processo de Desenvolvimento ```bash # 1. Criar branch a partir de develop git checkout develop git pull origin develop git checkout -b feature/nome-da-funcionalidade # 2. Desenvolver + commits frequentes git add . git commit -m "feat: add animal filter by district" # 3. Push e Pull Request para develop git push origin feature/nome-da-funcionalidade # Abrir PR no GitHub com descrição do que foi feito # 4. Após review e testes → merge para develop # 5. Periodicamente → release: merge develop para main ``` ### Mensagens de Commit (Conventional Commits) | Prefixo | Uso | |---|---| | `feat:` | Nova funcionalidade | | `fix:` | Correcção de bug | | `docs:` | Alteração de documentação | | `style:` | Formatação, espaços (sem alteração de lógica) | | `refactor:` | Refactorização sem nova funcionalidade | | `test:` | Adicionar ou corrigir testes | | `chore:` | Tarefas de manutenção (deps, config) | ```bash # ✅ Exemplos de boas mensagens feat: add reservation calendar with available dates fix: correct age validation for February 29 birthdays docs: update API routes documentation refactor: extract donation logic to separate service test: add E2E tests for adoption flow ``` --- ## 10.5 Testes ### Estrutura de Testes ``` pawlink/ ├── __tests__/ │ ├── unit/ │ │ ├── age-validation.test.ts │ │ ├── format.test.ts │ │ └── schemas.test.ts │ ├── integration/ │ │ ├── api/ │ │ │ ├── animals.test.ts │ │ │ └── reservations.test.ts │ └── components/ │ ├── AnimalCard.test.tsx │ └── DonationFlow.test.tsx └── e2e/ ├── auth.spec.ts ├── adoption.spec.ts └── donation.spec.ts ``` ### Exemplos de Testes ```typescript // __tests__/unit/age-validation.test.ts import { isAdult } from '@/lib/auth/age-validation'; import { describe, it, expect } from 'vitest'; describe('isAdult', () => { it('deve retornar true para utilizador com exactamente 18 anos hoje', () => { const today = new Date(); const birthdate = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate()); expect(isAdult(birthdate)).toBe(true); }); it('deve retornar false para utilizador com 17 anos', () => { const today = new Date(); const birthdate = new Date(today.getFullYear() - 17, today.getMonth(), today.getDate()); expect(isAdult(birthdate)).toBe(false); }); }); ``` ```typescript // e2e/adoption.spec.ts (Playwright) import { test, expect } from '@playwright/test'; test('fluxo completo de adopção', async ({ page }) => { // Login await page.goto('/login'); await page.fill('[name="email"]', 'teste@pawlink.pt'); await page.fill('[name="password"]', 'Password123!'); await page.click('[type="submit"]'); // Navegar para animal await page.goto('/animals'); await page.click('[data-testid="animal-card"]:first-child'); // Reservar await page.click('[data-testid="adopt-button"]'); await page.click('[data-testid="calendar-day-available"]:first-child'); await page.click('[data-testid="confirm-reservation"]'); // Verificar confirmação await expect(page.locator('[data-testid="reservation-success"]')).toBeVisible(); }); ``` --- ## 10.6 Performance - **Imagens:** usar sempre `next/image` com `sizes` adequado — geração automática WebP e lazy loading - **Paginação:** API de animais usa cursor-based pagination — nunca carregar mais de 20 por pedido - **Cache TanStack Query:** `staleTime: 5 * 60 * 1000` (5 minutos) para listas de animais - **Connection Pooling:** Supabase Pooler activado para PostgreSQL em produção - **Bundle Analysis:** executar `pnpm analyze` antes de cada release para identificar regressões ```typescript // Exemplo: query paginada com cursor export async function getAnimals(params: AnimalFilters & { cursor?: string }) { return prisma.animal.findMany({ where: buildWhereClause(params), take: 20, skip: params.cursor ? 1 : 0, cursor: params.cursor ? { id: params.cursor } : undefined, orderBy: [{ urgent: 'desc' }, { createdAt: 'desc' }], include: { photos: { where: { isPrimary: true }, take: 1 }, shelter: true } }); } ``` --- ## 10.7 Acessibilidade - **Imagens:** `alt` descritivo obrigatório em todas as imagens (ex: `"Bobi — Labrador macho de 2 anos no Canil de Lisboa"`) - **Formulários:** labels associados a todos os inputs, mensagens de erro ligadas via `aria-describedby` - **Foco:** ordem de foco lógica, indicadores de foco visíveis, skip links no topo - **Contraste:** mínimo 4.5:1 para texto normal, 3:1 para texto grande (testar com Lighthouse) - **Teclado:** todos os elementos interactivos acessíveis por teclado (Tab, Enter, Espaço, Esc) - **Screen readers:** testar com NVDA (Windows) ou VoiceOver (Mac/iOS) nas páginas principais