303 lines
9.0 KiB
Markdown
303 lines
9.0 KiB
Markdown
# 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<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
|
|
|
|
```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 <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
|
|
|
|
```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<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
|
|
|
|
```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
|