diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 135eed0d2..2d3a6a611 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -160,6 +160,15 @@ jobs: - name: 🏗️ Build Frontend Workspace working-directory: ./src/Web + env: + # Dummy values required so Next.js can collect page data during build. + # auth.ts throws at module load time if these are missing, causing SSG to fail. + KEYCLOAK_ADMIN_CLIENT_ID: ci-build-placeholder + KEYCLOAK_ADMIN_CLIENT_SECRET: ci-build-placeholder + KEYCLOAK_ISSUER: http://localhost:8080/realms/meajudaai + NEXTAUTH_URL: http://localhost:3000 + NEXTAUTH_SECRET: ci-build-placeholder + AUTH_SECRET: ci-build-placeholder run: | set -e # 1. Explicitly build the customer web app (uses generated API types) diff --git a/docs/admin-frontend-documentation.md b/docs/admin-frontend-documentation.md new file mode 100644 index 000000000..aae0ed6e4 --- /dev/null +++ b/docs/admin-frontend-documentation.md @@ -0,0 +1,751 @@ +# Admin Portal (React/Next.js) Frontend Documentation + +## Overview +Admin Portal built with React 19 + Next.js 15, migrating from Blazor WASM. Provides administrative interface for managing providers, categories, services, allowed cities, and documents. + +--- + +## Route: `/login` + +### Page Code +```tsx +// src/app/login/page.tsx +"use client"; + +import { useSearchParams } from "next/navigation"; +import { signIn } from "next-auth/react"; +import { useState } from "react"; +import { Shield, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +export default function LoginPage() { + const searchParams = useSearchParams(); + const error = searchParams.get("error"); + const [isLoading, setIsLoading] = useState(false); + + const handleSignIn = async () => { + setIsLoading(true); + await signIn("keycloak", { callbackUrl: "/dashboard" }); + }; + + return ( +
+
+
+
+ +
+

MeAjudaAí

+

Portal do Administrador

+
+ + {error && ( +
+ + {error === "OAuthSignin" && "Erro ao iniciar autenticação. Tente novamente."} + {error === "OAuthCallback" && "Erro no processo de autenticação."} + {error === "OAuthAccountNotLinked" && "Conta não vinculada."} + {error === "CredentialsSignin" && "Credenciais inválidas."} + {!["OAuthSignin", "OAuthCallback", "OAuthAccountNotLinked", "CredentialsSignin"].includes(error) && + "Erro de autenticação. Tente novamente."} +
+ )} + +
+ +
+ +

+ Este é um portal restrito. Acesso apenas para administradores autorizados. +

