Em vez de criar schema novo reserva_1001, reaproveita o schema reserva_hotel existente no projeto Supabase acdvblhzzaneddlxqyst (InAudit Hotel). Migration aditiva (3 tabelas + 4 colunas Chatwoot) ja aplicada via MCP antes do plano iniciar. Adaptacoes: - Credenciais reais do projeto em .env.local - Cliente Supabase com db.schema = reserva_hotel - Tipos gerados com --schema reserva_hotel - App.tsx le tabela 'marcas' (pt-br) em vez de 'brands' - Mock do Vitest atualizado - Task 5 vira "documenta migration aplicada" (sem db push) - Task 6 usa supabase link + gen types --schema Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1450 lines
41 KiB
Markdown
1450 lines
41 KiB
Markdown
# Reserva Rede 1001 — Fase 1: Fundação (Plano de Implementação)
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Criar o novo projeto `reserva-1001` do zero como Vite + React 19 + TypeScript + Tailwind v4 + Supabase, com a paleta premium aplicada, schema novo no Supabase, e uma página de teste renderizando marcas reais do banco.
|
|
|
|
**Architecture:** App Vite novo em `/Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001/`, separado do Chatwoot. POC antigo fica como `_poc-reference/` para consulta. Reutiliza o schema `reserva_hotel` existente no projeto Supabase `acdvblhzzaneddlxqyst` (InAudit Hotel), com migração aditiva para fotos, extras e integração Chatwoot. Nenhuma integração com Chatwoot ainda — isso é Fase 2.
|
|
|
|
**Tech Stack:** Vite 6 · React 19 · TypeScript 5.6 · Tailwind v4 · shadcn/ui (cva + Radix) · Supabase JS 2 · framer-motion 12 · anime.js 4 · Vitest · ESLint · Prettier
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-04-13-reserva-1001-design.md`
|
|
|
|
**Escopo desta fase:** somente fundação. Sem fluxo de reserva, sem checkout, sem admin funcional, sem animações avançadas. A entrega é: "app compila, roda, mostra marcas vindas do Supabase com paleta premium aplicada".
|
|
|
|
---
|
|
|
|
## Pré-requisitos (já resolvidos)
|
|
|
|
**Supabase:** projeto **InAudit Hotel** (`acdvblhzzaneddlxqyst`), schema **`reserva_hotel`** reaproveitado. Migration aditiva (3 tabelas novas: `fotos_categoria`, `extras`, `reserva_extras` + colunas `chatwoot_*`) já aplicada via MCP antes de iniciar o plano.
|
|
|
|
**Credenciais:**
|
|
- URL: `https://acdvblhzzaneddlxqyst.supabase.co`
|
|
- Anon key: ver `.env.local` (criado na Task 4)
|
|
- Schema: `reserva_hotel`
|
|
|
|
**Tabelas PT-BR disponíveis:**
|
|
`marcas`, `unidades`, `suites`, `suites_unidades`, `precos`, `contas_pagamento`, `reservas`, `fotos_categoria`, `extras`, `reserva_extras`, `vw_precos_completa`, `vw_reservas_completa`.
|
|
|
|
**⚠️ Ação manual do usuário necessária:** no Supabase Dashboard → Project Settings → API → **Data API settings** → **Exposed schemas**, adicionar `reserva_hotel` à lista (se ainda não estiver). Sem isso, o cliente JS não consegue ler as tabelas.
|
|
|
|
**Ferramental local (já validado):** Node v20, pnpm 9.14, supabase CLI 2.48.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
Estrutura final ao fim da Fase 1:
|
|
|
|
```
|
|
reserva-1001/
|
|
├── .env.local # credenciais (gitignored)
|
|
├── .env.local.example # template
|
|
├── .gitignore
|
|
├── .prettierrc
|
|
├── README.md
|
|
├── eslint.config.js
|
|
├── index.html # sem importmap esm.sh
|
|
├── package.json
|
|
├── pnpm-lock.yaml
|
|
├── tsconfig.json
|
|
├── tsconfig.app.json
|
|
├── tsconfig.node.json
|
|
├── vite.config.ts
|
|
├── _poc-reference/ # POC antigo, só pra consulta
|
|
│ └── hotel-1001-noites-prime---reserva/
|
|
├── supabase/
|
|
│ ├── config.toml
|
|
│ └── migrations/
|
|
│ └── 20260413000001_reserva_1001_additive_schema.sql
|
|
└── src/
|
|
├── main.tsx # entrypoint
|
|
├── App.tsx # shell simples exibindo marcas
|
|
├── index.css # tokens Tailwind v4 + paleta premium
|
|
├── lib/
|
|
│ ├── supabase.ts # cliente Supabase
|
|
│ └── utils.ts # cn() helper (clsx + tailwind-merge)
|
|
├── components/
|
|
│ ├── FormField.tsx # migrado do POC, adaptado à paleta
|
|
│ ├── SelectField.tsx # migrado do POC, adaptado à paleta
|
|
│ └── ui/
|
|
│ └── button.tsx # shadcn/ui Button base
|
|
├── types/
|
|
│ └── database.ts # gerado via supabase gen types
|
|
└── __tests__/
|
|
└── App.test.tsx # smoke test Vitest
|
|
```
|
|
|
|
---
|
|
|
|
## Task 1: Preparar o diretório (mover POC pra referência, iniciar git)
|
|
|
|
**Files:**
|
|
- Modify: `reserva-1001/` (estrutura)
|
|
- Create: `reserva-1001/.gitignore`
|
|
|
|
- [ ] **Step 1: Mover o POC pra subpasta de referência**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
mkdir -p _poc-reference
|
|
mv hotel-1001-noites-prime---reserva _poc-reference/
|
|
ls -la
|
|
```
|
|
|
|
Expected: agora `reserva-1001/` contém só `_poc-reference/`.
|
|
|
|
- [ ] **Step 2: Inicializar git e criar .gitignore**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
git init
|
|
```
|
|
|
|
Create `.gitignore`:
|
|
```
|
|
# Dependencies
|
|
node_modules/
|
|
.pnp
|
|
.pnp.js
|
|
|
|
# Build
|
|
dist/
|
|
build/
|
|
*.tsbuildinfo
|
|
|
|
# Env
|
|
.env
|
|
.env.local
|
|
.env.*.local
|
|
|
|
# Editor
|
|
.vscode/
|
|
.idea/
|
|
*.swp
|
|
.DS_Store
|
|
|
|
# Logs
|
|
npm-debug.log*
|
|
pnpm-debug.log*
|
|
yarn-debug.log*
|
|
|
|
# Test coverage
|
|
coverage/
|
|
|
|
# Supabase
|
|
supabase/.branches
|
|
supabase/.temp
|
|
```
|
|
|
|
- [ ] **Step 3: Primeiro commit (só o POC de referência e gitignore)**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
git add .gitignore _poc-reference/
|
|
git commit -m "chore: inicializa repo com POC como referencia"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Scaffold do projeto Vite + React + TypeScript
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/package.json`
|
|
- Create: `reserva-1001/vite.config.ts`
|
|
- Create: `reserva-1001/tsconfig.json`
|
|
- Create: `reserva-1001/tsconfig.node.json`
|
|
- Create: `reserva-1001/index.html`
|
|
- Create: `reserva-1001/src/main.tsx`
|
|
- Create: `reserva-1001/src/App.tsx`
|
|
|
|
- [ ] **Step 1: Criar package.json manualmente (evita o prompt interativo do create-vite)**
|
|
|
|
Create `reserva-1001/package.json`:
|
|
```json
|
|
{
|
|
"name": "reserva-1001",
|
|
"private": true,
|
|
"version": "0.1.0",
|
|
"type": "module",
|
|
"scripts": {
|
|
"dev": "vite",
|
|
"build": "tsc -b && vite build",
|
|
"preview": "vite preview",
|
|
"lint": "eslint .",
|
|
"format": "prettier --write .",
|
|
"test": "vitest run",
|
|
"test:watch": "vitest",
|
|
"typecheck": "tsc --noEmit",
|
|
"supabase:types": "supabase gen types typescript --local > src/types/database.ts"
|
|
},
|
|
"dependencies": {
|
|
"@radix-ui/react-slot": "^1.1.0",
|
|
"@supabase/supabase-js": "^2.45.0",
|
|
"animejs": "^4.0.2",
|
|
"class-variance-authority": "^0.7.0",
|
|
"clsx": "^2.1.1",
|
|
"lucide-react": "^0.454.0",
|
|
"motion": "^12.4.0",
|
|
"react": "^19.1.0",
|
|
"react-dom": "^19.1.0",
|
|
"tailwind-merge": "^2.5.0"
|
|
},
|
|
"devDependencies": {
|
|
"@tailwindcss/vite": "^4.0.0",
|
|
"@testing-library/jest-dom": "^6.5.0",
|
|
"@testing-library/react": "^16.0.0",
|
|
"@types/node": "^22.9.0",
|
|
"@types/react": "^19.0.0",
|
|
"@types/react-dom": "^19.0.0",
|
|
"@vitejs/plugin-react": "^5.0.0",
|
|
"eslint": "^9.14.0",
|
|
"eslint-config-prettier": "^9.1.0",
|
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
"eslint-plugin-react-refresh": "^0.4.14",
|
|
"globals": "^15.12.0",
|
|
"jsdom": "^25.0.1",
|
|
"prettier": "^3.3.3",
|
|
"tailwindcss": "^4.0.0",
|
|
"typescript": "^5.6.3",
|
|
"typescript-eslint": "^8.14.0",
|
|
"vite": "^6.0.0",
|
|
"vitest": "^2.1.5"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Criar tsconfig.json**
|
|
|
|
Create `reserva-1001/tsconfig.json`:
|
|
```json
|
|
{
|
|
"files": [],
|
|
"references": [
|
|
{ "path": "./tsconfig.app.json" },
|
|
{ "path": "./tsconfig.node.json" }
|
|
],
|
|
"compilerOptions": {
|
|
"skipLibCheck": true
|
|
}
|
|
}
|
|
```
|
|
|
|
Create `reserva-1001/tsconfig.app.json`:
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"useDefineForClassFields": true,
|
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
"module": "ESNext",
|
|
"skipLibCheck": true,
|
|
"moduleResolution": "bundler",
|
|
"allowImportingTsExtensions": true,
|
|
"isolatedModules": true,
|
|
"moduleDetection": "force",
|
|
"noEmit": true,
|
|
"jsx": "react-jsx",
|
|
"strict": true,
|
|
"noUnusedLocals": true,
|
|
"noUnusedParameters": true,
|
|
"noFallthroughCasesInSwitch": true,
|
|
"baseUrl": ".",
|
|
"paths": {
|
|
"@/*": ["./src/*"]
|
|
},
|
|
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
|
},
|
|
"include": ["src"]
|
|
}
|
|
```
|
|
|
|
Create `reserva-1001/tsconfig.node.json`:
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"lib": ["ES2023"],
|
|
"module": "ESNext",
|
|
"skipLibCheck": true,
|
|
"moduleResolution": "bundler",
|
|
"allowSyntheticDefaultImports": true,
|
|
"strict": true,
|
|
"noEmit": true,
|
|
"types": ["node"]
|
|
},
|
|
"include": ["vite.config.ts"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Criar vite.config.ts com alias @ e Tailwind v4 plugin**
|
|
|
|
Create `reserva-1001/vite.config.ts`:
|
|
```ts
|
|
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import path from 'node:path'
|
|
|
|
export default defineConfig({
|
|
plugins: [react(), tailwindcss()],
|
|
resolve: {
|
|
alias: {
|
|
'@': path.resolve(__dirname, './src'),
|
|
},
|
|
},
|
|
test: {
|
|
environment: 'jsdom',
|
|
globals: true,
|
|
setupFiles: './src/__tests__/setup.ts',
|
|
},
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 4: Criar index.html limpo (sem importmap esm.sh)**
|
|
|
|
Create `reserva-1001/index.html`:
|
|
```html
|
|
<!doctype html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Reserva Rede 1001</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="module" src="/src/main.tsx"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
- [ ] **Step 5: Criar src/main.tsx**
|
|
|
|
Create `reserva-1001/src/main.tsx`:
|
|
```tsx
|
|
import { StrictMode } from 'react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import './index.css'
|
|
import App from './App'
|
|
|
|
createRoot(document.getElementById('root')!).render(
|
|
<StrictMode>
|
|
<App />
|
|
</StrictMode>
|
|
)
|
|
```
|
|
|
|
- [ ] **Step 6: Criar src/App.tsx placeholder**
|
|
|
|
Create `reserva-1001/src/App.tsx`:
|
|
```tsx
|
|
export default function App() {
|
|
return (
|
|
<div className="min-h-screen bg-obsidian text-ivory flex items-center justify-center">
|
|
<h1 className="font-serif text-5xl text-champagne">Reserva Rede 1001</h1>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 7: Instalar dependências**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
pnpm install
|
|
```
|
|
|
|
Expected: instala sem erros. Gera `pnpm-lock.yaml`.
|
|
|
|
- [ ] **Step 8: Commit**
|
|
|
|
```bash
|
|
git add package.json pnpm-lock.yaml tsconfig*.json vite.config.ts index.html src/main.tsx src/App.tsx
|
|
git commit -m "feat: scaffold inicial do projeto vite + react + typescript"
|
|
```
|
|
|
|
Nota: ainda vai dar erro no build porque `index.css` não existe — resolvido na Task 3.
|
|
|
|
---
|
|
|
|
## Task 3: Configurar Tailwind v4 com paleta premium
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/src/index.css`
|
|
|
|
- [ ] **Step 1: Criar src/index.css com tokens do Tailwind v4 e paleta premium**
|
|
|
|
Create `reserva-1001/src/index.css`:
|
|
```css
|
|
@import 'tailwindcss';
|
|
|
|
@theme {
|
|
/* Paleta premium Reserva 1001 */
|
|
--color-obsidian: #0b0d12;
|
|
--color-midnight: #0f1a2e;
|
|
--color-champagne: #c9a961;
|
|
--color-rose-gold: #e8b4a0;
|
|
--color-ivory: #f5f1e8;
|
|
--color-slate: #6b7280;
|
|
--color-emerald: #10b981;
|
|
--color-ruby: #e11d48;
|
|
|
|
/* Tipografia */
|
|
--font-sans: 'Inter', system-ui, sans-serif;
|
|
--font-serif: 'Fraunces', Georgia, serif;
|
|
|
|
/* Raios */
|
|
--radius-lg: 0.75rem;
|
|
--radius-xl: 1rem;
|
|
--radius-2xl: 1.5rem;
|
|
}
|
|
|
|
@layer base {
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
margin: 0;
|
|
font-family: var(--font-sans);
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(180deg, var(--color-obsidian) 0%, var(--color-midnight) 100%);
|
|
color: var(--color-ivory);
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
h3 {
|
|
font-family: var(--font-serif);
|
|
letter-spacing: -0.02em;
|
|
}
|
|
}
|
|
|
|
@layer utilities {
|
|
.text-gradient-gold {
|
|
background: linear-gradient(135deg, var(--color-champagne), var(--color-rose-gold));
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.glow-champagne {
|
|
box-shadow: 0 0 30px rgba(201, 169, 97, 0.4);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Rodar o dev server pra validar visualmente**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
pnpm dev
|
|
```
|
|
|
|
Expected: abre em `http://localhost:5173`, mostra "Reserva Rede 1001" em Fraunces dourado (champagne) sobre fundo escuro com gradiente obsidian→midnight.
|
|
|
|
Pare o server com `Ctrl+C` depois de validar.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/index.css
|
|
git commit -m "feat: configura tailwind v4 com paleta premium (obsidian/champagne/rose-gold)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Setup do cliente Supabase + variáveis de ambiente
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/.env.local`
|
|
- Create: `reserva-1001/.env.local.example`
|
|
- Create: `reserva-1001/src/lib/supabase.ts`
|
|
- Create: `reserva-1001/src/lib/utils.ts`
|
|
- Create: `reserva-1001/src/types/database.ts` (placeholder inicial)
|
|
|
|
- [ ] **Step 1: Criar .env.local.example (template commitado)**
|
|
|
|
Create `reserva-1001/.env.local.example`:
|
|
```
|
|
# Supabase — projeto InAudit Hotel
|
|
VITE_SUPABASE_URL=https://acdvblhzzaneddlxqyst.supabase.co
|
|
VITE_SUPABASE_ANON_KEY=
|
|
VITE_SUPABASE_SCHEMA=reserva_hotel
|
|
|
|
# Chatwoot — token de integração (Fase 2)
|
|
VITE_CHATWOOT_API_URL=https://chatwoot.fazer.ai
|
|
VITE_CHATWOOT_API_TOKEN=
|
|
```
|
|
|
|
- [ ] **Step 2: Criar .env.local com as credenciais reais do projeto InAudit Hotel**
|
|
|
|
Create `reserva-1001/.env.local`:
|
|
```
|
|
VITE_SUPABASE_URL=https://acdvblhzzaneddlxqyst.supabase.co
|
|
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFjZHZibGh6emFuZWRkbHhxeXN0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA4MTMyNzcsImV4cCI6MjA2NjM4OTI3N30.wwmr0TMYuds-QLrTESR4kx34umssseDyEsPjVRt02qI
|
|
VITE_SUPABASE_SCHEMA=reserva_hotel
|
|
VITE_CHATWOOT_API_URL=https://chatwoot.fazer.ai
|
|
VITE_CHATWOOT_API_TOKEN=
|
|
```
|
|
|
|
**IMPORTANTE:** este arquivo está no `.gitignore` e NÃO vai pro git.
|
|
|
|
- [ ] **Step 3: Criar src/lib/utils.ts (cn helper)**
|
|
|
|
Create `reserva-1001/src/lib/utils.ts`:
|
|
```ts
|
|
import { clsx, type ClassValue } from 'clsx'
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Criar src/types/database.ts inicial (vazio — será regenerado na Task 6)**
|
|
|
|
Create `reserva-1001/src/types/database.ts`:
|
|
```ts
|
|
// Este arquivo é gerado automaticamente via `pnpm supabase:types`.
|
|
// Não edite à mão. Regenerar após cada migration.
|
|
|
|
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
|
|
|
export type Database = {
|
|
public: {
|
|
Tables: Record<string, never>
|
|
Views: Record<string, never>
|
|
Functions: Record<string, never>
|
|
Enums: Record<string, never>
|
|
CompositeTypes: Record<string, never>
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Criar src/lib/supabase.ts**
|
|
|
|
Create `reserva-1001/src/lib/supabase.ts`:
|
|
```ts
|
|
import { createClient } from '@supabase/supabase-js'
|
|
import type { Database } from '@/types/database'
|
|
|
|
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
|
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
|
const supabaseSchema = import.meta.env.VITE_SUPABASE_SCHEMA ?? 'reserva_hotel'
|
|
|
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
throw new Error(
|
|
'Variáveis VITE_SUPABASE_URL e VITE_SUPABASE_ANON_KEY não estão definidas. Copie .env.local.example para .env.local e preencha.'
|
|
)
|
|
}
|
|
|
|
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
|
db: { schema: supabaseSchema as 'reserva_hotel' },
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 6: Validar que o typecheck passa**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
pnpm typecheck
|
|
```
|
|
|
|
Expected: sem erros.
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
git add .env.local.example src/lib/supabase.ts src/lib/utils.ts src/types/database.ts
|
|
git commit -m "feat: configura cliente supabase com variaveis de ambiente"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Documentar a migration aditiva (schema já aplicado via MCP)
|
|
|
|
**Contexto:** A migration aditiva ao schema `reserva_hotel` já foi aplicada no projeto Supabase antes do plano começar (via MCP). Esta task **só cria o arquivo SQL** como fonte de verdade/histórico no repositório — **não roda `supabase db push`**.
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/supabase/config.toml` (via CLI)
|
|
- Create: `reserva-1001/supabase/migrations/20260413000001_reserva_1001_additive_schema.sql`
|
|
|
|
- [ ] **Step 1: Inicializar Supabase CLI no projeto**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
supabase init
|
|
```
|
|
|
|
Expected: cria `supabase/config.toml` e `supabase/` folder. Se perguntar sobre VSCode settings, responda N.
|
|
|
|
- [ ] **Step 2: Criar o arquivo de migration (DDL da migration já aplicada)**
|
|
|
|
Create `reserva-1001/supabase/migrations/20260413000001_reserva_1001_additive_schema.sql`:
|
|
```sql
|
|
-- Reserva Rede 1001 — adições ao schema reserva_hotel existente
|
|
-- Já aplicada via MCP. Este arquivo é a fonte de verdade histórica.
|
|
-- Zero alteração destrutiva. Tudo aditivo.
|
|
|
|
-- === 1. Fotos por categoria de suíte (linked por unidade + nome da categoria) ===
|
|
create table if not exists reserva_hotel.fotos_categoria (
|
|
id uuid primary key default gen_random_uuid(),
|
|
id_unidade uuid not null references reserva_hotel.unidades(id) on delete cascade,
|
|
categoria text not null,
|
|
url_foto text not null,
|
|
alt text,
|
|
ordem int not null default 0,
|
|
ativa boolean not null default true,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now()
|
|
);
|
|
|
|
create index if not exists idx_fotos_categoria_unidade on reserva_hotel.fotos_categoria(id_unidade);
|
|
create index if not exists idx_fotos_categoria_lookup on reserva_hotel.fotos_categoria(id_unidade, categoria);
|
|
|
|
-- === 2. Extras (adicionais por marca) ===
|
|
create table if not exists reserva_hotel.extras (
|
|
id uuid primary key default gen_random_uuid(),
|
|
id_marca uuid not null references reserva_hotel.marcas(id) on delete cascade,
|
|
titulo text not null,
|
|
descricao text,
|
|
preco numeric not null check (preco >= 0),
|
|
imagem_url text,
|
|
ativo boolean not null default true,
|
|
ordem int not null default 0,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now()
|
|
);
|
|
|
|
create index if not exists idx_extras_marca on reserva_hotel.extras(id_marca);
|
|
|
|
-- === 3. Junction reserva ↔ extras ===
|
|
create table if not exists reserva_hotel.reserva_extras (
|
|
id_reserva uuid not null references reserva_hotel.reservas(id) on delete cascade,
|
|
id_extra uuid not null references reserva_hotel.extras(id),
|
|
preco numeric not null check (preco >= 0),
|
|
created_at timestamptz not null default now(),
|
|
primary key (id_reserva, id_extra)
|
|
);
|
|
|
|
-- === 4. Integração Chatwoot em unidades ===
|
|
alter table reserva_hotel.unidades
|
|
add column if not exists chatwoot_unit_id bigint;
|
|
|
|
create index if not exists idx_unidades_chatwoot_unit on reserva_hotel.unidades(chatwoot_unit_id);
|
|
|
|
-- === 5. Integração Chatwoot em reservas ===
|
|
alter table reserva_hotel.reservas
|
|
add column if not exists chatwoot_contact_id bigint;
|
|
|
|
alter table reserva_hotel.reservas
|
|
add column if not exists chatwoot_conversation_id bigint;
|
|
|
|
alter table reserva_hotel.reservas
|
|
add column if not exists chatwoot_pix_charge_id bigint;
|
|
|
|
create index if not exists idx_reservas_chatwoot_conversation on reserva_hotel.reservas(chatwoot_conversation_id);
|
|
create index if not exists idx_reservas_txid_pix on reserva_hotel.reservas(txid_pix);
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
Nota: não há seed.sql neste plano — o schema `reserva_hotel` já contém 1 marca de seed e o usuário vai cadastrar o resto via admin (Fase 4). Também não rodamos `supabase db push` porque a migration já foi aplicada via MCP antes do plano.
|
|
|
|
```bash
|
|
git add supabase/config.toml supabase/migrations/
|
|
git commit -m "feat: documenta migration aditiva do schema reserva_hotel"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Gerar types TypeScript do schema reserva_hotel
|
|
|
|
**Files:**
|
|
- Modify: `reserva-1001/src/types/database.ts`
|
|
|
|
- [ ] **Step 1: Linkar CLI ao projeto InAudit Hotel**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
supabase link --project-ref acdvblhzzaneddlxqyst
|
|
```
|
|
|
|
Vai pedir a senha do banco. Use a senha do projeto. Se não souber, peça pro usuário.
|
|
|
|
- [ ] **Step 2: Gerar os tipos do schema reserva_hotel**
|
|
|
|
```bash
|
|
supabase gen types typescript --linked --schema reserva_hotel > src/types/database.ts
|
|
```
|
|
|
|
Expected: `src/types/database.ts` contém `Database` tipado com as tabelas `marcas`, `unidades`, `suites`, `precos`, `reservas`, `fotos_categoria`, `extras`, `reserva_extras`, `contas_pagamento` sob o namespace `reserva_hotel`.
|
|
|
|
**Nota:** o cliente Supabase configurado em Task 4 já aponta para o schema `reserva_hotel` via `db: { schema }`. Logo, ao fazer `supabase.from('marcas')`, ele automaticamente resolve `reserva_hotel.marcas`.
|
|
|
|
- [ ] **Step 3: Validar typecheck**
|
|
|
|
```bash
|
|
pnpm typecheck
|
|
```
|
|
|
|
Expected: passa sem erros.
|
|
|
|
- [ ] **Step 4: Atualizar o script `supabase:types` em package.json**
|
|
|
|
Edit `reserva-1001/package.json`, campo `scripts`:
|
|
|
|
De:
|
|
```json
|
|
"supabase:types": "supabase gen types typescript --local > src/types/database.ts"
|
|
```
|
|
|
|
Para:
|
|
```json
|
|
"supabase:types": "supabase gen types typescript --linked --schema reserva_hotel > src/types/database.ts"
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/types/database.ts package.json
|
|
git commit -m "feat: gera tipos typescript do schema reserva_hotel"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Renderizar marcas reais no App.tsx
|
|
|
|
**Files:**
|
|
- Modify: `reserva-1001/src/App.tsx`
|
|
|
|
- [ ] **Step 1: Reescrever App.tsx para buscar e exibir marcas do Supabase (schema reserva_hotel, tabela `marcas`)**
|
|
|
|
Replace `reserva-1001/src/App.tsx`:
|
|
```tsx
|
|
import { useEffect, useState } from 'react'
|
|
import { supabase } from '@/lib/supabase'
|
|
import type { Database } from '@/types/database'
|
|
|
|
type Marca = Database['reserva_hotel']['Tables']['marcas']['Row']
|
|
|
|
export default function App() {
|
|
const [marcas, setMarcas] = useState<Marca[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
async function loadMarcas() {
|
|
const { data, error } = await supabase
|
|
.from('marcas')
|
|
.select('*')
|
|
.eq('ativa', true)
|
|
.order('nome', { ascending: true })
|
|
|
|
if (error) {
|
|
setError(error.message)
|
|
} else {
|
|
setMarcas(data ?? [])
|
|
}
|
|
setLoading(false)
|
|
}
|
|
loadMarcas()
|
|
}, [])
|
|
|
|
return (
|
|
<main className="min-h-screen flex flex-col items-center justify-center px-6 py-12">
|
|
<header className="text-center mb-12">
|
|
<p className="font-sans text-sm uppercase tracking-[0.3em] text-rose-gold mb-4">
|
|
Experiência exclusiva
|
|
</p>
|
|
<h1 className="font-serif text-6xl md:text-7xl text-gradient-gold mb-4">
|
|
Reserva Rede 1001
|
|
</h1>
|
|
<p className="font-sans text-slate text-lg">Escolha uma das nossas marcas para começar</p>
|
|
</header>
|
|
|
|
{loading && <p className="text-slate">Carregando marcas...</p>}
|
|
|
|
{error && (
|
|
<div className="rounded-xl border border-ruby/40 bg-ruby/10 p-4 text-ivory">
|
|
Erro ao carregar: {error}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && marcas.length === 0 && (
|
|
<p className="text-slate">Nenhuma marca cadastrada ainda.</p>
|
|
)}
|
|
|
|
{!loading && !error && marcas.length > 0 && (
|
|
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full max-w-2xl">
|
|
{marcas.map((marca) => (
|
|
<li
|
|
key={marca.id}
|
|
className="rounded-xl border border-champagne/20 bg-midnight/60 backdrop-blur p-6 text-center transition hover:border-champagne hover:glow-champagne"
|
|
>
|
|
<h2 className="font-serif text-2xl text-champagne">{marca.nome}</h2>
|
|
{marca.descricao && <p className="text-slate text-sm mt-1">{marca.descricao}</p>}
|
|
{marca.categorias && marca.categorias.length > 0 && (
|
|
<p className="text-rose-gold text-xs mt-2 uppercase tracking-widest">
|
|
{marca.categorias.join(' · ')}
|
|
</p>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
<footer className="mt-16 text-slate text-xs uppercase tracking-widest">
|
|
© 2026 Reserva Rede 1001 · Fase 1 · Fundação
|
|
</footer>
|
|
</main>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Validar visualmente no browser**
|
|
|
|
```bash
|
|
pnpm dev
|
|
```
|
|
|
|
Abra `http://localhost:5173`. Expected: vê as marcas existentes no schema `reserva_hotel` (hoje 1 marca: "Hotel 1001 Noites") em cards escuros com borda dourada sutil, título "Reserva Rede 1001" em gradiente dourado. Hover nos cards faz a borda brilhar.
|
|
|
|
Pare com `Ctrl+C`.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/App.tsx
|
|
git commit -m "feat: renderiza marcas reais do supabase na pagina inicial"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Migrar FormField e SelectField do POC com paleta premium
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/src/components/FormField.tsx`
|
|
- Create: `reserva-1001/src/components/SelectField.tsx`
|
|
|
|
- [ ] **Step 1: Ler os componentes do POC como referência**
|
|
|
|
```bash
|
|
cat _poc-reference/hotel-1001-noites-prime---reserva/components/FormField.tsx
|
|
cat _poc-reference/hotel-1001-noites-prime---reserva/components/SelectField.tsx
|
|
```
|
|
|
|
- [ ] **Step 2: Criar FormField.tsx adaptado à nova paleta**
|
|
|
|
Create `reserva-1001/src/components/FormField.tsx`:
|
|
```tsx
|
|
import { forwardRef, type InputHTMLAttributes } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface FormFieldProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
label: string
|
|
error?: string
|
|
required?: boolean
|
|
}
|
|
|
|
export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
|
({ label, error, required, className, id, ...props }, ref) => {
|
|
const inputId = id ?? `field-${label.toLowerCase().replace(/\s+/g, '-')}`
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<label
|
|
htmlFor={inputId}
|
|
className="font-sans text-xs uppercase tracking-widest text-champagne"
|
|
>
|
|
{label}
|
|
{required && <span className="text-rose-gold ml-1">*</span>}
|
|
</label>
|
|
<input
|
|
ref={ref}
|
|
id={inputId}
|
|
aria-invalid={error ? 'true' : 'false'}
|
|
className={cn(
|
|
'w-full rounded-lg border border-champagne/30 bg-midnight/60 px-4 py-3',
|
|
'font-sans text-ivory placeholder:text-slate',
|
|
'focus:outline-none focus:border-champagne focus:glow-champagne',
|
|
'transition-all duration-200',
|
|
error && 'border-ruby focus:border-ruby',
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
{error && <span className="text-ruby text-xs font-sans">{error}</span>}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
|
|
FormField.displayName = 'FormField'
|
|
```
|
|
|
|
- [ ] **Step 3: Criar SelectField.tsx adaptado à nova paleta**
|
|
|
|
Create `reserva-1001/src/components/SelectField.tsx`:
|
|
```tsx
|
|
import { forwardRef, type SelectHTMLAttributes } from 'react'
|
|
import { ChevronDown } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface Option {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
interface SelectFieldProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
|
|
label: string
|
|
options: Option[]
|
|
placeholder?: string
|
|
error?: string
|
|
required?: boolean
|
|
}
|
|
|
|
export const SelectField = forwardRef<HTMLSelectElement, SelectFieldProps>(
|
|
(
|
|
{ label, options, placeholder = 'Selecione...', error, required, className, id, ...props },
|
|
ref
|
|
) => {
|
|
const selectId = id ?? `select-${label.toLowerCase().replace(/\s+/g, '-')}`
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<label
|
|
htmlFor={selectId}
|
|
className="font-sans text-xs uppercase tracking-widest text-champagne"
|
|
>
|
|
{label}
|
|
{required && <span className="text-rose-gold ml-1">*</span>}
|
|
</label>
|
|
<div className="relative">
|
|
<select
|
|
ref={ref}
|
|
id={selectId}
|
|
aria-invalid={error ? 'true' : 'false'}
|
|
className={cn(
|
|
'w-full appearance-none rounded-lg border border-champagne/30 bg-midnight/60 px-4 py-3 pr-10',
|
|
'font-sans text-ivory',
|
|
'focus:outline-none focus:border-champagne focus:glow-champagne',
|
|
'transition-all duration-200',
|
|
error && 'border-ruby focus:border-ruby',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<option value="" disabled>
|
|
{placeholder}
|
|
</option>
|
|
{options.map((opt) => (
|
|
<option key={opt.value} value={opt.value} className="bg-midnight text-ivory">
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<ChevronDown
|
|
aria-hidden
|
|
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-champagne"
|
|
/>
|
|
</div>
|
|
{error && <span className="text-ruby text-xs font-sans">{error}</span>}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
|
|
SelectField.displayName = 'SelectField'
|
|
```
|
|
|
|
- [ ] **Step 4: Validar typecheck**
|
|
|
|
```bash
|
|
pnpm typecheck
|
|
```
|
|
|
|
Expected: passa.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/components/FormField.tsx src/components/SelectField.tsx
|
|
git commit -m "feat: migra FormField e SelectField com paleta premium"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9: Criar componente Button base (shadcn/ui style)
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/src/components/ui/button.tsx`
|
|
|
|
- [ ] **Step 1: Criar Button com cva e variantes premium**
|
|
|
|
Create `reserva-1001/src/components/ui/button.tsx`:
|
|
```tsx
|
|
import { forwardRef, type ButtonHTMLAttributes } from 'react'
|
|
import { Slot } from '@radix-ui/react-slot'
|
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const buttonVariants = cva(
|
|
'inline-flex items-center justify-center gap-2 rounded-lg font-sans font-semibold transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-champagne/60',
|
|
{
|
|
variants: {
|
|
variant: {
|
|
primary:
|
|
'bg-gradient-to-r from-champagne to-rose-gold text-obsidian hover:glow-champagne hover:scale-[1.02]',
|
|
secondary:
|
|
'border border-champagne/30 bg-midnight/60 text-ivory hover:border-champagne hover:glow-champagne',
|
|
ghost: 'text-champagne hover:bg-champagne/10',
|
|
destructive: 'bg-ruby text-ivory hover:bg-ruby/90',
|
|
},
|
|
size: {
|
|
sm: 'h-9 px-4 text-sm',
|
|
md: 'h-11 px-6 text-base',
|
|
lg: 'h-14 px-8 text-lg',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: 'primary',
|
|
size: 'md',
|
|
},
|
|
}
|
|
)
|
|
|
|
interface ButtonProps
|
|
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
VariantProps<typeof buttonVariants> {
|
|
asChild?: boolean
|
|
}
|
|
|
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
const Comp = asChild ? Slot : 'button'
|
|
return (
|
|
<Comp className={cn(buttonVariants({ variant, size }), className)} ref={ref} {...props} />
|
|
)
|
|
}
|
|
)
|
|
|
|
Button.displayName = 'Button'
|
|
|
|
export { buttonVariants }
|
|
```
|
|
|
|
- [ ] **Step 2: Validar typecheck**
|
|
|
|
```bash
|
|
pnpm typecheck
|
|
```
|
|
|
|
Expected: passa.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/components/ui/button.tsx
|
|
git commit -m "feat: componente Button base com variantes premium"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 10: Configurar Vitest + setup file + smoke test
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/src/__tests__/setup.ts`
|
|
- Create: `reserva-1001/src/__tests__/App.test.tsx`
|
|
|
|
- [ ] **Step 1: Criar setup file do Vitest**
|
|
|
|
Create `reserva-1001/src/__tests__/setup.ts`:
|
|
```ts
|
|
import '@testing-library/jest-dom/vitest'
|
|
import { cleanup } from '@testing-library/react'
|
|
import { afterEach, vi } from 'vitest'
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
})
|
|
|
|
// Mock do Supabase client — imita from('marcas').select('*').eq('ativa', true).order('nome')
|
|
vi.mock('@/lib/supabase', () => {
|
|
const mockMarcas = [
|
|
{
|
|
id: '3fac5ed4-100f-4c0a-82ce-06110758b9c9',
|
|
nome: 'Hotel 1001 Noites',
|
|
categorias: ['Standard', 'Superior', 'Luxo'],
|
|
permanencias: ['3hrs', '6hrs', 'Pernoite'],
|
|
descricao: null,
|
|
ativa: true,
|
|
created_at: '2026-04-13T00:00:00Z',
|
|
updated_at: '2026-04-13T00:00:00Z',
|
|
},
|
|
{
|
|
id: '11111111-1111-1111-1111-111111111111',
|
|
nome: 'Dolce Amore',
|
|
categorias: ['Suite Master', 'Apartamento'],
|
|
permanencias: ['3hrs', '4hrs'],
|
|
descricao: null,
|
|
ativa: true,
|
|
created_at: '2026-04-13T00:00:00Z',
|
|
updated_at: '2026-04-13T00:00:00Z',
|
|
},
|
|
]
|
|
|
|
const orderFn = vi.fn(() => Promise.resolve({ data: mockMarcas, error: null }))
|
|
const eqFn = vi.fn(() => ({ order: orderFn }))
|
|
const selectFn = vi.fn(() => ({ eq: eqFn, order: orderFn }))
|
|
const fromFn = vi.fn(() => ({ select: selectFn }))
|
|
|
|
return {
|
|
supabase: { from: fromFn },
|
|
}
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Escrever smoke test do App.tsx (TDD: test primeiro)**
|
|
|
|
Create `reserva-1001/src/__tests__/App.test.tsx`:
|
|
```tsx
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import { describe, it, expect } from 'vitest'
|
|
import App from '@/App'
|
|
|
|
describe('App', () => {
|
|
it('renderiza título premium', () => {
|
|
render(<App />)
|
|
expect(screen.getByRole('heading', { name: /reserva rede 1001/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it('exibe marcas retornadas do supabase após carregar', async () => {
|
|
render(<App />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Hotel 1001 Noites')).toBeInTheDocument()
|
|
expect(screen.getByText('Dolce Amore')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('não mostra estado de loading após fetch completar', async () => {
|
|
render(<App />)
|
|
await waitFor(() => {
|
|
expect(screen.queryByText(/carregando marcas/i)).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 3: Rodar os testes**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
pnpm test
|
|
```
|
|
|
|
Expected: 3 testes passando. Se não, o App.tsx já foi escrito na Task 7 de forma a passar — provavelmente só um ajuste no mock.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/__tests__/
|
|
git commit -m "test: smoke test do App com mock do supabase"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 11: Configurar ESLint + Prettier
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/eslint.config.js`
|
|
- Create: `reserva-1001/.prettierrc`
|
|
- Create: `reserva-1001/.prettierignore`
|
|
|
|
- [ ] **Step 1: Criar eslint.config.js (flat config)**
|
|
|
|
Create `reserva-1001/eslint.config.js`:
|
|
```js
|
|
import js from '@eslint/js'
|
|
import globals from 'globals'
|
|
import reactHooks from 'eslint-plugin-react-hooks'
|
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
import tseslint from 'typescript-eslint'
|
|
import prettier from 'eslint-config-prettier'
|
|
|
|
export default tseslint.config(
|
|
{ ignores: ['dist', '_poc-reference', 'supabase'] },
|
|
{
|
|
extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
|
|
files: ['**/*.{ts,tsx}'],
|
|
languageOptions: {
|
|
ecmaVersion: 2022,
|
|
globals: globals.browser,
|
|
},
|
|
plugins: {
|
|
'react-hooks': reactHooks,
|
|
'react-refresh': reactRefresh,
|
|
},
|
|
rules: {
|
|
...reactHooks.configs.recommended.rules,
|
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
},
|
|
}
|
|
)
|
|
```
|
|
|
|
- [ ] **Step 2: Instalar dependências faltantes do ESLint**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
pnpm add -D @eslint/js
|
|
```
|
|
|
|
- [ ] **Step 3: Criar .prettierrc**
|
|
|
|
Create `reserva-1001/.prettierrc`:
|
|
```json
|
|
{
|
|
"semi": false,
|
|
"singleQuote": true,
|
|
"trailingComma": "es5",
|
|
"printWidth": 100,
|
|
"tabWidth": 2,
|
|
"arrowParens": "always",
|
|
"endOfLine": "lf"
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Criar .prettierignore**
|
|
|
|
Create `reserva-1001/.prettierignore`:
|
|
```
|
|
dist
|
|
node_modules
|
|
_poc-reference
|
|
supabase/.branches
|
|
supabase/.temp
|
|
pnpm-lock.yaml
|
|
```
|
|
|
|
- [ ] **Step 5: Rodar lint e format**
|
|
|
|
```bash
|
|
pnpm lint
|
|
pnpm format
|
|
```
|
|
|
|
Expected: lint passa sem erros. Prettier formata os arquivos.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add eslint.config.js .prettierrc .prettierignore package.json pnpm-lock.yaml src/
|
|
git commit -m "chore: configura eslint flat config + prettier"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 12: Criar README do projeto
|
|
|
|
**Files:**
|
|
- Create: `reserva-1001/README.md`
|
|
|
|
- [ ] **Step 1: Escrever README**
|
|
|
|
Create `reserva-1001/README.md`:
|
|
```markdown
|
|
# Reserva Rede 1001
|
|
|
|
Página pública de reserva para as marcas do Grupo Nova (Hotel 1001 Noites, Prime, Express, Dolce Amore).
|
|
|
|
**Status:** Fase 1 — Fundação
|
|
|
|
## Stack
|
|
|
|
- Vite 6 + React 19 + TypeScript
|
|
- Tailwind v4 com paleta premium (obsidian/champagne/rose-gold)
|
|
- Supabase (Postgres + Auth + Storage)
|
|
- framer-motion + anime.js
|
|
- Vitest + Testing Library
|
|
|
|
## Setup local
|
|
|
|
1. Instale as dependências:
|
|
```bash
|
|
pnpm install
|
|
```
|
|
|
|
2. Copie as variáveis de ambiente:
|
|
```bash
|
|
cp .env.local.example .env.local
|
|
```
|
|
Preencha `VITE_SUPABASE_URL` e `VITE_SUPABASE_ANON_KEY` com os dados do seu projeto Supabase.
|
|
|
|
3. Rode o dev server:
|
|
```bash
|
|
pnpm dev
|
|
```
|
|
Abre em `http://localhost:5173`.
|
|
|
|
## Comandos
|
|
|
|
| Comando | Descrição |
|
|
|---|---|
|
|
| `pnpm dev` | Dev server com HMR |
|
|
| `pnpm build` | Build de produção |
|
|
| `pnpm preview` | Preview do build |
|
|
| `pnpm lint` | Roda ESLint |
|
|
| `pnpm format` | Formata com Prettier |
|
|
| `pnpm test` | Roda testes (Vitest) |
|
|
| `pnpm test:watch` | Testes em watch mode |
|
|
| `pnpm typecheck` | TypeScript check |
|
|
| `pnpm supabase:types` | Regenera tipos do Supabase |
|
|
|
|
## Estrutura
|
|
|
|
```
|
|
src/
|
|
├── components/ # Componentes React
|
|
│ └── ui/ # Primitivos shadcn/ui
|
|
├── lib/ # Clientes (supabase) e utils
|
|
├── types/ # Tipos gerados do Supabase
|
|
└── __tests__/ # Testes Vitest
|
|
supabase/
|
|
└── migrations/ # SQL de schema (source of truth)
|
|
_poc-reference/ # POC antigo — só pra consultar
|
|
```
|
|
|
|
## Paleta premium
|
|
|
|
| Token | Hex | Uso |
|
|
|---|---|---|
|
|
| `obsidian` | `#0B0D12` | Fundo principal |
|
|
| `midnight` | `#0F1A2E` | Superfícies elevadas |
|
|
| `champagne` | `#C9A961` | Ação primária, luxo |
|
|
| `rose-gold` | `#E8B4A0` | Acento secundário |
|
|
| `ivory` | `#F5F1E8` | Texto principal |
|
|
| `slate` | `#6B7280` | Texto secundário |
|
|
| `emerald` | `#10B981` | Sucesso |
|
|
| `ruby` | `#E11D48` | Erro |
|
|
|
|
## Integração com Chatwoot
|
|
|
|
Esta fase **não** integra com Chatwoot ainda. A geração de PIX acontece na Fase 2, onde um endpoint novo é criado no Chatwoot (`POST /public/api/v1/captain/public_reservations`) e esta app passa a consumir.
|
|
|
|
## Referências
|
|
|
|
- Spec de design: `chatwoot/docs/superpowers/specs/2026-04-13-reserva-1001-design.md`
|
|
- Plano Fase 1: `chatwoot/docs/superpowers/plans/2026-04-13-reserva-1001-fase-1-fundacao.md`
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add README.md
|
|
git commit -m "docs: adiciona README da fase 1"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 13: Validação final — rodar tudo e conferir
|
|
|
|
- [ ] **Step 1: Typecheck**
|
|
|
|
```bash
|
|
cd /Users/user/Dev/Produtos/Chatwoot-fazer-ai/fazer-ai-kanban/reserva-1001
|
|
pnpm typecheck
|
|
```
|
|
|
|
Expected: sem erros.
|
|
|
|
- [ ] **Step 2: Lint**
|
|
|
|
```bash
|
|
pnpm lint
|
|
```
|
|
|
|
Expected: sem erros.
|
|
|
|
- [ ] **Step 3: Testes**
|
|
|
|
```bash
|
|
pnpm test
|
|
```
|
|
|
|
Expected: 3 testes passando.
|
|
|
|
- [ ] **Step 4: Build**
|
|
|
|
```bash
|
|
pnpm build
|
|
```
|
|
|
|
Expected: build sem erros, gera `dist/`.
|
|
|
|
- [ ] **Step 5: Dev server + inspeção visual**
|
|
|
|
```bash
|
|
pnpm dev
|
|
```
|
|
|
|
Abra `http://localhost:5173`:
|
|
- Título "Reserva Rede 1001" em gradiente dourado ✓
|
|
- Marcas do schema `reserva_hotel` listadas em cards escuros com borda champagne ✓
|
|
- Hover nos cards faz borda brilhar ✓
|
|
- Fundo com gradiente obsidian→midnight ✓
|
|
- Fonte serif (Fraunces) nos títulos, sans (Inter) no resto ✓
|
|
|
|
Pare com `Ctrl+C`.
|
|
|
|
- [ ] **Step 6: git log pra conferir a trilha de commits**
|
|
|
|
```bash
|
|
git log --oneline
|
|
```
|
|
|
|
Expected: algo parecido com:
|
|
```
|
|
abc1234 docs: adiciona README da fase 1
|
|
bcd2345 chore: configura eslint flat config + prettier
|
|
cde3456 test: smoke test do App com mock do supabase
|
|
def4567 feat: componente Button base com variantes premium
|
|
efg5678 feat: migra FormField e SelectField com paleta premium
|
|
fgh6789 feat: renderiza marcas reais do supabase na pagina inicial
|
|
ghi7890 feat: gera tipos typescript do supabase
|
|
hij8901 feat: documenta migration aditiva do schema reserva_hotel
|
|
ijk9012 feat: configura cliente supabase com variaveis de ambiente
|
|
jkl0123 feat: configura tailwind v4 com paleta premium (obsidian/champagne/rose-gold)
|
|
klm1234 feat: scaffold inicial do projeto vite + react + typescript
|
|
lmn2345 chore: inicializa repo com POC como referencia
|
|
```
|
|
|
|
---
|
|
|
|
## Critérios de conclusão da Fase 1
|
|
|
|
- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm test`, `pnpm build` todos passam
|
|
- [ ] Dev server sobe e renderiza a página inicial com as marcas vindas do schema `reserva_hotel`
|
|
- [ ] Paleta premium visível e consistente
|
|
- [ ] Tipografia Fraunces + Inter carregada
|
|
- [ ] FormField, SelectField e Button prontos para uso (mesmo que não usados ainda)
|
|
- [ ] Schema Supabase aplicado e tipos gerados
|
|
- [ ] README funcional
|
|
- [ ] Commits atômicos e descritivos
|
|
|
|
## Próxima fase
|
|
|
|
**Fase 2 — Backend Chatwoot:** criar o controller `PublicReservationsController` no repo do Chatwoot com os endpoints `POST /public_reservations` e `GET /public_reservations/:id/status`, com token auth e testes RSpec. Plano separado: `docs/superpowers/plans/2026-04-13-reserva-1001-fase-2-backend-chatwoot.md` (a escrever).
|