9.0 KiB
9.0 KiB
10. Orientações de Desenvolvimento
10.1 Configuração do Ambiente Local
Pré-requisitos
- Node.js 20+ — instalar via 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
# 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
// ✅ Usar tipos explícitos em funções públicas
export async function getAnimalById(id: string): Promise<Animal | null> { ... }
// ✅ 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
// ✅ 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 <AnimalProfile animal={animal} />;
}
// ✅ 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
// ✅ 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 |
// ✅ 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<UIStore>()(
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
# 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) |
# ✅ 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
// __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);
});
});
// 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@petlink.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/imagecomsizesadequado — 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 analyzeantes de cada release para identificar regressões
// 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:
altdescritivo 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