+
+
+ ); +} +``` + +### Design Details +- **Layout**: Centered card on gradient background +- **Gradient**: `from-background to-muted` +- **Card**: `border-border bg-surface rounded-xl shadow-lg p-8 max-w-md` +- **Logo**: 64x64px circle with primary background +- **Button**: Primary variant, full width, large size + +--- + +## Route: `/dashboard` + +### Page Code +```tsx +// src/app/(admin)/dashboard/page.tsx +"use client"; + +import { Users, Clock, CheckCircle, AlertCircle, TrendingUp, Loader2 } from "lucide-react"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"; +import { useDashboardStats } from "@/hooks/admin"; + +const verificationColors = { + approved: "#22c55e", + pending: "#f59e0b", + underReview: "#3b82f6", + rejected: "#ef4444", + suspended: "#6b7280", +}; + +const typeColors = { + individual: "#8b5cf6", + company: "#06b6d4", + freelancer: "#f97316", + cooperative: "#ec4899", +}; + +export default function DashboardPage() { + const { data: stats, isLoading, error } = useDashboardStats(); + + // ... (charts and KPI cards) +} +``` + +### Design Details +- **KPI Cards Grid**: `grid gap-6 md:grid-cols-2 lg:grid-cols-4` +- **Charts Grid**: `grid gap-6 md:grid-cols-2` +- **Card Structure**: + - CardHeader with title and icon + - CardContent with large number and trend +- **Charts**: Recharts PieChart with inner radius (donut style) + +--- + +## Route: `/providers` + +### Page Code (Table with Pagination) +```tsx +// src/app/(admin)/providers/page.tsx +"use client"; + +import { useState } from "react"; +import { Search, Plus, Eye, CheckCircle, XCircle, Trash2, Loader2, ChevronLeft, ChevronRight } from "lucide-react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; + +const ITEMS_PER_PAGE = 10; + +// Badge variants based on verification status +const getVerificationBadgeVariant = (status?: VerificationStatus) => { + switch (status) { + case 2: return "success" as const; + case 0: return "warning" as const; + case 3: + case 4: return "destructive" as const; + default: return "secondary" as const; + } +}; +``` + +### Design Details +- **Search Bar**: Card with relative positioning, search icon absolute left +- **Table**: Full width, header with border-b, cells with padding +- **Pagination**: Fixed bottom bar with prev/next buttons and page numbers +- **Action Buttons**: Icon buttons for view, approve, reject, delete + +--- + +## Route: `/providers/[id]` + +### Page Code (Detail View) +```tsx +// src/app/(admin)/providers/[id]/page.tsx +"use client"; + +import { ArrowLeft, Mail, Phone, MapPin, FileText, CheckCircle, XCircle } from "lucide-react"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +``` + +### Design Details +- **Back Link**: Inline with ArrowLeft icon +- **Header**: Title, badges row, action buttons row +- **Cards Grid**: `grid gap-6 md:grid-cols-2` +- **Span Columns**: `md:col-span-2` for full-width cards +- **Dialogs**: Approve/Reject confirmation dialogs + +--- + +## Route: `/categories`, `/services`, `/allowed-cities`, `/documents` + +All CRUD pages follow the same pattern: + +### Common Structure +```tsx +// Search Card + +
+
+ + +
+
+
+ +// Data Table Card + + {isLoading && } + {error &&
Error message
} + {!isLoading && !error && ( + + ... + ... +
+ )} +
+ +// Dialogs for Create/Edit/Delete + + + + ... + ... + +
Form fields
+ + + + +
+
+``` + +--- + +## Route: `/settings` + +### Page Code (Tabs Layout) +```tsx +// src/app/(admin)/settings/page.tsx +"use client"; + +import { Settings, User, Bell, Shield, Palette, Save } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; + +type SettingsTab = "profile" | "notifications" | "security" | "appearance"; + +export default function SettingsPage() { + const [activeTab, setActiveTab] = useState("profile"); + + // Tabs: profile | notifications | security | appearance +} +``` + +### Design Details +- **Layout**: `grid gap-6 lg:grid-cols-4` (sidebar + content) +- **Tabs Navigation**: Vertical list in Card +- **Active Tab**: `bg-primary text-primary-foreground` +- **Inactive Tab**: `text-muted-foreground hover:bg-muted` + +--- + +## Components + +### Button +```tsx +// Variants +primary: "bg-primary text-primary-foreground hover:bg-primary-hover" +secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-hover" +ghost: "border-transparent bg-transparent text-muted-foreground hover:text-foreground hover:bg-muted" +destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90" + +// Sizes +sm: "h-9 px-3 text-sm" +md: "h-10 px-4 text-sm" +lg: "h-11 px-6 text-base" +icon: "h-10 w-10" +``` + +### Card +```tsx +// Container +rounded-xl border border-border bg-surface p-6 shadow-sm + +// Header +flex flex-col gap-1.5 + +// Title +text-lg font-semibold + +// Content +pt-2 +``` + +### Badge +```tsx +// Variants +default: "border-transparent bg-primary text-primary-foreground" +secondary: "border-transparent bg-secondary text-secondary-foreground" +destructive: "border-transparent bg-destructive text-destructive-foreground" +success: "border-transparent bg-green-100 text-green-800" +warning: "border-transparent bg-yellow-100 text-yellow-800" +``` + +### Input +```tsx +flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm +focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring +disabled:cursor-not-allowed disabled:opacity-50 +``` + +### Dialog (Base UI) +```tsx +// Overlay +fixed inset-0 z-50 bg-black/50 backdrop-blur-sm + +// Content +fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] +w-full max-w-lg rounded-lg border border-border bg-background p-6 shadow-lg + +// Animation +data-[state=open]:animate-in +data-[state=closed]:animate-out +``` + +### Select (Base UI) +```tsx +// Trigger +flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 + +// Item +flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm +``` + +### Theme Toggle +```tsx +// Button with Moon/Sun icons +// Uses useTheme hook from theme-provider +``` + +--- + +## CSS Variables (Tailwind v4) + +```css +/* src/app/global.css */ +@import "tailwindcss"; + +@theme inline { + /* Primary - Blue */ + --color-primary: #395873; + --color-primary-foreground: #ffffff; + --color-primary-hover: #2E4760; + + /* Secondary */ + --color-secondary: #f5f5f5; + --color-secondary-foreground: #2e2e2e; + + /* Muted */ + --color-muted: #f5f5f5; + --color-muted-foreground: #666666; + + /* Destructive */ + --color-destructive: #dc2626; + --color-destructive-foreground: #ffffff; + + /* Border & Input */ + --color-border: #e0e0e0; + --color-input: #e0e0e0; + + /* Background & Surface */ + --color-background: #ffffff; + --color-surface: #ffffff; + --color-surface-raised: #f5f5f5; + + /* Foreground */ + --color-foreground: #2e2e2e; + --color-foreground-subtle: #666666; + + /* Card */ + --color-card: #ffffff; + --color-card-foreground: #2e2e2e; + + /* Ring */ + --color-ring: #395873; + + /* Radius */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; +} + +/* Light Mode */ +:root { + --background: #ffffff; + --foreground: #2e2e2e; + --surface: #ffffff; + --surface-raised: #f5f5f5; + --foreground-subtle: #666666; + --border: #e0e0e0; + --input: #e0e0e0; +} + +/* Dark Mode */ +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + --surface: #1a1a1a; + --surface-raised: #262626; + --foreground-subtle: #a3a3a3; + --border: #404040; + --input: #404040; + } +} +``` + +--- + +## Sidebar Layout + +```tsx +// src/components/layout/sidebar.tsx + + +/* Main Content */ +
+ {/* Pages render here */} +
+``` + +### Navigation Items +```tsx +const navItems = [ + { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, + { href: "/providers", label: "Prestadores", icon: Users }, + { href: "/documents", label: "Documentos", icon: FileText }, + { href: "/categories", label: "Categorias", icon: FolderTree }, + { href: "/services", label: "Serviços", icon: Wrench }, + { href: "/allowed-cities", label: "Cidades", icon: MapPin }, + { href: "/settings", label: "Configurações", icon: Settings }, +]; +``` + +--- + +## Icons (Lucide React) + +Available icons used throughout the application: + +| Icon | Usage | +|------|-------| +| `LayoutDashboard` | Dashboard nav | +| `Users` | Providers nav, KPI | +| `FileText` | Documents nav, documents section | +| `FolderTree` | Categories nav | +| `Wrench` | Services nav | +| `MapPin` | Cities nav, toggle active | +| `Settings` | Settings nav | +| `LogOut` | Logout button | +| `Search` | Search input | +| `Plus` | Create buttons | +| `Pencil` | Edit buttons | +| `Trash2` | Delete buttons | +| `Eye` | View buttons | +| `CheckCircle` | Approve, success states | +| `XCircle` | Reject, suspended states | +| `Loader2` | Loading spinner | +| `ChevronLeft` | Pagination prev | +| `ChevronRight` | Pagination next | +| `Mail` | Email field | +| `Phone` | Phone field | +| `ArrowLeft` | Back link | +| `Moon/Sun` | Theme toggle | +| `Shield` | Login logo | +| `AlertCircle` | Error messages | +| `TrendingUp` | Dashboard trends | +| `Clock` | Pending items | +| `AlertCircle` | Rejected KPI | + +--- + +## Form Validation (Zod Schemas) + +### Category Schema +```tsx +const categorySchema = z.object({ + name: z.string().min(2, "Nome deve ter pelo menos 2 caracteres").max(100, "Nome deve ter no máximo 100 caracteres"), + description: z.string().max(500, "Descrição deve ter no máximo 500 caracteres").optional(), + isActive: z.boolean(), +}); +``` + +### Service Schema +```tsx +const serviceSchema = z.object({ + name: z.string().min(2, "Nome deve ter pelo menos 2 caracteres").max(100, "Nome deve ter no máximo 100 caracteres"), + description: z.string().max(500, "Descrição deve ter no máximo 500 caracteres").optional(), + categoryId: z.string().min(1, "Selecione uma categoria"), + isActive: z.boolean(), +}); +``` + +### City Schema +```tsx +const citySchema = z.object({ + city: z.string().min(2, "Cidade deve ter pelo menos 2 caracteres").max(100, "Cidade deve ter no máximo 100 caracteres"), + state: z.string().min(1, "Selecione um estado"), + serviceRadiusKm: z.coerce.number().min(1, "Raio deve ser pelo menos 1 km").max(500, "Raio máximo é 500 km"), + isActive: z.boolean(), +}); +``` + +--- + +## State Labels + +### Provider Types +```tsx +export const providerTypeLabels: Record = { + 0: "Não definido", + 1: "Pessoa Física", + 2: "Empresa", + 3: "Cooperativa", + 4: "Freelancer", +}; +``` + +### Verification Status +```tsx +export const verificationStatusLabels: Record = { + 0: "Pendente", + 1: "Em Análise", + 2: "Aprovado", + 3: "Rejeitado", + 4: "Suspenso", + 5: "Correção de Dados Necessária", +}; +``` + +### Provider Status +```tsx +export const providerStatusLabels: Record = { + 0: "Pendente", + 1: "Dados Básicos Necessários", + 2: "Dados Básicos Enviados", + 3: "Documentos Necessários", + 4: "Documentos Enviados", + 5: "Ativo", +}; +``` + +### Provider Tiers +```tsx +export const providerTierLabels: Record = { + 0: "Grátis", + 1: "Básico", + 2: "Premium", + 3: "Enterprise", +}; +``` + +--- + +## Tech Stack + +| Category | Technology | +|----------|------------| +| Framework | Next.js 15 (React 19) | +| Language | TypeScript (strict mode) | +| Styling | Tailwind CSS v4 | +| Components | @base-ui/react | +| State | TanStack Query + useState | +| Forms | react-hook-form + Zod | +| Icons | Lucide React | +| Charts | Recharts | +| Auth | next-auth + Keycloak | +| Toasts | Sonner | +| Utilities | tailwind-merge, class-variance-authority | + +--- + +## File Structure + +``` +src/Web/MeAjudaAi.Web.Admin-React/ +├── src/ +│ ├── app/ +│ │ ├── global.css # Tailwind v4 theme +│ │ ├── layout.tsx # Root layout with providers +│ │ ├── page.tsx # Redirects to /dashboard +│ │ ├── login/ +│ │ │ └── page.tsx # Login page +│ │ └── (admin)/ +│ │ ├── layout.tsx # Admin layout with sidebar +│ │ ├── dashboard/page.tsx +│ │ ├── providers/ +│ │ │ ├── page.tsx +│ │ │ └── [id]/page.tsx +│ │ ├── documents/page.tsx +│ │ ├── categories/page.tsx +│ │ ├── services/page.tsx +│ │ ├── allowed-cities/page.tsx +│ │ └── settings/page.tsx +│ ├── components/ +│ │ ├── layout/ +│ │ │ └── sidebar.tsx +│ │ ├── providers/ +│ │ │ ├── app-providers.tsx +│ │ │ ├── theme-provider.tsx +│ │ │ └── toast-provider.tsx +│ │ └── ui/ +│ │ ├── button.tsx +│ │ ├── badge.tsx +│ │ ├── card.tsx +│ │ ├── input.tsx +│ │ ├── dialog.tsx +│ │ ├── select.tsx +│ │ └── theme-toggle.tsx +│ ├── hooks/admin/ +│ │ ├── index.ts +│ │ ├── use-providers.ts +│ │ ├── use-allowed-cities.ts +│ │ ├── use-categories.ts +│ │ ├── use-users.ts +│ │ ├── use-services.ts +│ │ └── use-dashboard.ts +│ ├── lib/ +│ │ ├── auth/auth.ts +│ │ ├── types.ts +│ │ └── api/generated/ +│ └── middleware.ts +├── tailwind.config.js +├── tsconfig.json +└── next.config.js +``` + +--- + +## Brazilian States List + +```tsx +const brazilianStates = [ + "AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", + "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", + "RS", "RO", "RR", "SC", "SP", "SE", "TO" +]; +``` + +--- + +## Chart Colors + +### Verification Status +```tsx +const verificationColors = { + approved: "#22c55e", // green + pending: "#f59e0b", // yellow + underReview: "#3b82f6", // blue + rejected: "#ef4444", // red + suspended: "#6b7280", // gray +}; +``` + +### Provider Types +```tsx +const typeColors = { + individual: "#8b5cf6", // purple + company: "#06b6d4", // cyan + freelancer: "#f97316", // orange + cooperative: "#ec4899", // pink +}; +``` + +--- + +## Pagination + +```tsx +const ITEMS_PER_PAGE = 10; + +// Calculate +const totalPages = Math.ceil(filteredItems.length / ITEMS_PER_PAGE); +const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; +const paginatedItems = filteredItems.slice(startIndex, startIndex + ITEMS_PER_PAGE); + +// Reset on search +const handleSearch = (value: string) => { + setSearch(value); + setCurrentPage(1); +}; +``` + +--- + +## Toast Notifications (Sonner) + +```tsx +import { toast } from "sonner"; + +// Success +toast.success("Categoria criada com sucesso"); + +// Error +toast.error("Erro ao criar categoria"); +``` + +--- + +## API Types + +All API types are generated from OpenAPI spec using `@hey-api/openapi-ts`. + +### Key Types +```tsx +export type ProviderDto = MeAjudaAiModulesProvidersApplicationDtosProviderDto; +export type BusinessProfileDto = MeAjudaAiModulesProvidersApplicationDtosBusinessProfileDto; +export type DocumentDto = MeAjudaAiModulesProvidersApplicationDtosDocumentDto; +export type ServiceCategoryDto = MeAjudaAiModulesProvidersApplicationDtosServiceCategoryDto; +export type AllowedCityDto = MeAjudaAiModulesLocationsApplicationDtosAllowedCityDto; +export type UserDto = MeAjudaAiModulesUsersApplicationDtosUserDto; +``` diff --git a/docs/customer-frontend-documentation.md b/docs/customer-frontend-documentation.md new file mode 100644 index 000000000..cf8e56fce --- /dev/null +++ b/docs/customer-frontend-documentation.md @@ -0,0 +1,2796 @@ +# MeAjudaAi.Web.Customer - Documentação Completa de Frontend + +Este documento contém toda a estrutura HTML, CSS e informações de design do projeto MeAjudaAi.Web.Customer. + +--- + +# ÍNDICE + +1. [CSS Global](#1-css-global) +2. [Pages (Rotas)](#2-pages-rotas) +3. [Components - UI](#3-components---ui) +4. [Components - Layout](#4-components---layout) +5. [Components - Features](#5-components---features) +6. [Imagens e Assets](#6-imagens-e-assets) +7. [Sistema de Design](#7-sistema-de-design) + +--- + +# 1. CSS GLOBAL + +## Arquivo: `app/globals.css` + +```css +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #2e2e2e; + + /* Light mode tokens */ + --surface: #ffffff; + --surface-raised: #f5f5f5; + --foreground-subtle: #666666; + --border: #e0e0e0; + --input: #e0e0e0; + --popover: #ffffff; + --popover-foreground: var(--foreground); + --card: #ffffff; + --card-foreground: var(--foreground); + --muted: #f5f5f5; + --muted-foreground: #666666; + --accent: #f5f5f5; + --accent-foreground: var(--foreground); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + + /* Dark mode overrides */ + --surface: #1a1a1a; + --surface-raised: #262626; + --foreground-subtle: #a3a3a3; + --border: #404040; + --input: #404040; + --popover: #1a1a1a; + --popover-foreground: var(--foreground); + --card: #1a1a1a; + --card-foreground: var(--foreground); + --muted: #262626; + --muted-foreground: #a3a3a3; + --accent: #262626; + --accent-foreground: var(--foreground); + } +} + +@theme inline { + /* Colors from Figma */ + --color-primary: #395873; + --color-primary-foreground: #ffffff; + --color-primary-hover: #2E4760; + + --color-secondary: #D96704; + --color-secondary-light: #F2AE72; + --color-secondary-foreground: #ffffff; + --color-secondary-hover: #B85703; + + --color-surface: var(--surface); + --color-surface-raised: var(--surface-raised); + + --color-foreground: var(--foreground); + --color-foreground-subtle: var(--foreground-subtle); + + --color-border: var(--border); + --color-input: var(--input); + --color-ring: #D96704; + + --color-brand: #E0702B; + --color-brand-hover: #c56226; + + --color-destructive: #dc2626; + --color-destructive-foreground: #ffffff; + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-background: var(--background); + --font-sans: 'Roboto', Arial, Helvetica, sans-serif; +} + +body { + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans); +} +``` + +--- + +# 2. PAGES (ROTAS) + +## ROTA: `/` (Home) +**Arquivo:** `app/(main)/page.tsx` + +```tsx +import { CheckCircle2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { AdBanner } from "@/components/ui/ad-banner"; +import { CitySearch } from "@/components/search/city-search"; +import Image from "next/image"; +import { HowItWorks } from "@/components/home/how-it-works"; + +export default function HomePage() { + return ( +
+ + + {/* Hero Section - White Background */} +
+
+
+

+ Conectando quem precisa com + quem sabe fazer. +

+
+ + {/* City Search - Center aligned */} +
+ +
+
+
+ + {/* Blue Section - Conheça */} +
+
+
+

+ Conheça o MeAjudaAí +

+

+ Você já precisou de algum serviço e não sabia de nenhuma referência + ou alguém que conhecia alguém que faça esse serviço que você está + precisando? +

+

+ Nós nascemos para solucionar esse problema, uma plataforma que + conecta quem está oferecendo serviço com quem está prestando + serviço. Oferecemos métodos de avaliação dos serviços prestados, + você consegue saber se o prestador possui boas indicações com + base nos serviços já prestados por ele pela nossa plataforma. +

+
+ +
+ Conheça o MeAjudaAí +
+
+
+ + {/* How It Works Section */} +
+
+ +
+
+ + {/* CTA Prestadores */} +
+
+
+ +
+ Seja um prestador +
+ +
+
+
+ +
+

+ Você é prestador de serviço? +

+
+ +

+ Faça seu cadastro na nossa plataforma, cadastre seus serviços, + meios de contato e apareça para seus clientes, tenha boas + recomendações e destaque-se frente aos seus concorrentes. +

+ +

+ Não importa qual tipo de serviço você presta, sempre tem alguém + precisando de uma ajuda! Conseguimos fazer com que o seu cliente + te encontre, você estará na vitrine virtual mais cobiçada do Brasil. +

+ + +
+
+
+
+
+ ); +} +``` + +--- + +## ROTA: `/buscar` +**Arquivo:** `app/(main)/buscar/page.tsx` + +```tsx +import { Suspense } from "react"; +import { Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ServiceCard } from "@/components/service/service-card"; +import { AdCard } from "@/components/search/ad-card"; +import { ServiceTags } from "@/components/search/service-tags"; +import { SearchFilters } from "@/components/search/search-filters"; + +export default async function SearchPage({ searchParams }) { + // ... (server component com fetch de providers) + + return ( +
+ {/* Search Bar Centered */} +
+
+
+ + +
+
+
+ +
+ {/* Sidebar Filters */} + + +
+ {/* Service Tags */} +
+ }> + + +
+ + {/* Provider Grid */} + {providers.length > 0 ? ( +
+ {gridItems.map((item, index) => { + if (item.type === 'ad') { + return ; + } + return ( + s.serviceName).filter((s): s is string => !!s)} + rating={provider.averageRating ?? 0} + reviewCount={provider.reviewCount ?? 0} + /> + ); + })} +
+ ) : ( +
+ +

+ Nenhum prestador encontrado +

+
+ )} +
+
+
+ ); +} +``` + +--- + +## ROTA: `/prestador/[id]` +**Arquivo:** `app/(main)/prestador/[id]/page.tsx` + +```tsx +import { Avatar } from "@/components/ui/avatar"; +import { Rating } from "@/components/ui/rating"; +import { ReviewList } from "@/components/reviews/review-list"; +import { ReviewForm } from "@/components/reviews/review-form"; +import { Badge } from "@/components/ui/badge"; +import { MessageCircle } from "lucide-react"; +import { VerifiedBadge } from "@/components/ui/verified-badge"; +import { getWhatsappLink } from "@/lib/utils/phone"; + +export default async function ProviderProfilePage({ params }) { + const { id } = await params; + const providerData = await getCachedProvider(id); + + return ( +
+
+
+ {/* Left Column: Avatar, Rating, Phones */} +
+ + +
+ + {reviewCount > 0 && ( + ({reviewCount} avaliações) + )} +
+ + {phones.length > 0 ? ( +
+ {phones.map((phone, i) => ( +
+ {phone} + + + +
+ ))} +
+ ) : isAuthenticated ? ( +
+

Este prestador não informou contatos.

+
+ ) : ( +
+

Faça login para visualizar os contatos.

+
+ )} +
+ + {/* Right Column: Name, Email, Description, Services */} +
+
+

{displayName}

+ +
+ + {providerData.email && ( +

{providerData.email}

+ )} + +
+

{description}

+
+ + {services.length > 0 && ( +
+

Serviços

+
+ {services.map((service, i) => ( + + {service} + + ))} +
+
+ )} +
+
+
+ + {/* Comments Section */} +
+
+

Comentários

+
+
+
+ +
+ +
+
+
+ ); +} +``` + +--- + +## ROTA: `/prestador` (Dashboard Provider) +**Arquivo:** `app/(main)/prestador/page.tsx` + +```tsx +import DashboardClient from "@/components/providers/dashboard-client"; + +export default async function DashboardPage() { + // Server component que busca dados do provider e renderiza DashboardClient + return ; +} +``` + +--- + +## ROTA: `/perfil` +**Arquivo:** `app/(main)/perfil/page.tsx` + +```tsx +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { User, Mail, Phone, MapPin, Pencil } from "lucide-react"; +import Link from "next/link"; + +export default async function ProfilePage() { + return ( +
+
+

Meu Perfil

+ +
+ + + + Informações Pessoais + + +
+
+

+ Nome Completo +

+

{user.fullName}

+
+ +
+

+ Email +

+

{user.email}

+
+ +
+

+ Telefone +

+

{"Não informado"}

+
+ +
+

+ Localização +

+

{"Não informado"}

+
+
+
+
+
+ ); +} +``` + +--- + +## ROTA: `/perfil/editar` +**Arquivo:** `app/(main)/perfil/editar/page.tsx` + +```tsx +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { EditProfileForm } from "@/components/profile/edit-profile-form"; + +export default async function EditProfilePage() { + return ( +
+

Editar Perfil

+ + + + Dados Pessoais + + + + + +
+ ); +} +``` + +--- + +## ROTA: `/cadastro/prestador` +**Arquivo:** `app/(main)/cadastro/prestador/page.tsx` + +```tsx +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Checkbox } from "@/components/ui/checkbox"; +import { ShieldCheck, Info } from "lucide-react"; +import Link from "next/link"; + +export default function RegisterProviderPage() { + const form = useForm({ + resolver: zodResolver(registerProviderSchema), + defaultValues: { + name: "", + type: EProviderType.Individual, + documentNumber: "", + phoneNumber: "", + email: "", + acceptedTerms: false, + acceptedPrivacyPolicy: false, + }, + }); + + return ( +
+ {/* Stepper */} +
+
+ +
+
+ 1 +
+ Dados Iniciais +
+ +
+
+ 2 +
+ Endereço +
+ +
+
+ 3 +
+ Documentos +
+
+ +
+

+ Passo 1: Crie sua conta +

+

+ Inicie seu credenciamento. Nas próximas etapas, pediremos seu endereço e documentos. +

+
+ +
+ + ( + + Nome Completo (ou Razão Social) + + + + + + )} /> + + ( + + Tipo de Pessoa + +
+ + + + +
+
+
+ )} /> + + {/* Checkboxes de termos */} + ( + + + + +
+ + Aceito os Termos de Uso + +
+
+ )} /> + + + + + + {/* Privacy Badge */} +
+ +
+
+ ); +} +``` + +--- + +## ROTA: `/cadastro/prestador/perfil` +**Arquivo:** `app/(main)/cadastro/prestador/perfil/page.tsx` + +```tsx +import { BasicInfoForm } from "@/components/providers/basic-info-form"; + +export default function BasicInfoPage() { + return ( +
+ {/* Stepper com passo 1 ativo */} + {/* Formulário BasicInfoForm */} + +
+ ); +} +``` + +--- + +## ROTA: `/cadastro/prestador/perfil/endereco` +**Arquivo:** `app/(main)/cadastro/prestador/perfil/endereco/page.tsx` + +```tsx +import { AddressForm } from "@/components/providers/address-form"; + +export default function AddressPage() { + return ( +
+ {/* Stepper com passo 2 ativo */} + +
+ ); +} +``` + +--- + +## ROTA: `/cadastro/prestador/perfil/documentos` +**Arquivo:** `app/(main)/cadastro/prestador/perfil/documentos/page.tsx` + +```tsx +import { DocumentUpload } from "@/components/providers/document-upload"; + +export default function DocumentsPage() { + return ( +
+ {/* Stepper com passo 3 ativo */} + +
+ ); +} +``` + +--- + +## ROTA: `/auth/signin` +**Arquivo:** `app/(auth)/auth/signin/page.tsx` + +```tsx +import { LoginForm } from "@/components/auth/login-form"; +import { AuthSelectionDropdown } from "@/components/auth/auth-selection-dropdown"; + +export default function SignInPage() { + return ( +
+
+ + +
+
+ ); +} +``` + +--- + +## ROTA: `/auth/cadastro/cliente` +**Arquivo:** `app/(auth)/cadastro/cliente/page.tsx` + +```tsx +import { CustomerRegisterForm } from "@/components/auth/customer-register-form"; + +export default function CustomerRegisterPage() { + return ( +
+
+ +
+
+ ); +} +``` + +--- + +# 3. COMPONENTS - UI + +## `components/ui/button.tsx` + +```tsx +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary-hover", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-hover", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + primary: "bg-primary text-primary-foreground hover:bg-primary-hover", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; +``` + +--- + +## `components/ui/card.tsx` + +```tsx +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); +CardTitle.displayName = "CardTitle"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardContent.displayName = "CardContent"; + +export { Card, CardHeader, CardTitle, CardContent }; +``` + +--- + +## `components/ui/badge.tsx` + +```tsx +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVariants }; +``` + +--- + +## `components/ui/input.tsx` + +```tsx +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; +``` + +--- + +## `components/ui/textarea.tsx` + +```tsx +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface TextareaProps extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +