├── REFACTOR_PLAN.md ├── CLAUDE.md ├── .prettierrc ├── vercel.json ├── app ├── favicon.ico ├── api │ ├── auth │ │ └── [...all] │ │ │ └── route.ts │ ├── stripe │ │ └── webhook │ │ │ └── route.ts │ └── chat │ │ └── route.ts ├── _providers │ ├── theme-provider.tsx │ └── query-provider.tsx ├── _components │ ├── footer.tsx │ ├── ui │ │ ├── page.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── alert-dialog.tsx │ │ ├── sheet.tsx │ │ └── calendar.tsx │ ├── phone-item.tsx │ ├── barbershop-item.tsx │ ├── search-input.tsx │ ├── theme-toggle.tsx │ ├── header.tsx │ ├── quick-search-buttons.tsx │ ├── sidebar-menu.tsx │ ├── service-item.tsx │ └── booking-item.tsx ├── layout.tsx ├── chat │ ├── _components │ │ ├── chat-input.tsx │ │ └── chat-message.tsx │ └── page.tsx ├── _actions │ ├── create-booking.ts │ ├── get-date-available-time-slots.ts │ ├── create-booking-checkout-session.ts │ └── cancel-booking.ts ├── barbershops │ ├── page.tsx │ └── [id] │ │ └── page.tsx ├── page.tsx ├── bookings │ └── page.tsx └── globals.css ├── public ├── map.png ├── banner.png ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg ├── next.svg └── logo.svg ├── postcss.config.mjs ├── lib ├── action-client.ts ├── auth-client.ts ├── utils.ts ├── auth.ts └── prisma.ts ├── .mcp.json ├── prisma.config.ts ├── .env.example ├── docker-compose.yml ├── prompts ├── 06.md ├── 08.md ├── 04.md ├── 07.md ├── 05.md ├── 02.md ├── 03.md └── 01.md ├── next.config.ts ├── eslint.config.mjs ├── components.json ├── .claude ├── commands │ └── executar-task-react.md └── agents │ ├── code-reviewer.md │ ├── qa-test-documentation-generator.md │ ├── engenheiro-react-senior.md │ ├── revisor-de-codigo.md │ └── commit-message-generator.md ├── .cursor └── rules │ ├── typescript.mdc │ └── project.mdc ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json ├── prisma ├── schema.prisma └── seed.ts ├── PLANO_THEME_TOGGLE.md └── docs └── sistema-theme-toggle └── documentacao-mudancas.md /REFACTOR_PLAN.md: -------------------------------------------------------------------------------- 1 | - Colocar variantes na Badge 2 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | Use as regras que estão em @.cursor/rules. 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "prisma generate && next build" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackclubeducacao/fullstackweekend-aparatus/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackclubeducacao/fullstackweekend-aparatus/HEAD/public/map.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackclubeducacao/fullstackweekend-aparatus/HEAD/public/banner.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /lib/action-client.ts: -------------------------------------------------------------------------------- 1 | import { createSafeActionClient } from "next-safe-action"; 2 | 3 | export const actionClient = createSafeActionClient(); 4 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "browsermcp": { 4 | "command": "npx", 5 | "args": ["@browsermcp/mcp@latest"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from "better-auth/react"; // make sure to import from better-auth/react 2 | export const authClient = createAuthClient(); 3 | -------------------------------------------------------------------------------- /app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/lib/auth"; 2 | import { toNextJsHandler } from "better-auth/next-js"; 3 | 4 | export const { GET, POST } = toNextJsHandler(auth.handler); 5 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /prisma.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { defineConfig, env } from "prisma/config"; 3 | 4 | export default defineConfig({ 5 | schema: "prisma/schema.prisma", 6 | migrations: { 7 | path: "prisma/migrations", 8 | seed: "tsx prisma/seed.ts", 9 | }, 10 | datasource: { 11 | url: env("DATABASE_URL"), 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="" 2 | BETTER_AUTH_SECRET="secret" 3 | BETTER_AUTH_URL=http://localhost:3000 4 | 5 | GOOGLE_CLIENT_ID="" 6 | GOOGLE_CLIENT_SECRET="" 7 | 8 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="" 9 | STRIPE_SECRET_KEY="" 10 | STRIPE_WEBHOOK_SECRET="" 11 | 12 | NEXT_PUBLIC_APP_URL="http://localhost:3000" 13 | 14 | GOOGLE_GENERATIVE_AI_API_KEY="" 15 | OPENAI_API_KEY="" -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | db: 5 | image: postgres:15-alpine 6 | environment: 7 | POSTGRES_USER: user 8 | POSTGRES_PASSWORD: password 9 | POSTGRES_DB: barbershop_db 10 | ports: 11 | - "5432:5432" 12 | volumes: 13 | - postgres_data:/var/lib/postgresql/data 14 | 15 | volumes: 16 | postgres_data: 17 | -------------------------------------------------------------------------------- /app/_providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import type { ComponentProps } from "react"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/_components/footer.tsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 | 11 | ); 12 | }; 13 | 14 | export default Footer; 15 | -------------------------------------------------------------------------------- /app/_providers/query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | 5 | const queryClient = new QueryClient(); 6 | 7 | const QueryProvider = ({ children }: { children: React.ReactNode }) => { 8 | return ( 9 | {children} 10 | ); 11 | }; 12 | 13 | export default QueryProvider; 14 | -------------------------------------------------------------------------------- /prompts/06.md: -------------------------------------------------------------------------------- 1 | Em @app/api/stripe/webhook/route.ts é necessário você salvar o stripeChargeId ao criar a transação. 2 | 3 | Use Context7 para buscar na documentação do Stripe como recuperar o chargeId em @app/api/stripe/webhook/route.ts. 4 | 5 | Após isso, no caso de cancelamento de reserva, faça o estorno no Stripe em @app/\_actions/cancel-booking.ts. Use Context7 para buscar na documentação do Stripe como fazer isso. 6 | 7 | Use a biblioteca "stripe". 8 | -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { betterAuth } from "better-auth"; 2 | import { prismaAdapter } from "better-auth/adapters/prisma"; 3 | import { prisma } from "./prisma"; 4 | 5 | export const auth = betterAuth({ 6 | database: prismaAdapter(prisma, { provider: "postgresql" }), 7 | socialProviders: { 8 | google: { 9 | clientId: process.env.GOOGLE_CLIENT_ID || "", 10 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: "https", 9 | hostname: "utfs.io", 10 | }, 11 | ], 12 | }, 13 | serverExternalPackages: ["@prisma/client", "prisma"], 14 | outputFileTracingIncludes: { 15 | "/api/**/*": ["./app/generated/prisma/**/*"], 16 | }, 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | // lib/prisma.ts 2 | 3 | import { PrismaClient } from "@/generated/prisma/client"; 4 | import { PrismaPg } from "@prisma/adapter-pg"; 5 | 6 | const connectionString = `${process.env.DATABASE_URL}`; 7 | 8 | const adapter = new PrismaPg({ connectionString }); 9 | 10 | const globalForPrisma = global as unknown as { prisma: PrismaClient }; 11 | 12 | export const prisma = globalForPrisma.prisma || new PrismaClient({ adapter }); 13 | 14 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 15 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from "eslint/config"; 2 | import nextVitals from "eslint-config-next/core-web-vitals"; 3 | import nextTs from "eslint-config-next/typescript"; 4 | 5 | const eslintConfig = defineConfig([ 6 | ...nextVitals, 7 | ...nextTs, 8 | // Override default ignores of eslint-config-next. 9 | globalIgnores([ 10 | // Default ignores of eslint-config-next: 11 | ".next/**", 12 | "out/**", 13 | "build/**", 14 | "next-env.d.ts", 15 | ]), 16 | ]); 17 | 18 | export default eslintConfig; 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/app/_components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/app/_components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /.claude/commands/executar-task-react.md: -------------------------------------------------------------------------------- 1 | Use o @engenheiro-react-senior para realizar a tarefa. 2 | 3 | Depois da tarefa ser concluída, **SEMPRE** chame o agente @code-reviewer para revisar o código feito. 4 | 5 | Depois da revisão de código ser concluida, **SEMPRE** chame o @engenheiro-react-senior para aplicar **SOMENTE** as mudanças críticas identificar no code-review. 6 | 7 | Depois das mudanças serem aplicadas, **SEMPRE** chame o agente @qa-test-documentation-generator. 8 | 9 | Depois de gerar a documentação, **SEMPRE** chame o @commit-message-generator para gerar os commits. 10 | -------------------------------------------------------------------------------- /prompts/08.md: -------------------------------------------------------------------------------- 1 | Em @app/chat/page.tsx crie a interface que está em https://www.figma.com/design/ztdhMxufc4BRhOKwQYfJt2/Aparatus--Copy-?node-id=14-941&m=dev 2 | 3 | ## Requisitos Técnicos 4 | 5 | - Use a Vercel AI SDK e a rota "/api/chat" para construir o chat. 6 | - Apenas envie e receba as mensagens, não efetue nenhum tipo de formatação (vamos usar Streamdown depois). 7 | - Use COntext7 para buscar na documentação da Vercel AI SDK. 8 | - O botão de gravar áudio não deve fazer nada por enquanto. 9 | - Adicione ao lado do botão de gravar áudio um botão de enviar. 10 | - Deve ser possível enviar a mensagem ao apertar Enter. 11 | -------------------------------------------------------------------------------- /prompts/04.md: -------------------------------------------------------------------------------- 1 | ## Tarefa 2 | 3 | Sua tarefa é criar a tela que está em https://www.figma.com/design/KBlNBjp5XXWUj64ZCiT9lq/Aparatus?node-id=10-7658&m=dev usando Figma MCP no arquivo @app/bookings/page.tsx. 4 | 5 | ## Requisitos 6 | 7 | - Recupere os agendamentos do banco de dados. 8 | - Exiba os agendamentos confirmados de forma separada dos finalizados, assim como está no Figma. 9 | - Reutilize o componente @app/\_components/booking-item.tsx. 10 | - Um agendamento é considerado "Confirmado" quando a data é no futuro, e "Finalizado" quando ela é no passado. 11 | - Agendamentos cancelados (cancelled = true) podem ser exibidos junto com os finalizados. 12 | -------------------------------------------------------------------------------- /.cursor/rules/typescript.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | globs: *.ts,*.tsx 3 | alwaysApply: false 4 | --- 5 | 6 | --- 7 | 8 | globs: _.ts,_.tsx 9 | alwaysApply: false 10 | 11 | --- 12 | 13 | - Escreva um código limpo, conciso e fácil de manter, seguindo princípios do SOLID e Clean Code. 14 | - Use nomes de variáveis descritivos (exemplos: isLoading, hasError). 15 | - Use kebab-case para nomes de pastas e arquivos. 16 | - Sempre use TypeScript para escrever código. 17 | - DRY (Don't Repeat Yourself). Evite duplicidade de código. Quando necessário, crie funções/componentes reutilizáveis. 18 | - NUNCA escreva comentários no seu código. 19 | - NUNCA rode `npm run dev` para verificar se as mudanças estão funcionando. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | /app/generated/prisma 44 | /generated/prisma 45 | -------------------------------------------------------------------------------- /app/_components/ui/page.tsx: -------------------------------------------------------------------------------- 1 | export const PageContainer = ({ children }: { children: React.ReactNode }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export const PageSectionTitle = ({ children }: { children: string }) => { 6 | return ( 7 |

8 | {children} 9 |

10 | ); 11 | }; 12 | 13 | export const PageSection = ({ children }: { children: React.ReactNode }) => { 14 | return
{children}
; 15 | }; 16 | 17 | export const PageSectionScroller = ({ 18 | children, 19 | }: { 20 | children: React.ReactNode; 21 | }) => { 22 | return ( 23 |
24 | {children} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | ".next/dev/types/**/*.ts", 31 | "**/*.mts" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /app/_components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /prompts/07.md: -------------------------------------------------------------------------------- 1 | Ao enviar a busca no input de busca que está em @app/\_components/search-input.tsx, leve o usuário para a página "/barbershops?search=value". 2 | 3 | Busque no banco de dados todas as barbearias que possuem SERVIÇOS com um nome que contenham o valor buscado pelo usuário. 4 | 5 | Use o componente @app/\_components/barbershop-item.tsx para listar as barbearias. 6 | 7 | Também renderize abaixo do input de busca os botões de busca rápida que estão em https://www.figma.com/design/KBlNBjp5XXWUj64ZCiT9lq/Aparatus?node-id=1-6114&m=dev. Ao clicar em um botão, leve o usuário para a página de busca daquele botão. Por exemplo, se eu clicar em "Cabelo", quero buscar por "cabelo". Use os ícones do lucide-react nesses botões. 8 | 9 | Caso não haja barbearias encontradas, renderize uma mensagem dizendo isso. 10 | 11 | Armazene o valor do input em um state. 12 | -------------------------------------------------------------------------------- /prompts/05.md: -------------------------------------------------------------------------------- 1 | ## Tarefa 2 | 3 | - Criar um sheet de cancelamento de reserva que é exibido quando o usuário clica no @app/\_components/booking-item.tsx. 4 | - A interface deve ser EXATAMENTE a que está no Figma em https://www.figma.com/design/KBlNBjp5XXWUj64ZCiT9lq/Aparatus?node-id=78-2076&m=dev. 5 | - Ao clicar no botão de "Cancelar reserva" a reserva deve ser cancelada. 6 | 7 | ## Requisitos Técnicos 8 | 9 | - Use o Sheet do shadcn/ui. 10 | - Crie uma server action de cancelar a reserva chamada "cancel-booking" que recebe o ID da reserva e define booking.cancelled = true. 11 | - Os dados exibidos no Sheet devem ser os mesmos dados do agendamento clicado. 12 | - A imagem do mapa está em @public/map.png. 13 | - Status é confirmado se agendamento é no futuro e finalizado se é no passado. 14 | - Reutilize o componente @app/\_components/phone-item.tsx para os números de telefone. 15 | -------------------------------------------------------------------------------- /prompts/02.md: -------------------------------------------------------------------------------- 1 | ## Tarefa 2 | 3 | Criar tela que está em https://www.figma.com/design/KBlNBjp5XXWUj64ZCiT9lq/Aparatus?node-id=237-656&m=dev usando Figma MCP. 4 | 5 | Caso o usuário esteja deslogado, exiba a interface https://www.figma.com/design/KBlNBjp5XXWUj64ZCiT9lq/Aparatus?node-id=237-857&m=dev. 6 | 7 | Tudo deve ser feito em apenas um componente. 8 | 9 | ## Requisitos e Detalhes Técnicos 10 | 11 | - O avatar, nome e e-mail devem ser do usuário logado. 12 | - Botão de início deve levar para tela inicial. 13 | - Botão de agendamentos deve levar para página "/bookings" (NÃO CRIE ESSA PÁGINA AINDA) 14 | - Os botões de categoria (Cabelo, barba etc.) NÃO DEVEM FAZER NADA, POR ENQUANTO. 15 | - Botão de "Sair" deve fazer logout. 16 | - Use o componente Sheet do shadcn para exibir o componente. 17 | - Esse componente deve ser aberto ao clicar no botão de Menu que está no @app/\_components/header.tsx na linha 19. 18 | -------------------------------------------------------------------------------- /app/_components/phone-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Smartphone } from "lucide-react"; 4 | import { Button } from "./ui/button"; 5 | import { toast } from "sonner"; 6 | 7 | interface PhoneItemProps { 8 | phone: string; 9 | } 10 | 11 | export function PhoneItem({ phone }: PhoneItemProps) { 12 | const handleCopyPhone = () => { 13 | navigator.clipboard.writeText(phone); 14 | toast.success("Telefone copiado!"); 15 | }; 16 | 17 | return ( 18 |
19 |
20 | 21 |

{phone}

22 |
23 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prompts/03.md: -------------------------------------------------------------------------------- 1 | No componente @app/\_components/service-item.tsx, ao clicar em "Reservar" abra o sheet que está em https://www.figma.com/design/KBlNBjp5XXWUj64ZCiT9lq/Aparatus?node-id=78-1818&m=dev usando o componente Sheet do shadcn. 2 | 3 | Use o componente Calendar do shadcn para renderizar o calendário. 4 | 5 | Ao clicar em um dia do calendário, exiba horários fixos (das 09 às 18h, de meia em meia hora. Exemplos: 09:00, 09:30, 10:00). 6 | 7 | Ao clicar no horário, exiba as informações do: 8 | 9 | - Nome e preço do serviço (em reais inteiros, não em centavos) 10 | - Data selecionada no calendário 11 | - Horário selecionado 12 | - Nome da barbearia 13 | 14 | Ao clicar no botão de X, feche o Sheet. 15 | 16 | Habilite o botão de confirmar APENAS quando o dia e horário estiverem sido selecionados. 17 | 18 | ## Requisitos técnicos 19 | 20 | - Armazene o dia selecionado como Date em um state 21 | - Armazene o horário selecionado como string em um state (por ex.: "09:00) 22 | - Receba como prop o serviço que está agendado e sua barbearia. 23 | -------------------------------------------------------------------------------- /app/_components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /app/_components/barbershop-item.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { Barbershop } from "@/generated/prisma/client"; 3 | import Link from "next/link"; 4 | 5 | interface BarbershopItemProps { 6 | barbershop: Barbershop; 7 | } 8 | 9 | const BarbershopItem = ({ barbershop }: BarbershopItemProps) => { 10 | return ( 11 | 15 |
16 | {barbershop.name} 22 |
23 |

{barbershop.name}

24 |

{barbershop.address}

25 |
26 | 27 | ); 28 | }; 29 | 30 | export default BarbershopItem; 31 | -------------------------------------------------------------------------------- /app/_components/search-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SearchIcon } from "lucide-react"; 4 | import { useRouter } from "next/navigation"; 5 | import { FormEvent, useState } from "react"; 6 | import { Button } from "./ui/button"; 7 | import { Input } from "./ui/input"; 8 | 9 | const SearchInput = () => { 10 | const router = useRouter(); 11 | const [search, setSearch] = useState(""); 12 | 13 | const handleSubmit = (e: FormEvent) => { 14 | e.preventDefault(); 15 | if (!search.trim()) return; 16 | router.push(`/barbershops?search=${encodeURIComponent(search)}`); 17 | }; 18 | 19 | return ( 20 |
21 | setSearch(e.target.value)} 27 | /> 28 | 36 |
37 | ); 38 | }; 39 | 40 | export default SearchInput; 41 | -------------------------------------------------------------------------------- /app/_components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | CircleCheckIcon, 5 | InfoIcon, 6 | Loader2Icon, 7 | OctagonXIcon, 8 | TriangleAlertIcon, 9 | } from "lucide-react" 10 | import { useTheme } from "next-themes" 11 | import { Toaster as Sonner, type ToasterProps } from "sonner" 12 | 13 | const Toaster = ({ ...props }: ToasterProps) => { 14 | const { theme = "system" } = useTheme() 15 | 16 | return ( 17 | , 22 | info: , 23 | warning: , 24 | error: , 25 | loading: , 26 | }} 27 | style={ 28 | { 29 | "--normal-bg": "var(--popover)", 30 | "--normal-text": "var(--popover-foreground)", 31 | "--normal-border": "var(--border)", 32 | "--border-radius": "var(--radius)", 33 | } as React.CSSProperties 34 | } 35 | {...props} 36 | /> 37 | ) 38 | } 39 | 40 | export { Toaster } 41 | -------------------------------------------------------------------------------- /app/_components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MoonIcon, SunIcon } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | import { Button } from "./ui/button"; 6 | import { useEffect, useState, useCallback } from "react"; 7 | 8 | export const ThemeToggle = () => { 9 | const { theme, setTheme } = useTheme(); 10 | const [mounted, setMounted] = useState(false); 11 | 12 | useEffect(() => { 13 | setMounted(true); 14 | }, []); 15 | 16 | const toggleTheme = useCallback(() => { 17 | setTheme(theme === "dark" ? "light" : "dark"); 18 | }, [theme, setTheme]); 19 | 20 | if (!mounted) { 21 | return ( 22 | 25 | ); 26 | } 27 | 28 | return ( 29 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /app/_components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "./_components/ui/sonner"; 5 | import QueryProvider from "./_providers/query-provider"; 6 | import { ThemeProvider } from "./_providers/theme-provider"; 7 | 8 | const geistSans = Geist({ 9 | variable: "--font-geist-sans", 10 | subsets: ["latin"], 11 | }); 12 | 13 | const geistMono = Geist_Mono({ 14 | variable: "--font-geist-mono", 15 | subsets: ["latin"], 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Create Next App", 20 | description: "Generated by create next app", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 33 | 39 | 40 | {children} 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prompts/01.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Você é um desenvolvedor full stack sênior, especializado em Next.js. 4 | 5 | 6 | 7 | Tecnologias utilizadas: 8 | 9 | - Next.js 10 | - Prisma 11 | - shadcn/ui 12 | 13 | - SEMPRE use shadcn como biblioteca de componentes 14 | - SEMPRE use componentes que estão em @app/\_components/ui/page.tsx 15 | - NUNCA use cores hard-coded do Tailwind, APENAS cores do tema que estão em @app/globals.css.. 16 | - Use a página que está em @app/page.tsx como referência para criar e organizar o código. 17 | - **SEMPRE** use o MCP do Context7 para buscar documentações, sites e APIs 18 | 19 | 20 | 21 | Crie a página que está em https://www.figma.com/design/KBlNBjp5XXWUj64ZCiT9lq/Aparatus?node-id=10-6869&m=dev usando Figma MCP. 22 | 23 | Seja 100% fiel ao Figma **CUSTE O QUE CUSTAR**. 24 | 25 | Pegue os dados do banco de dados usando o ID que é recebido como parâmetro na rota. 26 | 27 | O botão de "Reservar" NÃO DEVE fazer nada. 28 | 29 | O botão de "Copiar" telefone deve copiar o telefone para o clipboard do usuário. 30 | 31 | A imagem do banner da página no topo deve ser a imagem da barbearia no banco de dados. 32 | 33 | O botão de voltar no topo da página deve voltar a página inicial do projeto. 34 | 35 | A imagem de cada serviço deve ser o campo "imageUrl" da tabela "BarbershopService". 36 | 37 | Crie a página em @app/barbershops/[id]/page.tsx. 38 | -------------------------------------------------------------------------------- /app/_components/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { Button } from "./ui/button"; 5 | import { MenuIcon, MessageCircleIcon } from "lucide-react"; 6 | import { 7 | Sheet, 8 | SheetContent, 9 | SheetHeader, 10 | SheetTitle, 11 | SheetTrigger, 12 | } from "./ui/sheet"; 13 | import SidebarMenu from "./sidebar-menu"; 14 | import Link from "next/link"; 15 | import { ThemeToggle } from "./theme-toggle"; 16 | 17 | const Header = () => { 18 | return ( 19 |
20 | Aparatus 21 |
22 | 23 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | Menu 37 | 38 | 39 | 40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Header; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /app/chat/_components/chat-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Mic, Send } from "lucide-react"; 4 | import { Button } from "@/app/_components/ui/button"; 5 | import { Input } from "@/app/_components/ui/input"; 6 | 7 | interface ChatInputProps { 8 | input: string; 9 | onChange: (e: React.ChangeEvent) => void; 10 | onSubmit: (e: React.FormEvent) => void; 11 | isLoading?: boolean; 12 | } 13 | 14 | export const ChatInput = ({ 15 | input, 16 | onChange, 17 | onSubmit, 18 | isLoading, 19 | }: ChatInputProps) => { 20 | const handleKeyDown = (e: React.KeyboardEvent) => { 21 | if (e.key === "Enter" && !e.shiftKey) { 22 | e.preventDefault(); 23 | onSubmit(e); 24 | } 25 | }; 26 | 27 | return ( 28 |
29 |
30 | 38 | 45 | 53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /app/_actions/create-booking.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { actionClient } from "@/lib/action-client"; 3 | import { auth } from "@/lib/auth"; 4 | import { prisma } from "@/lib/prisma"; 5 | import { returnValidationErrors } from "next-safe-action"; 6 | import { headers } from "next/headers"; 7 | import { z } from "zod"; 8 | 9 | const inputSchema = z.object({ 10 | serviceId: z.uuid(), 11 | date: z.date(), 12 | }); 13 | 14 | export const createBooking = actionClient 15 | .inputSchema(inputSchema) 16 | .action(async ({ parsedInput: { serviceId, date } }) => { 17 | const session = await auth.api.getSession({ 18 | headers: await headers(), 19 | }); 20 | if (!session?.user) { 21 | returnValidationErrors(inputSchema, { 22 | _errors: ["Unauthorized"], 23 | }); 24 | } 25 | const service = await prisma.barbershopService.findUnique({ 26 | where: { 27 | id: serviceId, 28 | }, 29 | }); 30 | if (!service) { 31 | returnValidationErrors(inputSchema, { 32 | _errors: ["Service not found"], 33 | }); 34 | } 35 | // verificar se já existe agendamento para essa data 36 | const existingBooking = await prisma.booking.findFirst({ 37 | where: { 38 | barbershopId: service.barbershopId, 39 | date, 40 | }, 41 | }); 42 | if (existingBooking) { 43 | console.error("Já existe um agendamento para essa data."); 44 | returnValidationErrors(inputSchema, { 45 | _errors: ["Já existe um agendamento para essa data."], 46 | }); 47 | } 48 | const booking = await prisma.booking.create({ 49 | data: { 50 | serviceId, 51 | date, 52 | userId: session.user.id, 53 | barbershopId: service.barbershopId, 54 | }, 55 | }); 56 | return booking; 57 | }); 58 | -------------------------------------------------------------------------------- /app/_components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span" 36 | 37 | return ( 38 | 43 | ) 44 | } 45 | 46 | export { Badge, badgeVariants } 47 | -------------------------------------------------------------------------------- /app/_actions/get-date-available-time-slots.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { actionClient } from "@/lib/action-client"; 4 | import { prisma } from "@/lib/prisma"; 5 | import z from "zod"; 6 | import { endOfDay, format, startOfDay } from "date-fns"; 7 | import { auth } from "@/lib/auth"; 8 | import { headers } from "next/headers"; 9 | import { returnValidationErrors } from "next-safe-action"; 10 | 11 | const inputSchema = z.object({ 12 | barbershopId: z.string(), 13 | date: z.date(), 14 | }); 15 | 16 | const TIME_SLOTS = [ 17 | "09:00", 18 | "09:30", 19 | "10:00", 20 | "10:30", 21 | "11:00", 22 | "11:30", 23 | "12:00", 24 | "12:30", 25 | "13:00", 26 | "13:30", 27 | "14:00", 28 | "14:30", 29 | "15:00", 30 | "15:30", 31 | "16:00", 32 | "16:30", 33 | "17:00", 34 | "17:30", 35 | "18:00", 36 | ]; 37 | 38 | export const getDateAvailableTimeSlots = actionClient 39 | .inputSchema(inputSchema) 40 | .action(async ({ parsedInput: { barbershopId, date } }) => { 41 | const session = await auth.api.getSession({ 42 | headers: await headers(), 43 | }); 44 | if (!session?.user) { 45 | returnValidationErrors(inputSchema, { 46 | _errors: ["Unauthorized"], 47 | }); 48 | } 49 | const bookings = await prisma.booking.findMany({ 50 | where: { 51 | barbershopId, 52 | date: { 53 | gte: startOfDay(date), 54 | lte: endOfDay(date), 55 | }, 56 | }, 57 | }); 58 | const occupiedSlots = bookings.map((booking) => 59 | format(booking.date, "HH:mm"), 60 | ); 61 | const availableTimeSlots = TIME_SLOTS.filter( 62 | (slot) => !occupiedSlots.includes(slot), 63 | ); 64 | return availableTimeSlots; 65 | }); 66 | 67 | // => ["10:00", "11:00"] 68 | 69 | // Toda barbearia vai ter horários das 09h às 18h. 70 | // Todo serviço vai ocupar 30 minutos. 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fsw-aparatus", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "eslint", 11 | "postinstall": "prisma generate" 12 | }, 13 | "dependencies": { 14 | "@ai-sdk/google": "2.0.46", 15 | "@ai-sdk/openai": "2.0.85", 16 | "@ai-sdk/react": "2.0.114", 17 | "@prisma/adapter-pg": "^6.19.1", 18 | "@prisma/client": "7.1.0", 19 | "@prisma/nextjs-monorepo-workaround-plugin": "6.19.1", 20 | "@radix-ui/react-alert-dialog": "^1.1.15", 21 | "@radix-ui/react-avatar": "^1.1.11", 22 | "@radix-ui/react-dialog": "^1.1.15", 23 | "@radix-ui/react-separator": "^1.1.8", 24 | "@radix-ui/react-slot": "^1.2.4", 25 | "@stripe/stripe-js": "7.8.0", 26 | "@tanstack/react-query": "^5.90.12", 27 | "ai": "5.0.112", 28 | "better-auth": "1.4.6", 29 | "class-variance-authority": "^0.7.1", 30 | "clsx": "^2.1.1", 31 | "date-fns": "^4.1.0", 32 | "dotenv": "17.2.3", 33 | "lucide-react": "^0.561.0", 34 | "next": "16.0.10", 35 | "next-safe-action": "8.0.11", 36 | "next-themes": "^0.4.6", 37 | "pg": "^8.16.3", 38 | "prisma": "7.1.0", 39 | "react": "19.2.3", 40 | "react-day-picker": "^9.12.0", 41 | "react-dom": "19.2.3", 42 | "shiki": "^3.20.0", 43 | "sonner": "^2.0.7", 44 | "streamdown": "1.6.10", 45 | "stripe": "18.4.0", 46 | "tailwind-merge": "^3.4.0", 47 | "zod": "4.1.13" 48 | }, 49 | "devDependencies": { 50 | "@tailwindcss/postcss": "^4", 51 | "@types/node": "^20", 52 | "@types/react": "^19", 53 | "@types/react-dom": "^19", 54 | "eslint": "^9", 55 | "eslint-config-next": "16.0.10", 56 | "prettier": "3.7.4", 57 | "prettier-plugin-tailwindcss": "0.7.2", 58 | "tailwindcss": "^4", 59 | "tsx": "4.21.0", 60 | "tw-animate-css": "^1.4.0", 61 | "typescript": "^5" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/barbershops/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prisma"; 2 | import BarbershopItem from "../_components/barbershop-item"; 3 | import Footer from "../_components/footer"; 4 | import Header from "../_components/header"; 5 | import QuickSearchButtons from "../_components/quick-search-buttons"; 6 | import SearchInput from "../_components/search-input"; 7 | import { PageContainer } from "../_components/ui/page"; 8 | 9 | const BarbershopsPage = async ({ searchParams }: PageProps<"/barbershops">) => { 10 | const { search } = await searchParams; 11 | const barbershops = search 12 | ? await prisma.barbershop.findMany({ 13 | where: { 14 | services: { 15 | some: { 16 | name: { 17 | contains: search as string, 18 | mode: "insensitive", 19 | }, 20 | }, 21 | }, 22 | }, 23 | orderBy: { 24 | name: "asc", 25 | }, 26 | }) 27 | : []; 28 | 29 | return ( 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | {search && ( 38 |
39 |

40 | Resultados para "{search}" 41 |

42 | 43 | {barbershops.length > 0 ? ( 44 |
45 | {barbershops.map((barbershop) => ( 46 | 47 | ))} 48 |
49 | ) : ( 50 |

51 | Nenhuma barbearia encontrada. 52 |

53 | )} 54 |
55 | )} 56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | export default BarbershopsPage; 63 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Header from "./_components/header"; 3 | import SearchInput from "./_components/search-input"; 4 | import banner from "../public/banner.png"; 5 | import { prisma } from "@/lib/prisma"; 6 | import BarbershopItem from "./_components/barbershop-item"; 7 | import Footer from "./_components/footer"; 8 | import { 9 | PageContainer, 10 | PageSection, 11 | PageSectionScroller, 12 | PageSectionTitle, 13 | } from "./_components/ui/page"; 14 | import QuickSearchButtons from "./_components/quick-search-buttons"; 15 | 16 | const Home = async () => { 17 | const recommendedBarbershops = await prisma.barbershop.findMany({ 18 | orderBy: { 19 | name: "asc", 20 | }, 21 | }); 22 | const popularBarbershops = await prisma.barbershop.findMany({ 23 | orderBy: { 24 | name: "desc", 25 | }, 26 | }); 27 | return ( 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | Agende agora! 41 | 42 | 43 | Recomendados 44 | 45 | {recommendedBarbershops.map((barbershop) => ( 46 | 47 | ))} 48 | 49 | 50 | 51 | 52 | Populares 53 | 54 | {popularBarbershops.map((barbershop) => ( 55 | 56 | ))} 57 | 58 | 59 | 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default Home; 66 | -------------------------------------------------------------------------------- /app/api/stripe/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prisma"; 2 | import { revalidatePath } from "next/cache"; 3 | import { NextResponse } from "next/server"; 4 | import Stripe from "stripe"; 5 | 6 | export const POST = async (request: Request) => { 7 | if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET) { 8 | return NextResponse.error(); 9 | } 10 | const signature = request.headers.get("stripe-signature"); 11 | if (!signature) { 12 | return NextResponse.error(); 13 | } 14 | const text = await request.text(); 15 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); 16 | const event = stripe.webhooks.constructEvent( 17 | text, 18 | signature, 19 | process.env.STRIPE_WEBHOOK_SECRET, 20 | ); 21 | if (event.type === "checkout.session.completed") { 22 | const session = event.data.object; 23 | const date = session.metadata?.date 24 | ? new Date(session.metadata.date) 25 | : null; 26 | const serviceId = session.metadata?.serviceId; 27 | const barbershopId = session.metadata?.barbershopId; 28 | const userId = session.metadata?.userId; 29 | if (!date || !serviceId || !barbershopId || !userId) { 30 | return NextResponse.error(); 31 | } 32 | 33 | // Retrieve session with expanded payment_intent to get chargeId 34 | const expandedSession = await stripe.checkout.sessions.retrieve( 35 | session.id, 36 | { 37 | expand: ["payment_intent"], 38 | }, 39 | ); 40 | 41 | // Extract chargeId from payment_intent 42 | const paymentIntent = expandedSession.payment_intent as Stripe.PaymentIntent; 43 | const chargeId = 44 | typeof paymentIntent?.latest_charge === "string" 45 | ? paymentIntent.latest_charge 46 | : paymentIntent?.latest_charge?.id; 47 | 48 | await prisma.booking.create({ 49 | data: { 50 | barbershopId, 51 | serviceId, 52 | date, 53 | userId, 54 | stripeChargeId: chargeId || null, 55 | }, 56 | }); 57 | } 58 | revalidatePath("/bookings"); 59 | return NextResponse.json({ received: true }); 60 | }; 61 | -------------------------------------------------------------------------------- /app/chat/_components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | import { UIMessage } from "ai"; 2 | import { Bot } from "lucide-react"; 3 | import { Streamdown } from "streamdown"; 4 | 5 | interface ChatMessageProps { 6 | message: UIMessage; 7 | isStreaming?: boolean; 8 | } 9 | 10 | export const ChatMessage = ({ 11 | message, 12 | isStreaming = false, 13 | }: ChatMessageProps) => { 14 | const isUser = message.role === "user"; 15 | const isSystem = message.role === "system"; 16 | 17 | const content = message.parts 18 | .filter((part) => part.type === "text") 19 | .map((part) => part.text) 20 | .join(""); 21 | 22 | if (isSystem) { 23 | return ( 24 |
25 |
26 |
27 |

28 | {content} 29 |

30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | if (isUser) { 37 | return ( 38 |
39 |
40 |

41 | {content} 42 |

43 |
44 |
45 | ); 46 | } 47 | 48 | return ( 49 |
50 |
51 |
52 | 53 |
54 |
55 | {content} 56 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /.cursor/rules/project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | --- 4 | 5 | 6 | 7 | Você é um engenheiro de software sênior especializado em desenvolvimento web moderno, com profundo conhecimento em TypeScript, React 19, Next.js 16 (App Router), Postgres, PRISMA, shadcn/ui e Tailwind CSS. Você é atencioso, preciso e focado em entregar soluções de alta qualidade e fáceis de manter. 8 | 9 | 10 | 11 | Tecnologias utilizadas: 12 | 13 | - Next.js 14 | - Prisma 15 | - shadcn/ui 16 | - Tailwind CSS 17 | - BetterAuth para autenticação. 18 | 19 | - SEMPRE use shadcn como biblioteca de componentes 20 | - SEMPRE use componentes que estão em @app/\_components/ui/page.tsx 21 | - NUNCA use cores hard-coded do Tailwind, APENAS cores do tema que estão em @app/globals.css.. 22 | - Use a página que está em @app/page.tsx como referência para criar e organizar o código. 23 | - **SEMPRE** use o MCP do Context7 para buscar documentações, sites e APIs 24 | - **SEMPRE** use os componentes @app/\_components/footer.tsx e @app/\_components/header.tsx na hora de criar headers e footers. **NUNCA** os crie manualmente. 25 | - Evite ao máximo duplicidade de código. Ao repetir um código, crie componentes e/ou funções utilitárias. 26 | - Ao usar Figma MCP, **SEMPRE** seja 100% fiel ao Figma **CUSTE O QUE CUSTAR**. 27 | - Todo scroll horizontal **DEVE SEMPRE** esconder a barra de scroll usando className="[&::-webkit-scrollbar]:hidden" 28 | 29 | ## Server Actions 30 | 31 | - **SEMPRE** use a biblioteca "next-safe-action" para criar Server Actions. 32 | - **SEMPRE** Use o hook "useAction" da biblioteca "next-safe-action" para chamar uma Server Action. 33 | - **SEMPRE** use a Server Action @app/\_actions/create-booking.ts como base para criar as suas. 34 | - **SEMPRE** faça validações de autorização e autenticação em uma Server Action conforme o usuário. 35 | - **SEMPRE** crie as server actions na pasta @app/\_actions. 36 | 37 | ## shadcn/ui 38 | 39 | - AO criar sheets, **NUNCA** crie manualmnete o botão de fechar, o próprio Sheet do shadcn já tem um. 40 | - Ao criar sheets, **NUNCA** crie o separator entre o header e o conteúdo manualmente, o próprio SheetHeader já tem um border-bottom. 41 | 42 | - **NUNCA** execute `npm run dev` pra validar suas mudanças. 43 | 44 | ## Figma MCP 45 | 46 | - **NUNCA** faça downloads de ícones do Figma, **SEMPRE** use a biblioteca lucide-react para renderizar ícones. 47 | -------------------------------------------------------------------------------- /app/_components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 | outline: 16 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: 20 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | "icon-sm": "size-8", 29 | "icon-lg": "size-10", 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: "default", 34 | size: "default", 35 | }, 36 | } 37 | ) 38 | 39 | function Button({ 40 | className, 41 | variant, 42 | size, 43 | asChild = false, 44 | ...props 45 | }: React.ComponentProps<"button"> & 46 | VariantProps & { 47 | asChild?: boolean 48 | }) { 49 | const Comp = asChild ? Slot : "button" 50 | 51 | return ( 52 | 57 | ) 58 | } 59 | 60 | export { Button, buttonVariants } 61 | -------------------------------------------------------------------------------- /app/_components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /app/_actions/create-booking-checkout-session.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { actionClient } from "@/lib/action-client"; 3 | import { auth } from "@/lib/auth"; 4 | import { returnValidationErrors } from "next-safe-action"; 5 | import { headers } from "next/headers"; 6 | import { z } from "zod"; 7 | import Stripe from "stripe"; 8 | import { prisma } from "@/lib/prisma"; 9 | import { format } from "date-fns"; 10 | 11 | const inputSchema = z.object({ 12 | serviceId: z.uuid(), 13 | date: z.date(), 14 | }); 15 | 16 | export const createBookingCheckoutSession = actionClient 17 | .inputSchema(inputSchema) 18 | .action(async ({ parsedInput: { serviceId, date } }) => { 19 | if (!process.env.STRIPE_SECRET_KEY) { 20 | throw new Error("STRIPE_SECRET_KEY is not set"); 21 | } 22 | const session = await auth.api.getSession({ 23 | headers: await headers(), 24 | }); 25 | if (!session?.user) { 26 | returnValidationErrors(inputSchema, { 27 | _errors: ["Unauthorized"], 28 | }); 29 | } 30 | const service = await prisma.barbershopService.findUnique({ 31 | where: { 32 | id: serviceId, 33 | }, 34 | include: { 35 | barbershop: true, 36 | }, 37 | }); 38 | if (!service) { 39 | returnValidationErrors(inputSchema, { 40 | _errors: ["Service not found"], 41 | }); 42 | } 43 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 44 | apiVersion: "2025-07-30.basil", 45 | }); 46 | const checkoutSession = await stripe.checkout.sessions.create({ 47 | payment_method_types: ["card"], 48 | mode: "payment", 49 | success_url: `${process.env.NEXT_PUBLIC_APP_URL}/bookings`, 50 | cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}`, 51 | metadata: { 52 | serviceId: service.id, 53 | barbershopId: service.barbershopId, 54 | userId: session.user.id, 55 | date: date.toISOString(), 56 | }, 57 | line_items: [ 58 | { 59 | price_data: { 60 | currency: "brl", 61 | unit_amount: service.priceInCents, 62 | product_data: { 63 | name: `${service.barbershop.name} - ${service.name} em ${format(date, "dd/MM/yyyy HH:mm")}`, 64 | description: service.description, 65 | images: [service.imageUrl], 66 | }, 67 | }, 68 | quantity: 1, 69 | }, 70 | ], 71 | }); 72 | return checkoutSession; 73 | }); 74 | -------------------------------------------------------------------------------- /app/_components/quick-search-buttons.tsx: -------------------------------------------------------------------------------- 1 | import { Eye, Footprints, Scissors, Sparkles, User, Waves } from "lucide-react"; 2 | import Link from "next/link"; 3 | import { PageSectionScroller } from "./ui/page"; 4 | 5 | const QuickSearchButtons = () => { 6 | return ( 7 | 8 | 12 | 13 | Cabelo 14 | 15 | 16 | 20 | 21 | Barba 22 | 23 | 24 | 28 | 29 | 30 | Acabamento 31 | 32 | 33 | 34 | 38 | 39 | 40 | Sobrancelha 41 | 42 | 43 | 44 | 48 | 49 | 50 | Pézinho 51 | 52 | 53 | 54 | 58 | 59 | 60 | Progressiva 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default QuickSearchButtons; 68 | -------------------------------------------------------------------------------- /app/bookings/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/lib/auth"; 2 | import { prisma } from "@/lib/prisma"; 3 | import { headers } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | import Header from "../_components/header"; 6 | import Footer from "../_components/footer"; 7 | import { 8 | PageContainer, 9 | PageSection, 10 | PageSectionTitle, 11 | } from "../_components/ui/page"; 12 | import BookingItem from "../_components/booking-item"; 13 | 14 | const BookingsPage = async () => { 15 | const session = await auth.api.getSession({ 16 | headers: await headers(), 17 | }); 18 | 19 | if (!session) { 20 | redirect("/"); 21 | } 22 | 23 | const bookings = await prisma.booking.findMany({ 24 | where: { 25 | userId: session.user.id, 26 | }, 27 | include: { 28 | service: true, 29 | barbershop: true, 30 | }, 31 | orderBy: { 32 | date: "desc", 33 | }, 34 | }); 35 | 36 | const now = new Date(); 37 | 38 | const confirmedBookings = bookings.filter( 39 | (booking) => !booking.cancelled && new Date(booking.date) >= now, 40 | ); 41 | 42 | const finishedBookings = bookings.filter( 43 | (booking) => booking.cancelled || new Date(booking.date) < now, 44 | ); 45 | 46 | return ( 47 |
48 |
49 |
50 | 51 |

Agendamentos

52 | 53 | {confirmedBookings.length > 0 && ( 54 | 55 | Confirmados 56 |
57 | {confirmedBookings.map((booking) => ( 58 | 59 | ))} 60 |
61 |
62 | )} 63 | 64 | {finishedBookings.length > 0 && ( 65 | 66 | Finalizados 67 |
68 | {finishedBookings.map((booking) => ( 69 | 70 | ))} 71 |
72 |
73 | )} 74 | 75 | {bookings.length === 0 && ( 76 |

77 | Você ainda não tem agendamentos. 78 |

79 | )} 80 |
81 |
82 |
83 |
84 | ); 85 | }; 86 | 87 | export default BookingsPage; 88 | -------------------------------------------------------------------------------- /app/_actions/cancel-booking.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { actionClient } from "@/lib/action-client"; 3 | import { auth } from "@/lib/auth"; 4 | import { prisma } from "@/lib/prisma"; 5 | import { returnValidationErrors } from "next-safe-action"; 6 | import { revalidatePath } from "next/cache"; 7 | import { headers } from "next/headers"; 8 | import Stripe from "stripe"; 9 | import { z } from "zod"; 10 | 11 | const inputSchema = z.object({ 12 | bookingId: z.uuid(), 13 | }); 14 | 15 | export const cancelBooking = actionClient 16 | .inputSchema(inputSchema) 17 | .action(async ({ parsedInput: { bookingId } }) => { 18 | const session = await auth.api.getSession({ 19 | headers: await headers(), 20 | }); 21 | if (!session?.user) { 22 | returnValidationErrors(inputSchema, { 23 | _errors: ["Unauthorized"], 24 | }); 25 | } 26 | 27 | const booking = await prisma.booking.findUnique({ 28 | where: { 29 | id: bookingId, 30 | }, 31 | }); 32 | 33 | if (!booking) { 34 | returnValidationErrors(inputSchema, { 35 | _errors: ["Reserva não encontrada"], 36 | }); 37 | } 38 | 39 | if (booking.userId !== session.user.id) { 40 | returnValidationErrors(inputSchema, { 41 | _errors: ["Você não tem permissão para cancelar esta reserva"], 42 | }); 43 | } 44 | 45 | if (booking.cancelled) { 46 | returnValidationErrors(inputSchema, { 47 | _errors: ["Esta reserva já foi cancelada"], 48 | }); 49 | } 50 | 51 | if (booking.date < new Date()) { 52 | returnValidationErrors(inputSchema, { 53 | _errors: ["Não é possível cancelar reservas passadas"], 54 | }); 55 | } 56 | 57 | // Process refund if booking has a stripeChargeId 58 | if (booking.stripeChargeId) { 59 | if (!process.env.STRIPE_SECRET_KEY) { 60 | returnValidationErrors(inputSchema, { 61 | _errors: ["Erro ao processar reembolso. Tente novamente."], 62 | }); 63 | } 64 | 65 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); 66 | 67 | try { 68 | await stripe.refunds.create({ 69 | charge: booking.stripeChargeId, 70 | reason: "requested_by_customer", 71 | }); 72 | } catch (error) { 73 | if (error instanceof Stripe.errors.StripeError) { 74 | console.error("Stripe refund error:", error.message); 75 | returnValidationErrors(inputSchema, { 76 | _errors: [ 77 | "Erro ao processar reembolso. Entre em contato com o suporte.", 78 | ], 79 | }); 80 | } 81 | throw error; 82 | } 83 | } 84 | 85 | const updatedBooking = await prisma.booking.update({ 86 | where: { 87 | id: bookingId, 88 | }, 89 | data: { 90 | cancelled: true, 91 | cancelledAt: new Date(), 92 | }, 93 | }); 94 | revalidatePath("/bookings"); 95 | return updatedBooking; 96 | }); 97 | -------------------------------------------------------------------------------- /app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useChat } from "@ai-sdk/react"; 4 | import { DefaultChatTransport } from "ai"; 5 | import { useState, useEffect, useRef } from "react"; 6 | import { ChevronLeft } from "lucide-react"; 7 | import Link from "next/link"; 8 | import { ChatMessage } from "./_components/chat-message"; 9 | import { ChatInput } from "./_components/chat-input"; 10 | 11 | const INITIAL_MESSAGES = [ 12 | { 13 | id: "system-welcome", 14 | role: "system" as const, 15 | parts: [ 16 | { 17 | type: "text" as const, 18 | text: "Seu assistente de agendamentos está online.", 19 | }, 20 | ], 21 | }, 22 | { 23 | id: "assistant-welcome", 24 | role: "assistant" as const, 25 | parts: [ 26 | { 27 | type: "text" as const, 28 | text: "Olá! Sou o Aparatus, seu assistente pessoal.\n\nEstou aqui para te auxiliar a agendar seu corte ou barba, encontrar as barbearias disponíveis perto de você e responder às suas dúvidas.", 29 | }, 30 | ], 31 | }, 32 | ]; 33 | 34 | export default function ChatPage() { 35 | const [message, setMessage] = useState(""); 36 | const messagesEndRef = useRef(null); 37 | const { messages, sendMessage, status } = useChat({ 38 | transport: new DefaultChatTransport({ 39 | api: "/api/chat", 40 | }), 41 | }); 42 | 43 | const scrollToBottom = () => { 44 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 45 | }; 46 | 47 | useEffect(() => { 48 | scrollToBottom(); 49 | }, [messages]); 50 | 51 | const handleSubmit = (e: React.FormEvent) => { 52 | e.preventDefault(); 53 | if (message.trim()) { 54 | sendMessage({ 55 | text: message, 56 | }); 57 | setMessage(""); 58 | } 59 | }; 60 | 61 | const isLoading = status === "streaming" || status === "submitted"; 62 | 63 | return ( 64 |
65 |
66 | 67 | 68 | 69 |

70 | Aparatus 71 |

72 |
73 |
74 | 75 |
76 | {messages.length === 0 77 | ? INITIAL_MESSAGES.map((msg) => ( 78 | 79 | )) 80 | : messages.map((msg, index) => ( 81 | 88 | ))} 89 |
90 |
91 | 92 | setMessage(e.target.value)} 95 | onSubmit={handleSubmit} 96 | isLoading={isLoading} 97 | /> 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client" 9 | output = "../generated/prisma" 10 | engineType = "binary" 11 | } 12 | 13 | datasource db { 14 | provider = "postgresql" 15 | } 16 | 17 | model Barbershop { 18 | id String @id @default(uuid()) 19 | name String 20 | address String 21 | description String 22 | imageUrl String 23 | phones String[] 24 | services BarbershopService[] 25 | bookings Booking[] 26 | } 27 | 28 | model BarbershopService { 29 | id String @id @default(uuid()) 30 | name String 31 | description String 32 | imageUrl String 33 | barbershopId String 34 | barbershop Barbershop @relation(fields: [barbershopId], references: [id]) 35 | priceInCents Int 36 | bookings Booking[] 37 | } 38 | 39 | model Booking { 40 | id String @id @default(uuid()) 41 | serviceId String 42 | service BarbershopService @relation(fields: [serviceId], references: [id]) 43 | barbershopId String 44 | barbershop Barbershop @relation(fields: [barbershopId], references: [id]) 45 | userId String 46 | user User @relation(fields: [userId], references: [id]) 47 | date DateTime @db.Timestamptz 48 | cancelled Boolean? @default(false) 49 | cancelledAt DateTime? @db.Timestamptz 50 | stripeChargeId String? 51 | } 52 | 53 | model User { 54 | id String @id 55 | name String 56 | email String 57 | emailVerified Boolean @default(false) 58 | image String? 59 | createdAt DateTime @default(now()) 60 | updatedAt DateTime @default(now()) @updatedAt 61 | sessions Session[] 62 | accounts Account[] 63 | bookings Booking[] 64 | 65 | @@unique([email]) 66 | @@map("user") 67 | } 68 | 69 | model Session { 70 | id String @id 71 | expiresAt DateTime 72 | token String 73 | createdAt DateTime @default(now()) 74 | updatedAt DateTime @updatedAt 75 | ipAddress String? 76 | userAgent String? 77 | userId String 78 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 79 | 80 | @@unique([token]) 81 | @@map("session") 82 | } 83 | 84 | model Account { 85 | id String @id 86 | accountId String 87 | providerId String 88 | userId String 89 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 90 | accessToken String? 91 | refreshToken String? 92 | idToken String? 93 | accessTokenExpiresAt DateTime? 94 | refreshTokenExpiresAt DateTime? 95 | scope String? 96 | password String? 97 | createdAt DateTime @default(now()) 98 | updatedAt DateTime @updatedAt 99 | 100 | @@map("account") 101 | } 102 | 103 | model Verification { 104 | id String @id 105 | identifier String 106 | value String 107 | expiresAt DateTime 108 | createdAt DateTime @default(now()) 109 | updatedAt DateTime @default(now()) @updatedAt 110 | 111 | @@map("verification") 112 | } 113 | -------------------------------------------------------------------------------- /app/_components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/app/_components/ui/button" 8 | 9 | function AlertDialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function AlertDialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 20 | ) 21 | } 22 | 23 | function AlertDialogPortal({ 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | ) 29 | } 30 | 31 | function AlertDialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | function AlertDialogContent({ 48 | className, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 62 | 63 | ) 64 | } 65 | 66 | function AlertDialogHeader({ 67 | className, 68 | ...props 69 | }: React.ComponentProps<"div">) { 70 | return ( 71 |
76 | ) 77 | } 78 | 79 | function AlertDialogFooter({ 80 | className, 81 | ...props 82 | }: React.ComponentProps<"div">) { 83 | return ( 84 |
92 | ) 93 | } 94 | 95 | function AlertDialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps) { 99 | return ( 100 | 105 | ) 106 | } 107 | 108 | function AlertDialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps) { 112 | return ( 113 | 118 | ) 119 | } 120 | 121 | function AlertDialogAction({ 122 | className, 123 | ...props 124 | }: React.ComponentProps) { 125 | return ( 126 | 130 | ) 131 | } 132 | 133 | function AlertDialogCancel({ 134 | className, 135 | ...props 136 | }: React.ComponentProps) { 137 | return ( 138 | 142 | ) 143 | } 144 | 145 | export { 146 | AlertDialog, 147 | AlertDialogPortal, 148 | AlertDialogOverlay, 149 | AlertDialogTrigger, 150 | AlertDialogContent, 151 | AlertDialogHeader, 152 | AlertDialogFooter, 153 | AlertDialogTitle, 154 | AlertDialogDescription, 155 | AlertDialogAction, 156 | AlertDialogCancel, 157 | } 158 | -------------------------------------------------------------------------------- /app/_components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SheetPrimitive from "@radix-ui/react-dialog"; 5 | import { XIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Sheet({ ...props }: React.ComponentProps) { 10 | return ; 11 | } 12 | 13 | function SheetTrigger({ 14 | ...props 15 | }: React.ComponentProps) { 16 | return ; 17 | } 18 | 19 | function SheetClose({ 20 | ...props 21 | }: React.ComponentProps) { 22 | return ; 23 | } 24 | 25 | function SheetPortal({ 26 | ...props 27 | }: React.ComponentProps) { 28 | return ; 29 | } 30 | 31 | function SheetOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ); 45 | } 46 | 47 | function SheetContent({ 48 | className, 49 | children, 50 | side = "right", 51 | ...props 52 | }: React.ComponentProps & { 53 | side?: "top" | "right" | "bottom" | "left"; 54 | }) { 55 | return ( 56 | 57 | 58 | 74 | {children} 75 | 76 | 77 | Close 78 | 79 | 80 | 81 | ); 82 | } 83 | 84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { 85 | return ( 86 |
94 | ); 95 | } 96 | 97 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { 98 | return ( 99 |
104 | ); 105 | } 106 | 107 | function SheetTitle({ 108 | className, 109 | ...props 110 | }: React.ComponentProps) { 111 | return ( 112 | 117 | ); 118 | } 119 | 120 | function SheetDescription({ 121 | className, 122 | ...props 123 | }: React.ComponentProps) { 124 | return ( 125 | 130 | ); 131 | } 132 | 133 | export { 134 | Sheet, 135 | SheetTrigger, 136 | SheetClose, 137 | SheetContent, 138 | SheetHeader, 139 | SheetFooter, 140 | SheetTitle, 141 | SheetDescription, 142 | }; 143 | -------------------------------------------------------------------------------- /.claude/agents/code-reviewer.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: code-reviewer 3 | description: Use este agente quando você tiver escrito um bloco lógico de código e quiser garantir que ele segue as melhores práticas e as regras específicas do projeto definidas em .cursor/rules. Exemplos:\n\n- Usuário: "Acabei de terminar de implementar o serviço de autenticação de usuário. Você pode revisar?"\n Assistente: "Vou usar o agente code-reviewer para analisar a implementação do seu serviço de autenticação e verificar se ela segue nossos padrões de projeto."\n\n- Usuário: "Aqui está meu novo endpoint de API para processar pagamentos:"\n \n Assistente: "Vou acionar o agente code-reviewer para garantir que esse endpoint de pagamento segue nossos padrões de código e as regras definidas em .cursor/rules."\n\n- Usuário: "Refatorei a lógica de conexão com o banco de dados. Você pode verificar se está tudo certo?"\n Assistente: "Vou usar o agente code-reviewer para revisar sua lógica de conexão com o banco refatorada, verificando boas práticas e conformidade com as regras do nosso projeto."\n\n- Usuário: "Acabei de finalizar a nova feature de notificações em tempo real."\n Assistente: "Perfeito! Vou usar o agente code-reviewer para revisar a implementação da sua feature de notificações com base nas diretrizes do nosso projeto." 4 | model: sonnet 5 | color: purple 6 | --- 7 | 8 | Você é um revisor de código especialista, com profundo conhecimento das melhores práticas de engenharia de software, padrões de projeto e padrões de qualidade de código. Sua principal responsabilidade é revisar o código de forma completa, garantindo aderência estrita às regras específicas do projeto definidas em `.cursor/rules`. 9 | 10 | Seu processo de revisão deve: 11 | 12 | 1. **Prioridade: Conformidade com as Regras do Projeto** 13 | - SEMPRE comece revisando o arquivo `.cursor/rules` para entender os requisitos específicos do projeto 14 | - Verifique se o código segue estritamente TODAS as regras definidas em `.cursor/rules` 15 | - Aponte QUALQUER desvio das regras do projeto como um problema crítico 16 | - Quando as regras do projeto entrarem em conflito com boas práticas gerais, as regras do projeto têm precedência 17 | 18 | 2. **Avaliação da Qualidade do Código** 19 | - Avalie legibilidade, manutenibilidade e clareza do código 20 | - Verifique o uso adequado de convenções de nomenclatura (variáveis, funções, classes) 21 | - Analise a organização e a estrutura do código 22 | - Identifique complexidade desnecessária ou code smells 23 | - Verifique consistência de formatação e estilo 24 | 25 | 3. **Verificação de Boas Práticas** 26 | - Segurança: Verifique vulnerabilidades, validação de entrada e sanitização de dados 27 | - Performance: Identifique possíveis gargalos, algoritmos ineficientes e vazamentos de memória 28 | - Tratamento de Erros: Garanta tratamento adequado de exceções e falhas graciosas 29 | - Testes: Verifique testabilidade e sugira casos de teste quando estiverem ausentes 30 | - Documentação: Cheque se há comentários e documentação adequados quando necessário 31 | 32 | 4. **Arquitetura e Design** 33 | - Avalie a aderência aos princípios SOLID 34 | - Verifique o uso apropriado de padrões de projeto 35 | - Analise a separação de responsabilidades (separation of concerns) 36 | - Verifique o gerenciamento adequado de dependências 37 | - Revise o design de APIs e interfaces 38 | 39 | 5. **Padrões Específicos da Linguagem** 40 | - Aplique convenções e idiomatismos específicos da linguagem 41 | - Verifique o uso adequado dos recursos da linguagem 42 | - Identifique padrões obsoletos ou desaconselhados 43 | 44 | **Formato de Saída:** 45 | Estruture sua revisão da seguinte forma: 46 | 47 | **✅ Pontos Fortes:** 48 | 49 | - Liste o que o código faz bem 50 | 51 | **🔴 Problemas Críticos (Devem ser Corrigidos):** 52 | 53 | - Violações de `.cursor/rules` (maior prioridade) 54 | - Vulnerabilidades de segurança 55 | - Bugs ou erros de lógica 56 | - Erros de TypeScript 57 | 58 | **🟡 Melhorias Recomendadas:** 59 | 60 | - Violações de boas práticas 61 | - Questões de performance 62 | - Problemas de manutenibilidade 63 | 64 | **💡 Sugestões e/ou detalhes (nitpicks):** 65 | 66 | - Melhorias opcionais 67 | - Abordagens alternativas 68 | 69 | **📋 Resumo:** 70 | 71 | - Avaliação geral 72 | - Ações prioritárias 73 | 74 | Seja construtivo e específico em seu feedback. Ao identificar problemas, sempre: 75 | 76 | - Explique POR QUE aquilo é um problema 77 | - Forneça um exemplo concreto de como corrigir 78 | - Faça referência a regras específicas de `.cursor/rules` quando aplicável 79 | 80 | Se o código em revisão parecer incompleto ou se você precisar de esclarecimentos sobre os requisitos, faça perguntas específicas de forma proativa antes de fornecer sua revisão. 81 | 82 | Lembre-se: seu objetivo é ajudar a melhorar a qualidade do código, garantindo ao mesmo tempo conformidade total com os padrões estabelecidos do projeto em `.cursor/rules`. 83 | 84 | ## Ferramentas 85 | 86 | - SEMPRE use o Context7 para buscar documentações e sites de APIs 87 | -------------------------------------------------------------------------------- /app/_components/sidebar-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { authClient } from "@/lib/auth-client"; 4 | import { 5 | CalendarDaysIcon, 6 | HomeIcon, 7 | LogInIcon, 8 | LogOutIcon, 9 | } from "lucide-react"; 10 | import Link from "next/link"; 11 | import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; 12 | import { Button } from "./ui/button"; 13 | import { Separator } from "./ui/separator"; 14 | import { SheetClose } from "./ui/sheet"; 15 | 16 | const SidebarMenu = () => { 17 | const { data: session } = authClient.useSession(); 18 | 19 | const handleLogin = async () => { 20 | await authClient.signIn.social({ 21 | provider: "google", 22 | }); 23 | }; 24 | 25 | const handleLogout = async () => { 26 | await authClient.signOut(); 27 | }; 28 | 29 | return ( 30 |
31 | {/* User Section */} 32 |
33 | {session?.user ? ( 34 |
35 | 36 | 37 | 38 | {session.user.name?.charAt(0).toUpperCase()} 39 | 40 | 41 |
42 |

{session.user.name}

43 |

44 | {session.user.email} 45 |

46 |
47 |
48 | ) : ( 49 |
50 |
51 |

Olá. Faça seu login!

52 |
53 | 60 |
61 | )} 62 |
63 | 64 | {/* Navigation Buttons */} 65 |
66 | 67 | 68 | 75 | 76 | 77 | 78 | 79 | 86 | 87 | 88 |
89 | 90 | 91 | 92 | {/* Category Buttons */} 93 |
94 | 98 | Barba 99 | 100 | 104 | Cabelo 105 | 106 | 110 | Acabamento 111 | 112 | 116 | Sobrancelha 117 | 118 | 122 | Pézinho 123 | 124 | 128 | Progressiva 129 | 130 |
131 | 132 | 133 | 134 | {/* Logout Button */} 135 | 136 | 146 | 147 |
148 | ); 149 | }; 150 | 151 | export default SidebarMenu; 152 | -------------------------------------------------------------------------------- /app/barbershops/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prisma"; 2 | import { notFound } from "next/navigation"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import { ChevronLeft } from "lucide-react"; 6 | import { Button } from "@/app/_components/ui/button"; 7 | import { Separator } from "@/app/_components/ui/separator"; 8 | import { ServiceItem } from "@/app/_components/service-item"; 9 | import { PhoneItem } from "@/app/_components/phone-item"; 10 | 11 | const BarbershopPage = async (props: PageProps<"/barbershops/[id]">) => { 12 | const { id } = await props.params; 13 | const barbershop = await prisma.barbershop.findUnique({ 14 | where: { 15 | id, 16 | }, 17 | include: { 18 | services: true, 19 | }, 20 | }); 21 | 22 | if (!barbershop) { 23 | notFound(); 24 | } 25 | 26 | return ( 27 |
28 | {/* Hero Section com Imagem */} 29 |
30 |
31 | {barbershop.name} 37 |
38 | 39 | {/* Botão Voltar */} 40 |
41 | 51 |
52 |
53 | 54 | {/* Container Principal */} 55 |
56 | {/* Informações da Barbearia */} 57 |
58 |
59 |
60 |
61 | {barbershop.name} 67 |
68 |

69 | {barbershop.name} 70 |

71 |
72 |
73 |
74 |

75 | {barbershop.address} 76 |

77 |
78 |
79 |
80 |
81 | 82 | {/* Divider */} 83 |
84 | 85 |
86 | 87 | {/* Sobre Nós */} 88 |
89 |
90 |

91 | SOBRE NÓS 92 |

93 |
94 |

95 | {barbershop.description} 96 |

97 |
98 | 99 | {/* Divider */} 100 |
101 | 102 |
103 | 104 | {/* Serviços */} 105 |
106 |
107 |

108 | SERVIÇOS 109 |

110 |
111 |
112 | {barbershop.services.map((service) => ( 113 | 117 | ))} 118 |
119 |
120 | 121 | {/* Divider */} 122 |
123 | 124 |
125 | 126 | {/* Contato */} 127 |
128 |
129 |

130 | CONTATO 131 |

132 |
133 |
134 | {barbershop.phones.map((phone, index) => ( 135 | 136 | ))} 137 |
138 |
139 | 140 | {/* Footer */} 141 |
142 |
143 |

144 | © 2025 Copyright Aparatus 145 |

146 |

147 | Todos os direitos reservados. 148 |

149 |
150 |
151 |
152 |
153 | ); 154 | }; 155 | 156 | export default BarbershopPage; 157 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "../generated/prisma/client"; 2 | import { PrismaPg } from "@prisma/adapter-pg"; 3 | 4 | const prisma = new PrismaClient({ 5 | adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), 6 | }); 7 | 8 | async function seedDatabase() { 9 | try { 10 | const images = [ 11 | "https://utfs.io/f/c97a2dc9-cf62-468b-a851-bfd2bdde775f-16p.png", 12 | "https://utfs.io/f/45331760-899c-4b4b-910e-e00babb6ed81-16q.png", 13 | "https://utfs.io/f/5832df58-cfd7-4b3f-b102-42b7e150ced2-16r.png", 14 | "https://utfs.io/f/7e309eaa-d722-465b-b8b6-76217404a3d3-16s.png", 15 | "https://utfs.io/f/178da6b6-6f9a-424a-be9d-a2feb476eb36-16t.png", 16 | "https://utfs.io/f/2f9278ba-3975-4026-af46-64af78864494-16u.png", 17 | "https://utfs.io/f/988646ea-dcb6-4f47-8a03-8d4586b7bc21-16v.png", 18 | "https://utfs.io/f/60f24f5c-9ed3-40ba-8c92-0cd1dcd043f9-16w.png", 19 | "https://utfs.io/f/f64f1bd4-59ce-4ee3-972d-2399937eeafc-16x.png", 20 | "https://utfs.io/f/e995db6d-df96-4658-99f5-11132fd931e1-17j.png", 21 | "https://utfs.io/f/3bcf33fc-988a-462b-8b98-b811ee2bbd71-17k.png", 22 | "https://utfs.io/f/5788be0e-2307-4bb4-b603-d9dd237950a2-17l.png", 23 | "https://utfs.io/f/6b0888f8-b69f-4be7-a13b-52d1c0c9cab2-17m.png", 24 | "https://utfs.io/f/ef45effa-415e-416d-8c4a-3221923cd10f-17n.png", 25 | "https://utfs.io/f/ef45effa-415e-416d-8c4a-3221923cd10f-17n.png", 26 | "https://utfs.io/f/a55f0f39-31a0-4819-8796-538d68cc2a0f-17o.png", 27 | "https://utfs.io/f/5c89f046-80cd-4443-89df-211de62b7c2a-17p.png", 28 | "https://utfs.io/f/23d9c4f7-8bdb-40e1-99a5-f42271b7404a-17q.png", 29 | "https://utfs.io/f/9f0847c2-d0b8-4738-a673-34ac2b9506ec-17r.png", 30 | "https://utfs.io/f/07842cfb-7b30-4fdc-accc-719618dfa1f2-17s.png", 31 | "https://utfs.io/f/0522fdaf-0357-4213-8f52-1d83c3dcb6cd-18e.png", 32 | ]; 33 | // Nomes criativos para as barbearias 34 | const creativeNames = [ 35 | "Barbearia Vintage", 36 | "Corte & Estilo", 37 | "Barba & Navalha", 38 | "The Dapper Den", 39 | "Cabelo & Cia.", 40 | "Machado & Tesoura", 41 | "Barbearia Elegance", 42 | "Aparência Impecável", 43 | "Estilo Urbano", 44 | "Estilo Clássico", 45 | ]; 46 | 47 | // Endereços fictícios para as barbearias 48 | const addresses = [ 49 | "Rua da Barbearia, 123", 50 | "Avenida dos Cortes, 456", 51 | "Praça da Barba, 789", 52 | "Travessa da Navalha, 101", 53 | "Alameda dos Estilos, 202", 54 | "Estrada do Machado, 303", 55 | "Avenida Elegante, 404", 56 | "Praça da Aparência, 505", 57 | "Rua Urbana, 606", 58 | "Avenida Clássica, 707", 59 | ]; 60 | 61 | const services = [ 62 | { 63 | name: "Corte de Cabelo", 64 | description: "Estilo personalizado com as últimas tendências.", 65 | price: 60.0, 66 | imageUrl: 67 | "https://utfs.io/f/0ddfbd26-a424-43a0-aaf3-c3f1dc6be6d1-1kgxo7.png", 68 | }, 69 | { 70 | name: "Barba", 71 | description: "Modelagem completa para destacar sua masculinidade.", 72 | price: 40.0, 73 | imageUrl: 74 | "https://utfs.io/f/e6bdffb6-24a9-455b-aba3-903c2c2b5bde-1jo6tu.png", 75 | }, 76 | { 77 | name: "Pézinho", 78 | description: "Acabamento perfeito para um visual renovado.", 79 | price: 35.0, 80 | imageUrl: 81 | "https://utfs.io/f/8a457cda-f768-411d-a737-cdb23ca6b9b5-b3pegf.png", 82 | }, 83 | { 84 | name: "Sobrancelha", 85 | description: "Expressão acentuada com modelagem precisa.", 86 | price: 20.0, 87 | imageUrl: 88 | "https://utfs.io/f/2118f76e-89e4-43e6-87c9-8f157500c333-b0ps0b.png", 89 | }, 90 | { 91 | name: "Massagem", 92 | description: "Relaxe com uma massagem revigorante.", 93 | price: 50.0, 94 | imageUrl: 95 | "https://utfs.io/f/c4919193-a675-4c47-9f21-ebd86d1c8e6a-4oen2a.png", 96 | }, 97 | { 98 | name: "Hidratação", 99 | description: "Hidratação profunda para cabelo e barba.", 100 | price: 25.0, 101 | imageUrl: 102 | "https://utfs.io/f/8a457cda-f768-411d-a737-cdb23ca6b9b5-b3pegf.png", 103 | }, 104 | ]; 105 | 106 | // Criar 10 barbearias com nomes e endereços fictícios 107 | const barbershops = []; 108 | for (let i = 0; i < 10; i++) { 109 | const name = creativeNames[i]; 110 | const address = addresses[i]; 111 | const imageUrl = images[i]; 112 | 113 | const barbershop = await prisma.barbershop.create({ 114 | data: { 115 | name, 116 | address, 117 | imageUrl: imageUrl, 118 | phones: ["(11) 99999-9999", "(11) 99999-9999"], 119 | description: 120 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac augue ullamcorper, pharetra orci mollis, auctor tellus. Phasellus pharetra erat ac libero efficitur tempus. Donec pretium convallis iaculis. Etiam eu felis sollicitudin, cursus mi vitae, iaculis magna. Nam non erat neque. In hac habitasse platea dictumst. Pellentesque molestie accumsan tellus id laoreet.", 121 | }, 122 | }); 123 | 124 | for (const service of services) { 125 | await prisma.barbershopService.create({ 126 | data: { 127 | name: service.name, 128 | description: service.description, 129 | priceInCents: service.price * 100, 130 | barbershop: { 131 | connect: { 132 | id: barbershop.id, 133 | }, 134 | }, 135 | imageUrl: service.imageUrl, 136 | }, 137 | }); 138 | } 139 | 140 | barbershops.push(barbershop); 141 | } 142 | 143 | // Fechar a conexão com o banco de dados 144 | await prisma.$disconnect(); 145 | } catch (error) { 146 | console.error("Erro ao criar as barbearias:", error); 147 | } 148 | } 149 | 150 | seedDatabase(); 151 | -------------------------------------------------------------------------------- /PLANO_THEME_TOGGLE.md: -------------------------------------------------------------------------------- 1 | # Plano de Implementação: Dark/Light Theme Toggle 2 | 3 | ## Visão Geral 4 | Implementar sistema completo de alternância entre tema claro e escuro com botão no header da aplicação. 5 | 6 | ## Situação Atual 7 | 8 | ### ✅ Já existe no projeto: 9 | - `next-themes` instalado (v0.4.6) 10 | - Variáveis CSS para dark/light theme em `app/globals.css` 11 | - shadcn/ui configurado com componentes Radix UI 12 | - Estrutura de cores usando OKLch (perceptualmente uniforme) 13 | 14 | ### ❌ Faltando: 15 | - ThemeProvider integrado no layout 16 | - Botão de toggle no header 17 | - Estilos dinâmicos baseados no tema 18 | 19 | ## Análise Técnica 20 | 21 | ### Configuração Atual de Temas 22 | **Arquivo:** `app/globals.css` 23 | - **Light theme:** Definido em `:root` (linhas 7-65) 24 | - **Dark theme:** Definido em `.dark` (linhas 67-123) 25 | - **Variáveis disponíveis:** 26 | - Background, foreground, card, popover 27 | - Primary, secondary, muted, accent 28 | - Destructive, border, input, ring 29 | - Chart colors (1-5) 30 | - Sidebar colors 31 | 32 | ### Problema Identificado 33 | **Arquivo:** `app/_components/header.tsx` (linha 18) 34 | - Background hardcoded: `bg-white` 35 | - Não responde a mudanças de tema 36 | - Precisa usar variável CSS: `bg-background` 37 | 38 | ## Etapas de Implementação 39 | 40 | ### 1. Criar Theme Provider 41 | **Arquivo novo:** `app/_providers/theme-provider.tsx` 42 | 43 | **Responsabilidades:** 44 | - Wrapper do `ThemeProvider` do `next-themes` 45 | - Configuração: 46 | - `attribute="class"` (compatível com `.dark` selector) 47 | - `defaultTheme="system"` (respeita preferência do SO) 48 | - `enableSystem={true}` 49 | - `disableTransitionOnChange={false}` (transições suaves) 50 | 51 | **Código esperado:** 52 | ```tsx 53 | "use client"; 54 | 55 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 56 | import { type ThemeProviderProps } from "next-themes/dist/types"; 57 | 58 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 59 | return {children}; 60 | } 61 | ``` 62 | 63 | ### 2. Integrar Provider no Layout 64 | **Arquivo:** `app/layout.tsx` 65 | 66 | **Modificações:** 67 | - Importar `ThemeProvider` 68 | - Envolver `children` com `` 69 | - Configurar atributos do provider 70 | - Manter `QueryProvider` existente 71 | 72 | **Estrutura esperada:** 73 | ```tsx 74 | 79 | 80 | {children} 81 | 82 | 83 | ``` 84 | 85 | ### 3. Criar Componente Theme Toggle Button 86 | **Arquivo novo:** `app/_components/theme-toggle.tsx` 87 | 88 | **Funcionalidades:** 89 | - Client component (`"use client"`) 90 | - Hook `useTheme()` do `next-themes` 91 | - Ícones do `lucide-react`: 92 | - `SunIcon` para modo claro 93 | - `MoonIcon` para modo escuro 94 | - Estilo: `Button variant="outline" size="icon"` 95 | - Lógica de toggle: light ↔ dark 96 | - Acessibilidade: aria-label apropriado 97 | 98 | **Comportamento:** 99 | - Clique alterna entre light/dark 100 | - Ícone muda conforme tema ativo 101 | - Transição suave 102 | 103 | ### 4. Adicionar Toggle ao Header 104 | **Arquivo:** `app/_components/header.tsx` 105 | 106 | **Modificações necessárias:** 107 | 108 | #### Linha 18 - Atualizar background: 109 | ```tsx 110 | // Antes: 111 | className="flex items-center justify-between bg-white px-5 py-6" 112 | 113 | // Depois: 114 | className="flex items-center justify-between bg-background px-5 py-6" 115 | ``` 116 | 117 | #### Linhas 20-25 - Adicionar botão de theme: 118 | ```tsx 119 |
120 | {/* NOVO */} 121 | 126 | {/* ... resto do código ... */} 127 |
128 | ``` 129 | 130 | **Layout visual esperado:** 131 | ``` 132 | [Logo] .................... [🌙] [💬] [☰] 133 | tema chat menu 134 | ``` 135 | 136 | ### 5. Ajustar Estilos do Header 137 | **Garantir:** 138 | - Contraste adequado em ambos os temas 139 | - Bordas usando variável `border` 140 | - Texto usando variável `foreground` 141 | - Consistência visual com resto da aplicação 142 | 143 | ## Arquivos que Serão Criados/Modificados 144 | 145 | ### Novos Arquivos (2) 146 | 1. ✨ `app/_providers/theme-provider.tsx` (~15 linhas) 147 | 2. ✨ `app/_components/theme-toggle.tsx` (~25 linhas) 148 | 149 | ### Arquivos Modificados (2) 150 | 1. ✏️ `app/layout.tsx` (+3 linhas) 151 | - Import ThemeProvider 152 | - Wrap children 153 | 154 | 2. ✏️ `app/_components/header.tsx` (+2 linhas, 1 modificação) 155 | - Import ThemeToggle 156 | - Adicionar componente 157 | - Atualizar className background 158 | 159 | ## Resultado Esperado 160 | 161 | ### Funcionalidades 162 | ✅ Botão de toggle funcional no header 163 | ✅ Posicionado entre chat e menu 164 | ✅ Alternância suave entre temas 165 | ✅ Persistência da preferência do usuário 166 | ✅ Suporte a preferência do sistema operacional 167 | ✅ Ícones dinâmicos (sol/lua) 168 | ✅ Transições suaves de cores 169 | 170 | ### UX/UI 171 | - Botão com mesmo estilo dos outros (outline, size icon) 172 | - Ícone intuitivo (sol = claro, lua = escuro) 173 | - Feedback visual imediato 174 | - Sem flash de tema incorreto no carregamento 175 | 176 | ## Dependências 177 | - ✅ `next-themes` (já instalado) 178 | - ✅ `lucide-react` (já instalado) 179 | - ✅ shadcn/ui Button (já existe) 180 | 181 | ## Considerações Técnicas 182 | 183 | ### Performance 184 | - Provider usa React Context (re-render mínimo) 185 | - `next-themes` otimizado para SSR/SSG 186 | - CSS variables evitam re-paint excessivo 187 | 188 | ### Acessibilidade 189 | - Botão com aria-label descritivo 190 | - Contraste WCAG AA em ambos os temas 191 | - Suporte a preferência de movimento reduzido 192 | 193 | ### Compatibilidade 194 | - Next.js 14+ (App Router) 195 | - React 18+ 196 | - Navegadores modernos (CSS variables) 197 | 198 | ## Teste Manual Sugerido 199 | 200 | Após implementação, testar: 201 | 1. ✓ Clique no botão alterna o tema 202 | 2. ✓ Ícone muda corretamente 203 | 3. ✓ Cores do header mudam 204 | 4. ✓ Preferência persiste após reload 205 | 5. ✓ Tema do sistema é respeitado (primeira visita) 206 | 6. ✓ Transições são suaves 207 | 7. ✓ Sem erros no console 208 | -------------------------------------------------------------------------------- /.claude/agents/qa-test-documentation-generator.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: qa-test-documentation-generator 3 | description: Use este agente quando você tiver concluído uma série de mudanças no código e precisar documentá-las para o time de QA. Exemplos específicos:\n\n\nContexto: O usuário acabou de implementar uma nova funcionalidade de autenticação de usuários.\nuser: "Implementei o sistema de login com autenticação de dois fatores"\nassistant: "Vou usar o agente qa-test-documentation-generator para gerar a documentação de mudanças e a lista de testes necessários para o QA"\n\nO usuário completou uma mudança significativa que requer documentação e testes. Use o agente para gerar automaticamente a documentação completa.\n\n\n\n\nContexto: O usuário corrigiu vários bugs e fez melhorias de performance.\nuser: "Corrigi os bugs #123, #145 e #167, além de otimizar as queries do banco de dados"\nassistant: "Vou utilizar o agente qa-test-documentation-generator para documentar essas correções e gerar os casos de teste para validação"\n\nMúltiplas mudanças foram feitas e precisam ser documentadas adequadamente para o time de QA validar.\n\n\n\n\nContexto: O usuário acabou de fazer refatoração de código.\nuser: "Refatorei o módulo de pagamentos para usar o novo gateway"\nassistant: "Vou acionar o agente qa-test-documentation-generator para criar a documentação e os cenários de teste necessários"\n\nRefatorações significativas requerem documentação detalhada e testes abrangentes para garantir que a funcionalidade permanece intacta.\n\n 4 | model: sonnet 5 | color: cyan 6 | --- 7 | 8 | Você é um Especialista em Documentação de QA e Gestão de Testes, com vasta experiência em processos de garantia de qualidade, documentação técnica e coordenação entre equipes de desenvolvimento e QA. 9 | 10 | Sua missão é analisar mudanças de código recentemente implementadas e gerar dois entregáveis essenciais: 11 | 12 | 1. Documentação clara e compreensível das mudanças realizadas 13 | 2. Lista detalhada de casos de teste necessários para validação pelo time de QA 14 | 15 | Quando receber informações sobre mudanças no código, você deve: 16 | 17 | **ANÁLISE INICIAL:** 18 | 19 | - Identificar todas as funcionalidades afetadas pelas mudanças 20 | - Determinar o escopo e impacto das alterações 21 | - Detectar possíveis efeitos colaterais ou áreas de risco 22 | - Verificar se há dependências ou integrações afetadas 23 | 24 | **DOCUMENTAÇÃO DE MUDANÇAS:** 25 | Gere uma documentação estruturada em português contendo: 26 | 27 | 1. **Resumo Executivo**: Visão geral das mudanças em linguagem clara 28 | 2. **Mudanças Detalhadas**: Para cada alteração, inclua: 29 | - Descrição da funcionalidade antes e depois 30 | - Razão/motivação da mudança 31 | - Componentes/arquivos afetados 32 | - Impacto para o usuário final 33 | 3. **Considerações Técnicas**: Detalhes relevantes sobre implementação 34 | 4. **Riscos Identificados**: Possíveis pontos de atenção 35 | 36 | **LISTA DE TESTES PARA QA:** 37 | Crie uma lista abrangente e priorizada de casos de teste incluindo: 38 | 39 | 1. **Testes Funcionais**: 40 | - Cenários de uso normal (happy path) 41 | - Cenários de validação de dados 42 | - Cenários de erro e exceções 43 | - Testes de integração com outros componentes 44 | 45 | 2. **Testes de Regressão**: 46 | - Funcionalidades existentes que podem ter sido afetadas 47 | - Fluxos críticos do sistema 48 | - Casos de uso principais 49 | 50 | 3. **Testes de Borda**: 51 | - Casos limite e valores extremos 52 | - Condições inesperadas 53 | - Situações de carga ou stress (se aplicável) 54 | 55 | 4. **Para cada caso de teste, especifique**: 56 | - **Prioridade**: (Crítica/Alta/Média/Baixa) 57 | - **Pré-condições**: Estado necessário antes do teste 58 | - **Passos**: Sequência clara de ações 59 | - **Resultado Esperado**: O que deve acontecer 60 | - **Dados de Teste**: Exemplos específicos quando relevante 61 | 62 | **FORMATO DE SAÍDA:** 63 | Estruture sua resposta em Markdown com as seguintes seções: 64 | 65 | ```markdown 66 | # Documentação de Mudanças 67 | 68 | ## Resumo Executivo 69 | 70 | [visão geral] 71 | 72 | ## Mudanças Detalhadas 73 | 74 | [detalhamento de cada mudança] 75 | 76 | ## Considerações Técnicas 77 | 78 | [detalhes técnicos relevantes] 79 | 80 | ## Riscos Identificados 81 | 82 | [pontos de atenção] 83 | 84 | --- 85 | 86 | # Plano de Testes para QA 87 | 88 | ## Testes Funcionais 89 | 90 | [lista de casos de teste funcionais] 91 | 92 | ## Testes de Regressão 93 | 94 | [lista de casos de teste de regressão] 95 | 96 | ## Testes de Borda 97 | 98 | [lista de casos de teste de borda] 99 | 100 | ## Matriz de Priorização 101 | 102 | [tabela resumida com prioridades] 103 | ``` 104 | 105 | **ARMAZENAMENTO DA DOCUMENTAÇÃO:** 106 | 107 | - Sempre gerar um **slug da tarefa** (por exemplo, a partir do título/resumo informado pelo usuário), em formato `kebab-case` e sem caracteres especiais. 108 | - Sempre salvar **dois documentos separados**: 109 | - Um arquivo para a **documentação das mudanças** 110 | - Um arquivo para a **lista de testes de QA** 111 | - Sempre salvar esses documentos no diretório `@docs/{slug-da-tarefa}`. 112 | - O agente é responsável por **definir e utilizar o mesmo slug** consistentemente para a pasta e para a identificação dos arquivos. 113 | 114 | **PRINCÍPIOS DE QUALIDADE:** 115 | 116 | - Use linguagem clara e objetiva em português brasileiro 117 | - Seja específico - evite descrições genéricas 118 | - Priorize testes baseados em risco e impacto 119 | - Assegure cobertura completa das mudanças 120 | - Considere a perspectiva do usuário final 121 | - Inclua tanto testes positivos quanto negativos 122 | - Organize a informação de forma lógica e fácil de seguir 123 | 124 | **AUTO-VERIFICAÇÃO:** 125 | Antes de finalizar, confirme que: 126 | 127 | - [ ] Todas as mudanças mencionadas estão documentadas 128 | - [ ] Os casos de teste cobrem cenários normais, de erro e de borda 129 | - [ ] As prioridades estão claramente definidas 130 | - [ ] As instruções de teste são claras e executáveis 131 | - [ ] A documentação está em português correto e profissional 132 | 133 | Se alguma informação sobre as mudanças estiver incompleta ou ambígua, solicite esclarecimentos específicos antes de gerar a documentação. Sua documentação será usada diretamente pelo time de QA, portanto deve ser completa e precisa. 134 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.claude/agents/engenheiro-react-senior.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: engenheiro-react-senior 3 | description: Use este agente proativamente quando você precisar de expertise avançada em desenvolvimento React, incluindo: arquitetura de aplicações React complexas, otimização de performance, design patterns e best practices, implementação de hooks customizados, gerenciamento de estado (Context API, Redux, Zustand), Server-Side Rendering (SSR) e Static Site Generation (SSG), integração com TypeScript, testes (Jest, React Testing Library), acessibilidade (a11y), e code reviews de componentes React. Exemplos de uso:\n\n\nContexto: O usuário está desenvolvendo um novo componente React e quer uma revisão de código após completá-lo.\nusuário: "Criei um componente de formulário com validação. Aqui está o código: [código]"\nassistente: "Vou usar a ferramenta Task para acionar o agente engenheiro-react-senior para revisar a qualidade, performance e best practices deste componente."\n\nO usuário está solicitando revisão de código React, então use o agente engenheiro-react-senior para análise detalhada.\n\n\n\n\nContexto: O usuário precisa de ajuda para arquitetar uma solução complexa em React.\nusuário: "Preciso criar um sistema de dashboard com múltiplos gráficos interativos e atualização em tempo real. Como devo estruturar isso?"\nassistente: "Vou acionar o engenheiro-react-senior para desenhar uma arquitetura robusta e escalável para este dashboard."\n\nProblema de arquitetura React requer expertise do agente especializado.\n\n\n\n\nContexto: O usuário está enfrentando problemas de performance em uma aplicação React.\nusuário: "Minha aplicação está lenta quando renderiza listas grandes. O que posso fazer?"\nassistente: "Vou usar o agente engenheiro-react-senior para analisar e propor soluções de otimização de performance."\n\nQuestão de performance React requer análise especializada do agente.\n\n 4 | model: sonnet 5 | color: blue 6 | --- 7 | 8 | Você é um Engenheiro de Software Sênior altamente especializado em React, com mais de 10 anos de experiência em desenvolvimento frontend e arquitetura de aplicações web modernas. Você possui profundo conhecimento do ecossistema React, incluindo suas APIs internas, padrões de otimização, e evolução do framework ao longo dos anos. 9 | 10 | **Suas Responsabilidades Principais:** 11 | 12 | 1. **Arquitetura e Design**: Projetar soluções escaláveis, manuteníveis e performáticas usando React. Considerar trade-offs entre diferentes abordagens e recomendar a melhor solução baseada no contexto específico. 13 | 14 | 2. **Code Review Rigoroso**: Analisar código React com olhar crítico para: 15 | - Aderência às best practices e padrões do React 16 | - Performance (memo, useMemo, useCallback, lazy loading, code splitting) 17 | - Acessibilidade (ARIA, navegação por teclado, leitores de tela) 18 | - Segurança (XSS, sanitização de dados, validação) 19 | - Manutenibilidade (separação de responsabilidades, reutilização) 20 | - Testabilidade (componentes puros, lógica isolada) 21 | 22 | 3. **Otimização de Performance**: Identificar e resolver gargalos de performance, incluindo: 23 | - Re-renderizações desnecessárias 24 | - Bundles grandes 25 | - Carregamento lento de recursos 26 | - Memory leaks 27 | - Otimização de imagens e assets 28 | 29 | 4. **Padrões e Best Practices**: Aplicar e ensinar: 30 | - Composition over inheritance 31 | - Custom hooks para lógica reutilizável 32 | - Compound components 33 | - Render props e HOCs quando apropriado 34 | - Error boundaries 35 | - Suspense e Concurrent Features 36 | 37 | **Metodologia de Trabalho:** 38 | 39 | - **Sempre em Português**: Toda comunicação, código, comentários e documentação devem ser em português brasileiro. 40 | 41 | - **Abordagem Contextual**: Antes de propor soluções, entenda completamente o contexto: requisitos do projeto, constraints, stack tecnológica, e objetivos de negócio. 42 | 43 | - **Explicações Detalhadas**: Não apenas forneça código, mas explique o raciocínio por trás das decisões, trade-offs considerados, e alternativas avaliadas. 44 | 45 | - **Código Exemplar**: Todo código que você escreve deve: 46 | - Seguir convenções de nomenclatura consistentes (camelCase para variáveis/funções, PascalCase para componentes) 47 | - Incluir TypeScript quando apropriado para type safety 48 | - Ter comentários explicativos em partes complexas 49 | - Ser autodocumentado através de nomes descritivos 50 | - Incluir tratamento de erros apropriado 51 | 52 | - **Verificação de Qualidade**: Antes de entregar qualquer solução: 53 | 1. Verifique se o código segue as best practices do React 54 | 2. Considere implicações de performance 55 | 3. Avalie acessibilidade 56 | 4. Pense em casos extremos (edge cases) 57 | 5. Verifique se é testável 58 | 59 | **Ferramentas e Tecnologias que Você Domina:** 60 | 61 | - React (versões 16.8+, incluindo hooks e concurrent features) 62 | - TypeScript para type safety 63 | - State management: Context API, Redux, Zustand, Jotai, Recoil 64 | - Frameworks: Next.js, Remix, Gatsby 65 | - Styling: CSS Modules, Styled Components, Emotion, Tailwind CSS 66 | - Testing: Jest, React Testing Library, Cypress, Playwright 67 | - Build tools: Vite, Webpack, esbuild 68 | - Performance tools: React DevTools Profiler, Lighthouse, Web Vitals 69 | 70 | **Princípios de Comunicação:** 71 | 72 | - **Seja Claro e Objetivo**: Evite jargões desnecessários, mas use terminologia técnica precisa quando apropriado. 73 | 74 | - **Seja Pedagógico**: Quando ensinar conceitos, use analogias e exemplos práticos. Ajude o desenvolvedor a entender o "porquê" além do "como". 75 | 76 | - **Seja Honesto sobre Limitações**: Se uma solução tem trade-offs ou limitações, comunique-as claramente. Se não tiver certeza sobre algo, admita e sugira como investigar. 77 | 78 | - **Busque Clarificação**: Se os requisitos não estiverem claros, faça perguntas específicas antes de propor soluções. 79 | 80 | **Formato de Resposta:** 81 | 82 | Para code reviews: 83 | 84 | 1. Resumo geral da qualidade do código 85 | 2. Pontos positivos 86 | 3. Áreas de melhoria (organizadas por prioridade: crítico, importante, sugestão) 87 | 4. Exemplos de código refatorado quando relevante 88 | 5. Recomendações de próximos passos 89 | 90 | Para soluções arquiteturais: 91 | 92 | 1. Análise do problema e requisitos 93 | 2. Opções consideradas com prós e contras 94 | 3. Solução recomendada com justificativa 95 | 4. Implementação detalhada com código 96 | 5. Considerações de manutenção e escalabilidade 97 | 98 | Para debugging: 99 | 100 | 1. Análise do problema 101 | 2. Causa raiz identificada 102 | 3. Solução com explicação 103 | 4. Prevenção de problemas similares no futuro 104 | 105 | Você é um mentor técnico que não apenas resolve problemas, mas capacita outros desenvolvedores a escrever código React de alta qualidade. 106 | -------------------------------------------------------------------------------- /.claude/agents/revisor-de-codigo.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: revisor-de-codigo 3 | description: Use este agente quando precisar revisar o código em busca de melhorias de qualidade, conformidade com TypeScript/ESLint e alinhamento com as regras específicas do projeto, e aplicar as correções necessárias. Exemplos:\n\n1. Após implementar uma nova funcionalidade:\nusuário: "Acabei de implementar o caso de uso de criação de agente"\nassistente: "Deixe-me usar o agente revisor-de-qualidade-de-codigo para revisar o código e corrigir quaisquer problemas."\n\n2. Após corrigir um bug:\nusuário: "Corrigido o bug de autenticação na rota de login"\nassistente: "Vou usar o agente revisor-de-qualidade-de-codigo para verificar a correção e limpar a implementação."\n\n3. Ao completar um trecho de código lógico:\nusuário: "Aqui está a nova implementação do UserRepository"\nassistente: "Agora vou lançar o agente revisor-de-qualidade-de-codigo para analisar e corrigir quaisquer problemas de qualidade."\n\n4. Revisão proativa durante o desenvolvimento:\nassistente: "Concluí a integração do serviço de pagamento. Deixe-me usar o agente revisor-de-qualidade-de-codigo para garantir que tudo atenda aos nossos padrões de qualidade e aplicar correções antes de prosseguir." 4 | model: sonnet 5 | color: red 6 | --- 7 | 8 | Você é um arquiteto de qualidade de código de elite com profundo conhecimento em TypeScript, Node.js e práticas modernas de desenvolvimento web. Sua missão principal é revisar o código com precisão cirúrgúrgica e aplicar todas as correções necessárias para garantir que ele atenda aos mais altos padrões de qualidade, manutenibilidade e alinhamento com as regras específicas do projeto. 9 | 10 | ## Suas Responsabilidades Principais 11 | 12 | Você analisará as alterações de código e realizará melhorias de qualidade abrangentes, focando em: 13 | 14 | 1. **Conformidade com TypeScript & ESLint**: Identificar e corrigir TODOS os erros de TypeScript, problemas de segurança de tipo e violações do ESLint. Nunca permita tipos `any` - sempre forneça definições de tipo adequadas. 15 | 16 | 2. **Aderir às Regras do Projeto**: Sempre consulte e siga as regras de todo o projeto definidas em `.cursor/rules/` e também na pasta específica do projeto `.cursor/rules/`. Justifique quaisquer desvios necessários. 17 | 18 | 3. **Integridade Arquitetural**: 19 | - Garantir que o padrão arquitetural escolhido pelo projeto (e.g., Hexagonal, Arquitetura Limpa, MVC) seja mantido. 20 | - Verificar se a lógica de negócios está devidamente isolada das preocupações de infraestrutura e framework. 21 | - Confirmar que o fluxo de dados e as regras de dependência entre as camadas são respeitados. 22 | 23 | 4. **Padrões de Qualidade de Código**: 24 | - Aplicar os princípios SOLID e práticas de Código Limpo 25 | - Eliminar a duplicação de código (DRY) 26 | - Usar nomes de variáveis descritivos (e.g., `isLoading`, `hasError`) 27 | - Garantir o uso adequado de exportações nomeadas (nunca exportações padrão) 28 | - Remover comentários desnecessários (a menos que em testes) 29 | - Verificar padrões adequados de injeção de dependência 30 | 31 | 5. **Qualidade Específica do Frontend**: 32 | - Garantir que os componentes compartilhados sejam importados da biblioteca/pacote de UI designado. 33 | - Verificar o uso adequado da biblioteca de componentes do projeto (e.g., shadcn/ui, Material-UI). 34 | - Verificar a adesão às melhores práticas para o framework de frontend em uso (e.g., React, Next.js). 35 | - Validar as convenções de CSS e estilo (e.g., Tailwind CSS). 36 | 37 | ## Sua Metodologia de Revisão 38 | 39 | 1. **Análise Contextual**: Primeiro, identifique a qual aplicação/pacote o código pertence e carregue as regras relevantes. 40 | 41 | 2. **Inspeção Sistemática**: Revise o código nesta ordem: 42 | - Erros e avisos do TypeScript/ESLint 43 | - Violações da camada arquitetural (se aplicável) 44 | - Problemas de qualidade e manutenibilidade do código 45 | - Violações de regras específicas do projeto 46 | - Preocupações de desempenho e segurança 47 | 48 | 3. **Feedback Priorizado**: Organize os achados por severidade: 49 | - **Crítico**: Erros de TypeScript, violações arquiteturais, problemas de segurança 50 | - **Alto**: Erros do ESLint, violações de regras, preocupações com a manutenibilidade 51 | - **Médio**: Melhorias no estilo do código, oportunidades de otimização 52 | - **Baixo**: Sugestões para melhorar a legibilidade 53 | 54 | 4. **Soluções Acionáveis & Estratégia de Aplicação**: 55 | - **Para Problemas Críticos e de Alta Prioridade**: Aplique diretamente as correções necessárias usando as ferramentas apropriadas. Estas são tipicamente mudanças não negociáveis, como erros de TypeScript, vulnerabilidades de segurança ou violações arquiteturais claras. 56 | - **Para Problemas de Média e Baixa Prioridade**: Proponha as mudanças como sugestões e aguarde a aprovação do usuário antes de aplicá-las. Estas são muitas vezes melhorias estilísticas, oportunidades de refatoração ou otimizações menores onde a discrição do desenvolvedor é valiosa. 57 | - Para todos os problemas, explique claramente o que está errado, por que é importante e referencie a regra específica que está sendo violada. 58 | 59 | 5. **Verificação**: Após aplicar as alterações: 60 | - Confirme que todos os problemas de TypeScript/ESLint foram resolvidos 61 | - Verifique se os padrões arquiteturais estão corretos 62 | - Garanta que todas as regras do projeto foram satisfeitas 63 | - Verifique se as melhorias não introduzem novos problemas 64 | 65 | ## Formato de Saída 66 | 67 | Estruture sua revisão da seguinte forma: 68 | 69 | ``` 70 | ## Revisão de Qualidade de Código 71 | 72 | ### Correções Aplicadas (Prioridade Crítica e Alta) 73 | - [Lista de todas as correções aplicadas automaticamente ao código, incluindo arquivo e número da linha] 74 | 75 | ### Alterações Recomendadas (Prioridade Média e Baixa) 76 | - [Lista de melhorias sugeridas para o usuário considerar, incluindo arquivo e número da linha] 77 | 78 | ### Resumo 79 | - **Correções Automáticas Aplicadas**: X 80 | - **Alterações Recomendadas**: Y 81 | - **Qualidade Geral do Código**: [Excelente/Boa/Precisa de Melhoria] 82 | ``` 83 | 84 | ## Princípios Importantes 85 | 86 | - **Seja preciso**: Referencie linhas, funções ou padrões específicos 87 | - **Seja completo**: Não ignore problemas sutis 88 | - **Seja construtivo**: Enquadre o feedback como melhorias, não como críticas 89 | - **Seja consistente**: Aplique as regras uniformemente em toda a base de código 90 | - **Seja proativo**: Sugira medidas preventivas e melhores práticas 91 | - **Busque esclarecimentos**: Se a intenção do código não estiver clara, pergunte antes de assumir 92 | 93 | Suas revisões devem deixar o código em um estado em que esteja pronto para produção, manutenível e perfeitamente alinhado com os padrões e padrões arquiteturais estabelecidos pelo projeto. Após a revisão, o código NÃO PODE TER ERROS DE TYPESCRIPT OU ESLINT. 94 | 95 | ## Ferramentas 96 | 97 | - SEMPRE use o Zen MCP (quando disponível) com o Gemini 2.5 Pro para realizar a revisão do código. 98 | - SEMPRE use o Serena MCP (quando disponível) para recuperação de código semântico e ferramentas de edição 99 | - SEMPRE use o Context7 MCP (quando disponível) para documentação atualizada sobre código de terceiros 100 | - SEMPRE use o Perplexity MCP (quando disponível) para pesquisa na web 101 | -------------------------------------------------------------------------------- /.claude/agents/commit-message-generator.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: commit-message-generator 3 | description: Use this agent when you need to create well-crafted, concise commit messages following the Conventional Commits standard. Specifically:\n\n\nContext: The user has just finished implementing a new authentication feature.\nuser: "I've added user authentication with JWT tokens, including login and logout endpoints"\nassistant: "Let me use the commit-message-generator agent to create a proper commit message for these changes."\nThe user has described code changes that need to be committed. Use the Task tool to launch the commit-message-generator agent to analyze the changes and create an appropriate conventional commit message.\n\n\n\nContext: The user has fixed a critical bug in the payment processing system.\nuser: "Fixed the bug where payments were failing when using international credit cards"\nassistant: "I'll use the commit-message-generator agent to create a proper commit message for this bug fix."\nA bug fix has been described. Use the commit-message-generator agent to create a conventional commit message with the 'fix' type and appropriate scope.\n\n\n\nContext: The user has refactored some code but hasn't explicitly asked for a commit.\nuser: "I've cleaned up the database query logic and improved performance"\nassistant: "Great work on the refactoring! Let me use the commit-message-generator agent to create a commit message for these improvements."\nEven though the user didn't explicitly request a commit, the changes described should be committed. Proactively use the commit-message-generator agent to create an appropriate message.\n 4 | model: sonnet 5 | color: yellow 6 | --- 7 | 8 | Você é um especialista em mensagens de commit Git, com foco total na especificação Conventional Commits. Seu objetivo principal é transformar mudanças de código em mensagens de commit precisas e atômicas, que maximizem a clareza e a manutenibilidade do repositório. 9 | 10 | ## Responsabilidades Centrais 11 | 12 | Você irá analisar mudanças de código e criar mensagens de commit que sejam: 13 | 14 | - **Concisas**: cada palavra deve agregar valor; elimine redundâncias 15 | - **Atômicas**: cada commit deve representar uma mudança lógica única 16 | - **Compatíveis com o padrão**: seguir estritamente a especificação Conventional Commits 1.0.0 17 | - **Informativas**: fornecer contexto suficiente para que futuros desenvolvedores entendam a mudança 18 | 19 | ## Formato de Conventional Commits 20 | 21 | Você DEVE usar exatamente esta estrutura: 22 | 23 | ``` 24 | [optional scope]: 25 | 26 | [optional body] 27 | 28 | [optional footer(s)] 29 | ``` 30 | 31 | ### Type Selection (REQUIRED) 32 | 33 | Escolha o tipo mais apropriado: 34 | 35 | - **feat**: nova funcionalidade para o usuário 36 | - **fix**: correção de bug 37 | - **docs**: mudanças apenas em documentação 38 | - **style**: mudanças de estilo de código (formatação, pontos e vírgulas, etc.) 39 | - **refactor**: mudança de código que não corrige bug nem adiciona feature 40 | - **perf**: melhorias de performance 41 | - **test**: adição ou correção de testes 42 | - **build**: alterações no sistema de build ou dependências externas 43 | - **ci**: mudanças em arquivos e scripts de CI 44 | - **chore**: outras mudanças que não modificam arquivos de src ou test 45 | - **revert**: reverte um commit anterior 46 | 47 | ### Scope (OPTIONAL) 48 | 49 | Use o escopo para especificar a área da mudança (por exemplo, `auth`, `api`, `database`, `ui`). Mantenha em minúsculas e seja conciso. 50 | 51 | ### Regras da Descrição 52 | 53 | 1. Use modo imperativo ("add" e não "added" ou "adds") 54 | 2. Não capitalize a primeira letra 55 | 3. Não use ponto final 56 | 4. Máximo de 72 caracteres 57 | 5. Seja específico e preciso 58 | 6. Foque no O QUÊ e no POR QUÊ, não no COMO 59 | 60 | ### Corpo (Body) (OPCIONAL) 61 | 62 | Inclua quando: 63 | 64 | - A descrição sozinha não fornece contexto suficiente 65 | - Você precisa explicar a motivação da mudança 66 | - A mudança tem efeitos colaterais ou implicações relevantes 67 | 68 | Formato: 69 | 70 | - Separe da descrição com uma linha em branco 71 | - Quebre as linhas em 72 caracteres 72 | - Use listas/bullets para múltiplos itens 73 | - Explique o raciocínio por trás da mudança 74 | 75 | ### Rodapé (Footer) (OPCIONAL) 76 | 77 | Use para: 78 | 79 | - **BREAKING CHANGE**: descrever mudanças quebrando compatibilidade (também adicione `!` após type/scope) 80 | - **Closes #123**: referenciar números de issues fechadas 81 | - **Refs #456**: referenciar issues relacionadas 82 | 83 | ## Framework de Tomada de Decisão 84 | 85 | 1. **Analisar as mudanças**: 86 | - Identificar o propósito principal das mudanças 87 | - Determinar se são necessários múltiplos commits para manter atomicidade 88 | - Avaliar impacto e escopo 89 | 90 | 2. **Selecionar tipo e escopo**: 91 | - Escolher o tipo mais específico que se encaixa 92 | - Adicionar escopo apenas se trouxer clareza 93 | - Usar `!` para mudanças breaking 94 | 95 | 3. **Criar a descrição**: 96 | - Começar com a informação mais importante 97 | - Usar voz ativa e modo imperativo 98 | - Ser específico sobre o que mudou 99 | - Manter abaixo de 72 caracteres 100 | 101 | 4. **Decidir se o corpo é necessário**: 102 | - Adicionar body se o "por quê" não for óbvio 103 | - Incluir contexto que ajude futuros desenvolvedores 104 | - Omitir o body se a descrição já for autoexplicativa 105 | 106 | 5. **Adicionar rodapé, se necessário**: 107 | - Sempre documentar breaking changes 108 | - Referenciar issues relevantes 109 | - Registrar qualquer metadado importante 110 | 111 | ## Padrões de Qualidade 112 | 113 | - **Clareza**: qualquer pessoa lendo o commit deve entender a mudança rapidamente 114 | - **Rastreabilidade**: use palavras-chave que facilitem encontrar o commit 115 | - **Contexto**: forneça informação suficiente sem ser prolixo 116 | - **Consistência**: mantenha estilo uniforme em todos os commits 117 | 118 | ## Casos Especiais e Diretrizes 119 | 120 | - **Múltiplas mudanças**: sugira dividir em commits separados se as mudanças tiverem propósitos diferentes 121 | - **Mudanças pouco claras**: faça perguntas de esclarecimento sobre intenção e impacto 122 | - **Breaking changes**: SEMPRE destaque fortemente com `!` e rodapé 123 | - **Reverts**: use o tipo `revert:` e referencie o commit revertido 124 | - **Dependências**: use o tipo `build:` e mencione os pacotes específicos 125 | 126 | ## Formato de Saída 127 | 128 | Forneça: 129 | 130 | 1. A mensagem de commit completa e pronta para uso 131 | 2. Breve explicação das escolhas de tipo/escopo (se solicitado) 132 | 3. Sugestões de divisão em múltiplos commits (se aplicável) 133 | 134 | ## Exemplos 135 | 136 | Bom: 137 | 138 | ``` 139 | feat(auth): adicionar mecanismo de renovação de token JWT 140 | 141 | Implementa renovação automática de token para melhorar a experiência 142 | do usuário, evitando logouts inesperados. Os tokens são renovados 5 143 | minutos antes da expiração. 144 | 145 | Closes #234 146 | ``` 147 | 148 | Ruim: 149 | 150 | ``` 151 | Updated the authentication system with some improvements and fixed a few bugs. 152 | ``` 153 | 154 | Lembre-se: seus commits são documentação. Eles contam a história da evolução do código. Faça cada mensagem valer a pena. 155 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | @source "../node_modules/streamdown/dist/index.js"; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | :root { 8 | --background: oklch(0.994 0 0); 9 | --foreground: oklch(0.2178 0 0); 10 | --card: oklch(1 0 0); 11 | --card-foreground: oklch(0.2178 0 0); 12 | --popover: oklch(1 0 0); 13 | --popover-foreground: oklch(0.2178 0 0); 14 | --primary: oklch(0.433 0.0745 149.7347); 15 | --primary-foreground: oklch(1 0 0); 16 | --secondary: oklch(0.9067 0 0); 17 | --secondary-foreground: oklch(0.2178 0 0); 18 | --muted: oklch(0.9551 0 0); 19 | --muted-foreground: oklch(0.5555 0 0); 20 | --accent: oklch(0.9454 0.015 151.7659); 21 | --accent-foreground: oklch(0.2178 0 0); 22 | --destructive: oklch(0.53 0.1547 24.056); 23 | --destructive-foreground: oklch(1 0 0); 24 | --border: oklch(0.8699 0 0); 25 | --input: oklch(1 0 0); 26 | --ring: oklch(0.433 0.0745 149.7347); 27 | --chart-1: oklch(0.433 0.0745 149.7347); 28 | --chart-2: oklch(0.6402 0.069 155.0903); 29 | --chart-3: oklch(0.7641 0.0506 150.5513); 30 | --chart-4: oklch(0.8883 0.0287 151.8825); 31 | --chart-5: oklch(0.9712 0.0075 151.8901); 32 | --sidebar: oklch(0.9791 0 0); 33 | --sidebar-foreground: oklch(0.2178 0 0); 34 | --sidebar-primary: oklch(0.433 0.0745 149.7347); 35 | --sidebar-primary-foreground: oklch(1 0 0); 36 | --sidebar-accent: oklch(0.9454 0.015 151.7659); 37 | --sidebar-accent-foreground: oklch(0.2178 0 0); 38 | --sidebar-border: oklch(0.9067 0 0); 39 | --sidebar-ring: oklch(0.433 0.0745 149.7347); 40 | --font-sans: Inter, sans-serif; 41 | --font-serif: Georgia, serif; 42 | --font-mono: JetBrains Mono, monospace; 43 | --radius: 0.5rem; 44 | --shadow-x: 0; 45 | --shadow-y: 0.125rem; 46 | --shadow-blur: 0.25rem; 47 | --shadow-spread: 0; 48 | --shadow-opacity: 0.1; 49 | --shadow-color: 0 0 0; 50 | --shadow-2xs: 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.05); 51 | --shadow-xs: 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.05); 52 | --shadow-sm: 53 | 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.1), 0 1px 2px -1px hsl(0 0 0 / 0.1); 54 | --shadow: 55 | 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.1), 0 1px 2px -1px hsl(0 0 0 / 0.1); 56 | --shadow-md: 57 | 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.1), 0 2px 4px -1px hsl(0 0 0 / 0.1); 58 | --shadow-lg: 59 | 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.1), 0 4px 6px -1px hsl(0 0 0 / 0.1); 60 | --shadow-xl: 61 | 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.1), 0 8px 10px -1px hsl(0 0 0 / 0.1); 62 | --shadow-2xl: 0 0.125rem 0.25rem 0 hsl(0 0 0 / 0.25); 63 | --tracking-normal: 0; 64 | --spacing: 0.25rem; 65 | } 66 | 67 | .dark { 68 | --background: oklch(0.2178 0 0); 69 | --foreground: oklch(0.994 0 0); 70 | --card: oklch(0.2686 0 0); 71 | --card-foreground: oklch(0.994 0 0); 72 | --popover: oklch(0.2686 0 0); 73 | --popover-foreground: oklch(0.994 0 0); 74 | --primary: oklch(0.433 0.0745 149.7347); 75 | --primary-foreground: oklch(1 0 0); 76 | --secondary: oklch(0.3211 0 0); 77 | --secondary-foreground: oklch(0.994 0 0); 78 | --muted: oklch(0.2891 0 0); 79 | --muted-foreground: oklch(0.7155 0 0); 80 | --accent: oklch(0.3338 0.0309 150.119); 81 | --accent-foreground: oklch(0.994 0 0); 82 | --destructive: oklch(0.6591 0.153 22.1703); 83 | --destructive-foreground: oklch(0.2178 0 0); 84 | --border: oklch(0.3979 0 0); 85 | --input: oklch(0.2686 0 0); 86 | --ring: oklch(0.433 0.0745 149.7347); 87 | --chart-1: oklch(0.433 0.0745 149.7347); 88 | --chart-2: oklch(0.6402 0.069 155.0903); 89 | --chart-3: oklch(0.7641 0.0506 150.5513); 90 | --chart-4: oklch(0.8883 0.0287 151.8825); 91 | --chart-5: oklch(0.9712 0.0075 151.8901); 92 | --sidebar: oklch(0.2478 0 0); 93 | --sidebar-foreground: oklch(0.994 0 0); 94 | --sidebar-primary: oklch(0.433 0.0745 149.7347); 95 | --sidebar-primary-foreground: oklch(1 0 0); 96 | --sidebar-accent: oklch(0.3338 0.0309 150.119); 97 | --sidebar-accent-foreground: oklch(0.994 0 0); 98 | --sidebar-border: oklch(0.3485 0 0); 99 | --sidebar-ring: oklch(0.433 0.0745 149.7347); 100 | --font-sans: Inter, sans-serif; 101 | --font-serif: Georgia, serif; 102 | --font-mono: JetBrains Mono, monospace; 103 | --radius: 0.5rem; 104 | --shadow-x: 0; 105 | --shadow-y: 0.25rem; 106 | --shadow-blur: 0.5rem; 107 | --shadow-spread: 0; 108 | --shadow-opacity: 0.4; 109 | --shadow-color: 0 0 0; 110 | --shadow-2xs: 0 0.25rem 0.5rem 0 hsl(0 0 0 / 0.2); 111 | --shadow-xs: 0 0.25rem 0.5rem 0 hsl(0 0 0 / 0.2); 112 | --shadow-sm: 113 | 0 0.25rem 0.5rem 0 hsl(0 0 0 / 0.4), 0 1px 2px -1px hsl(0 0 0 / 0.4); 114 | --shadow: 115 | 0 0.25rem 0.5rem 0 hsl(0 0 0 / 0.4), 0 1px 2px -1px hsl(0 0 0 / 0.4); 116 | --shadow-md: 117 | 0 0.25rem 0.5rem 0 hsl(0 0 0 / 0.4), 0 2px 4px -1px hsl(0 0 0 / 0.4); 118 | --shadow-lg: 119 | 0 0.25rem 0.5rem 0 hsl(0 0 0 / 0.4), 0 4px 6px -1px hsl(0 0 0 / 0.4); 120 | --shadow-xl: 121 | 0 0.25rem 0.5rem 0 hsl(0 0 0 / 0.4), 0 8px 10px -1px hsl(0 0 0 / 0.4); 122 | --shadow-2xl: 0 0.25rem 0.5rem 0 hsl(0 0 0 / 1); 123 | } 124 | 125 | @theme inline { 126 | --color-background: var(--background); 127 | --color-foreground: var(--foreground); 128 | --color-card: var(--card); 129 | --color-card-foreground: var(--card-foreground); 130 | --color-popover: var(--popover); 131 | --color-popover-foreground: var(--popover-foreground); 132 | --color-primary: var(--primary); 133 | --color-primary-foreground: var(--primary-foreground); 134 | --color-secondary: var(--secondary); 135 | --color-secondary-foreground: var(--secondary-foreground); 136 | --color-muted: var(--muted); 137 | --color-muted-foreground: var(--muted-foreground); 138 | --color-accent: var(--accent); 139 | --color-accent-foreground: var(--accent-foreground); 140 | --color-destructive: var(--destructive); 141 | --color-destructive-foreground: var(--destructive-foreground); 142 | --color-border: var(--border); 143 | --color-input: var(--input); 144 | --color-ring: var(--ring); 145 | --color-chart-1: var(--chart-1); 146 | --color-chart-2: var(--chart-2); 147 | --color-chart-3: var(--chart-3); 148 | --color-chart-4: var(--chart-4); 149 | --color-chart-5: var(--chart-5); 150 | --color-sidebar: var(--sidebar); 151 | --color-sidebar-foreground: var(--sidebar-foreground); 152 | --color-sidebar-primary: var(--sidebar-primary); 153 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 154 | --color-sidebar-accent: var(--sidebar-accent); 155 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 156 | --color-sidebar-border: var(--sidebar-border); 157 | --color-sidebar-ring: var(--sidebar-ring); 158 | 159 | --font-sans: var(--font-sans); 160 | --font-mono: var(--font-mono); 161 | --font-serif: var(--font-serif); 162 | 163 | --radius-sm: calc(var(--radius) - 4px); 164 | --radius-md: calc(var(--radius) - 2px); 165 | --radius-lg: var(--radius); 166 | --radius-xl: calc(var(--radius) + 4px); 167 | 168 | --shadow-2xs: var(--shadow-2xs); 169 | --shadow-xs: var(--shadow-xs); 170 | --shadow-sm: var(--shadow-sm); 171 | --shadow: var(--shadow); 172 | --shadow-md: var(--shadow-md); 173 | --shadow-lg: var(--shadow-lg); 174 | --shadow-xl: var(--shadow-xl); 175 | --shadow-2xl: var(--shadow-2xl); 176 | 177 | --tracking-tighter: calc(var(--tracking-normal) - 0.05em); 178 | --tracking-tight: calc(var(--tracking-normal) - 0.025em); 179 | --tracking-normal: var(--tracking-normal); 180 | --tracking-wide: calc(var(--tracking-normal) + 0.025em); 181 | --tracking-wider: calc(var(--tracking-normal) + 0.05em); 182 | --tracking-widest: calc(var(--tracking-normal) + 0.1em); 183 | } 184 | 185 | @layer base { 186 | * { 187 | @apply border-border outline-ring/50; 188 | } 189 | body { 190 | @apply bg-background text-foreground; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { streamText, convertToModelMessages, tool, stepCountIs } from "ai"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import z from "zod"; 4 | import { prisma } from "@/lib/prisma"; 5 | import { getDateAvailableTimeSlots } from "@/app/_actions/get-date-available-time-slots"; 6 | import { createBooking } from "@/app/_actions/create-booking"; 7 | 8 | export const POST = async (request: Request) => { 9 | const { messages } = await request.json(); 10 | const result = streamText({ 11 | model: openai("gpt-4o-mini"), 12 | stopWhen: stepCountIs(10), 13 | system: `Você é o Agenda.ai, um assistente virtual de agendamento de barbearias. 14 | 15 | DATA ATUAL: Hoje é ${new Date().toLocaleDateString("pt-BR", { 16 | weekday: "long", 17 | year: "numeric", 18 | month: "long", 19 | day: "numeric", 20 | })} (${new Date().toISOString().split("T")[0]}) 21 | 22 | Seu objetivo é ajudar os usuários a: 23 | - Encontrar barbearias (por nome ou todas disponíveis) 24 | - Verificar disponibilidade de horários para barbearias específicas 25 | - Fornecer informações sobre serviços e preços 26 | 27 | Fluxo de atendimento: 28 | 29 | CENÁRIO 1 - Usuário menciona data/horário na primeira mensagem (ex: "quero um corte pra hoje", "preciso cortar o cabelo amanhã", "quero marcar para sexta"): 30 | 1. Use a ferramenta searchBarbershops para buscar barbearias 31 | 2. IMEDIATAMENTE após receber as barbearias, use a ferramenta getAvailableTimeSlotsForBarbershop para CADA barbearia retornada, passando a data mencionada pelo usuário 32 | 3. Apresente APENAS as barbearias que têm horários disponíveis, mostrando: 33 | - Nome da barbearia 34 | - Endereço 35 | - Serviços oferecidos com preços 36 | - Alguns horários disponíveis (4-5 opções espaçadas) 37 | 4. Quando o usuário escolher, forneça o resumo final 38 | 39 | CENÁRIO 2 - Usuário não menciona data/horário inicialmente: 40 | 1. Use a ferramenta searchBarbershops para buscar barbearias 41 | 2. Apresente as barbearias encontradas com: 42 | - Nome da barbearia 43 | - Endereço 44 | - Serviços oferecidos com preços 45 | 3. Quando o usuário demonstrar interesse em uma barbearia específica ou mencionar uma data, pergunte a data desejada (se ainda não foi informada) 46 | 4. Use a ferramenta getAvailableTimeSlotsForBarbershop passando o barbershopId e a data 47 | 5. Apresente os horários disponíveis (liste alguns horários, não todos - sugira 4-5 opções espaçadas) 48 | 49 | Resumo final (quando o usuário escolher): 50 | - Nome da barbearia 51 | - Endereço 52 | - Serviço escolhido 53 | - Data e horário escolhido 54 | - Preço 55 | 56 | Criação da reserva: 57 | - Após o usuário confirmar explicitamente a escolha (ex: "confirmo", "pode agendar", "quero esse horário"), use a ferramenta createBooking 58 | - Parâmetros necessários: 59 | * serviceId: ID do serviço escolhido 60 | * date: Data e horário no formato ISO (YYYY-MM-DDTHH:mm:ss) - exemplo: "2025-11-05T10:00:00" 61 | - Se a criação for bem-sucedida (success: true), informe ao usuário que a reserva foi confirmada com sucesso 62 | - Se houver erro (success: false), explique o erro ao usuário: 63 | * Se o erro for "User must be logged in", informe que é necessário fazer login para criar uma reserva 64 | * Para outros erros, informe que houve um problema e peça para tentar novamente 65 | 66 | Importante: 67 | - NUNCA mostre informações técnicas ao usuário (barbershopId, serviceId, formatos ISO de data, etc.) 68 | - Seja sempre educado, prestativo e use uma linguagem informal e amigável 69 | - Não liste TODOS os horários disponíveis, sugira apenas 4-5 opções espaçadas ao longo do dia 70 | - Se não houver horários disponíveis, sugira uma data alternativa 71 | - Quando o usuário mencionar "hoje", "amanhã", "depois de amanhã" ou dias da semana, calcule a data correta automaticamente`, 72 | messages: convertToModelMessages(messages), 73 | tools: { 74 | searchBarbershops: tool({ 75 | description: 76 | "Pesquisa barbearias pelo nome. Se nenhum nome é fornecido, retorna todas as barbearias.", 77 | inputSchema: z.object({ 78 | name: z.string().optional().describe("Nome opcional da barbearia"), 79 | }), 80 | execute: async ({ name }) => { 81 | if (!name?.trim()) { 82 | const barbershops = await prisma.barbershop.findMany({ 83 | include: { 84 | services: true, 85 | }, 86 | }); 87 | return barbershops.map((barbershop) => ({ 88 | barbershopId: barbershop.id, 89 | name: barbershop.name, 90 | address: barbershop.address, 91 | services: barbershop.services.map((service) => ({ 92 | id: service.id, 93 | name: service.name, 94 | price: service.priceInCents / 100, 95 | })), 96 | })); 97 | } 98 | const barbershops = await prisma.barbershop.findMany({ 99 | where: { 100 | name: { 101 | contains: name, 102 | mode: "insensitive", 103 | }, 104 | }, 105 | include: { 106 | services: true, 107 | }, 108 | }); 109 | return barbershops.map((barbershop) => ({ 110 | barbershopId: barbershop.id, 111 | name: barbershop.name, 112 | address: barbershop.address, 113 | services: barbershop.services.map((service) => ({ 114 | id: service.id, 115 | name: service.name, 116 | price: service.priceInCents / 100, 117 | })), 118 | })); 119 | }, 120 | }), 121 | getAvailableTimeSlotsForBarbershop: tool({ 122 | description: 123 | "Obtém os horários disponíveis para uma barbearia em uma data específica.", 124 | inputSchema: z.object({ 125 | barbershopId: z.string().describe("ID da barbearia"), 126 | date: z 127 | .string() 128 | .describe( 129 | "Data no formato YYYY-MM-DD para a qual deseja obter os horários disponíveis", 130 | ), 131 | }), 132 | execute: async ({ barbershopId, date }) => { 133 | const parsedDate = new Date(date); 134 | const result = await getDateAvailableTimeSlots({ 135 | barbershopId, 136 | date: parsedDate, 137 | }); 138 | if (result.serverError || result.validationErrors) { 139 | return { 140 | error: 141 | result.validationErrors?._errors?.[0] || 142 | "Erro ao buscar horários disponíveis", 143 | }; 144 | } 145 | return { 146 | barbershopId, 147 | date, 148 | availableTimeSlots: result.data, 149 | }; 150 | }, 151 | }), 152 | createBooking: tool({ 153 | description: 154 | "Cria um agendamento para um serviço em uma data específica.", 155 | inputSchema: z.object({ 156 | serviceId: z.string().describe("ID do serviço"), 157 | date: z 158 | .string() 159 | .describe("Data em ISO String para a qual deseja agendar"), 160 | }), 161 | execute: async ({ serviceId, date }) => { 162 | const parsedDate = new Date(date); 163 | const result = await createBooking({ 164 | serviceId, 165 | date: parsedDate, 166 | }); 167 | if (result.serverError || result.validationErrors) { 168 | return { 169 | error: 170 | result.validationErrors?._errors?.[0] || 171 | "Erro ao criar agendamento", 172 | }; 173 | } 174 | return { 175 | success: true, 176 | message: "Agendamento criado com sucesso", 177 | }; 178 | }, 179 | }), 180 | }, 181 | }); 182 | return result.toUIMessageStreamResponse(); 183 | }; 184 | -------------------------------------------------------------------------------- /docs/sistema-theme-toggle/documentacao-mudancas.md: -------------------------------------------------------------------------------- 1 | # Documentação de Mudanças - Sistema de Theme Toggle 2 | 3 | ## Resumo Executivo 4 | 5 | Foi implementado um sistema completo de alternância entre tema claro e escuro (dark/light mode) na aplicação, permitindo que os usuários personalizem a aparência da interface de acordo com suas preferências. O sistema oferece três modos: claro, escuro e automático (que sincroniza com a preferência do sistema operacional). A funcionalidade inclui persistência da escolha do usuário, prevenção de flash de conteúdo incorreto (FOUC), e transições suaves entre os temas. 6 | 7 | ## Mudanças Detalhadas 8 | 9 | ### 1. Criação do Theme Provider 10 | 11 | **Arquivo**: `app/_providers/theme-provider.tsx` 12 | 13 | **Descrição**: Componente wrapper que encapsula a funcionalidade do next-themes e disponibiliza o contexto de tema para toda a aplicação. 14 | 15 | **Funcionalidade**: 16 | - Wrapper client-side do NextThemesProvider 17 | - Repassa todas as props para o provider subjacente 18 | - Permite configuração centralizada do comportamento de temas 19 | 20 | **Impacto para o usuário**: Invisível diretamente, mas habilita toda a funcionalidade de alternância de temas. 21 | 22 | --- 23 | 24 | ### 2. Criação do Componente Theme Toggle 25 | 26 | **Arquivo**: `app/_components/theme-toggle.tsx` 27 | 28 | **Antes**: Não existia 29 | **Depois**: Botão interativo no header que permite alternar entre temas 30 | 31 | **Funcionalidade**: 32 | - **Estado de Montagem**: Previne problemas de hidratação renderizando um botão desabilitado até que o componente esteja montado no cliente 33 | - **Ícones Dinâmicos**: 34 | - Tema escuro ativo: exibe SunIcon (sol) indicando "clique para modo claro" 35 | - Tema claro ativo: exibe MoonIcon (lua) indicando "clique para modo escuro" 36 | - **Alternância**: Clique no botão alterna entre dark e light 37 | - **Otimização**: Utiliza useCallback para memoizar a função de alternância 38 | - **Acessibilidade**: ARIA labels descritivos que mudam de acordo com o tema ativo 39 | 40 | **Comportamento**: 41 | 1. Ao carregar a página, o botão aparece brevemente desabilitado 42 | 2. Após hidratação, o botão se torna interativo 43 | 3. Clique alterna o tema e persiste a escolha no localStorage 44 | 4. O ícone muda imediatamente após o clique 45 | 46 | **Impacto para o usuário**: Controle visual e intuitivo para mudar o tema da aplicação. 47 | 48 | --- 49 | 50 | ### 3. Integração no Layout Raiz 51 | 52 | **Arquivo**: `app/layout.tsx` 53 | 54 | **Mudanças**: 55 | - Importação do ThemeProvider 56 | - Envolvimento da aplicação com ThemeProvider 57 | - Configuração do provider com os seguintes parâmetros: 58 | - `attribute="class"`: Aplica o tema através de classe CSS no elemento HTML 59 | - `defaultTheme="system"`: Usa a preferência do SO por padrão 60 | - `enableSystem={true}`: Permite detecção automática do tema do sistema 61 | - `disableTransitionOnChange`: Remove animações CSS durante a mudança para evitar transições bruscas 62 | - Adição do atributo `suppressHydrationWarning` no elemento `` para evitar avisos de hidratação causados pela classe de tema 63 | 64 | **Razão**: Necessário para disponibilizar o contexto de tema para todos os componentes da aplicação. 65 | 66 | **Impacto para o usuário**: Na primeira visita, a aplicação respeita automaticamente a preferência de tema do sistema operacional. 67 | 68 | --- 69 | 70 | ### 4. Adição do Botão no Header 71 | 72 | **Arquivo**: `app/_components/header.tsx` 73 | 74 | **Mudanças**: 75 | - Alteração de `bg-white` para `bg-background` na classe do header 76 | - Importação do componente ThemeToggle 77 | - Adição do componente ThemeToggle no conjunto de botões do header 78 | - Posicionamento: entre o logo e os botões de chat/menu 79 | 80 | **Layout anterior**: [Logo] ................... [Chat] [Menu] 81 | **Layout atual**: [Logo] ............ [Theme] [Chat] [Menu] 82 | 83 | **Razão**: 84 | - `bg-background` é uma variável CSS do Tailwind que se adapta automaticamente ao tema ativo 85 | - `bg-white` seria sempre branco, independente do tema 86 | 87 | **Impacto para o usuário**: 88 | - Botão de alternância visível e acessível em todas as páginas 89 | - Header adapta sua cor de fundo de acordo com o tema ativo 90 | - Consistência visual em ambos os temas 91 | 92 | --- 93 | 94 | ## Considerações Técnicas 95 | 96 | ### Dependências Adicionadas 97 | - **next-themes** v0.4.6: Biblioteca otimizada para gerenciamento de temas em Next.js 98 | 99 | ### Padrões de Implementação 100 | 1. **Client-Side Only**: Todos os componentes relacionados a tema são "use client" devido à necessidade de acesso ao localStorage e interação do usuário 101 | 2. **Prevenção de Hidratação Incorreta**: Estado `mounted` garante que o conteúdo renderizado no servidor seja idêntico ao do cliente inicial 102 | 3. **Performance**: Uso de useCallback para evitar re-renderizações desnecessárias 103 | 4. **Acessibilidade**: ARIA labels dinâmicos para leitores de tela 104 | 105 | ### Fluxo de Dados 106 | 1. ThemeProvider lê a preferência salva no localStorage ou do sistema 107 | 2. Contexto de tema é disponibilizado via hook useTheme 108 | 3. ThemeToggle consome o contexto e renderiza o estado atual 109 | 4. Mudanças são propagadas para todos os componentes consumidores 110 | 5. Preferência é persistida automaticamente no localStorage 111 | 112 | ### CSS e Styling 113 | - A aplicação deve ter variáveis CSS definidas para ambos os temas 114 | - Tailwind CSS com modo dark configurado via classe (class strategy) 115 | - Componentes UI do shadcn/ui já suportam dark mode nativamente 116 | 117 | --- 118 | 119 | ## Riscos Identificados 120 | 121 | ### 1. Flash of Unstyled Content (FOUC) 122 | **Risco**: Breve exibição do tema errado ao carregar a página 123 | **Mitigação**: 124 | - next-themes injeta script inline no head 125 | - suppressHydrationWarning no elemento html 126 | - disableTransitionOnChange evita animações no primeiro render 127 | 128 | **Severidade**: Baixa (mitigação implementada) 129 | 130 | --- 131 | 132 | ### 2. Compatibilidade com Navegadores Antigos 133 | **Risco**: Browsers que não suportam localStorage ou preferência de tema do sistema 134 | **Impacto**: Funcionalidade degradada graciosamente para tema padrão 135 | **Severidade**: Baixa (graceful degradation) 136 | 137 | --- 138 | 139 | ### 3. Sobrescrita de Estilos Personalizados 140 | **Risco**: Componentes com estilos inline ou classes específicas podem não respeitar o tema 141 | **Ação Recomendada**: Revisar componentes existentes para garantir uso de variáveis CSS do tema 142 | **Severidade**: Média 143 | 144 | --- 145 | 146 | ### 4. Performance em Dispositivos Lentos 147 | **Risco**: Delay na exibição do botão de tema (mounted state) 148 | **Impacto**: Usuário pode ver botão desabilitado por alguns milissegundos 149 | **Severidade**: Muito Baixa (comportamento esperado) 150 | 151 | --- 152 | 153 | ### 5. Testes E2E e Screenshots 154 | **Risco**: Testes automatizados que dependem de cores específicas podem falhar 155 | **Ação Recomendada**: Configurar tema fixo nos ambientes de teste ou atualizar asserções 156 | **Severidade**: Baixa (impacto apenas em testes) 157 | 158 | --- 159 | 160 | ## Impacto em Outras Funcionalidades 161 | 162 | ### Funcionalidades Não Afetadas 163 | - Sistema de roteamento 164 | - QueryProvider e React Query 165 | - Componentes de formulário 166 | - Sheet/Dialog/Modal 167 | - Sistema de notificações (Toaster) 168 | - Autenticação e autorização 169 | - API routes 170 | 171 | ### Funcionalidades Potencialmente Impactadas 172 | - **Header**: Mudança no background (testado e funcionando) 173 | - **Componentes UI**: Devem adaptar cores automaticamente (shadcn/ui tem suporte nativo) 174 | - **Imagens e Logos**: Podem necessitar versões específicas para cada tema 175 | - **Gráficos e Charts**: Podem precisar de cores adaptativas 176 | 177 | --- 178 | 179 | ## Próximos Passos Recomendados 180 | 181 | 1. **Auditoria de Acessibilidade**: Validar contraste de cores em ambos os temas 182 | 2. **Revisão de Imagens**: Verificar se logos/imagens ficam legíveis em ambos os temas 183 | 3. **Documentação de Usuário**: Criar guia explicando a funcionalidade 184 | 4. **Analytics**: Implementar tracking para entender preferências dos usuários 185 | 5. **Refinamento**: Considerar adicionar mais opções de tema (ex: tema de alto contraste) 186 | 187 | --- 188 | 189 | ## Referências Técnicas 190 | 191 | - **Biblioteca**: [next-themes](https://github.com/pacocoursey/next-themes) 192 | - **Documentação Next.js**: [Dark Mode](https://nextjs.org/docs/app/building-your-application/styling/css-modules#dark-mode) 193 | - **Tailwind Dark Mode**: [Dark Mode Guide](https://tailwindcss.com/docs/dark-mode) 194 | - **shadcn/ui Theming**: [Theming Documentation](https://ui.shadcn.com/docs/theming) 195 | -------------------------------------------------------------------------------- /app/_components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { 5 | ChevronDownIcon, 6 | ChevronLeftIcon, 7 | ChevronRightIcon, 8 | } from "lucide-react" 9 | import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" 10 | 11 | import { cn } from "@/lib/utils" 12 | import { Button, buttonVariants } from "@/app/_components/ui/button" 13 | 14 | function Calendar({ 15 | className, 16 | classNames, 17 | showOutsideDays = true, 18 | captionLayout = "label", 19 | buttonVariant = "ghost", 20 | formatters, 21 | components, 22 | ...props 23 | }: React.ComponentProps & { 24 | buttonVariant?: React.ComponentProps["variant"] 25 | }) { 26 | const defaultClassNames = getDefaultClassNames() 27 | 28 | return ( 29 | svg]:rotate-180`, 34 | String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, 35 | className 36 | )} 37 | captionLayout={captionLayout} 38 | formatters={{ 39 | formatMonthDropdown: (date) => 40 | date.toLocaleString("default", { month: "short" }), 41 | ...formatters, 42 | }} 43 | classNames={{ 44 | root: cn("w-fit", defaultClassNames.root), 45 | months: cn( 46 | "flex gap-4 flex-col md:flex-row relative", 47 | defaultClassNames.months 48 | ), 49 | month: cn("flex flex-col w-full gap-4", defaultClassNames.month), 50 | nav: cn( 51 | "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", 52 | defaultClassNames.nav 53 | ), 54 | button_previous: cn( 55 | buttonVariants({ variant: buttonVariant }), 56 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 57 | defaultClassNames.button_previous 58 | ), 59 | button_next: cn( 60 | buttonVariants({ variant: buttonVariant }), 61 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 62 | defaultClassNames.button_next 63 | ), 64 | month_caption: cn( 65 | "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", 66 | defaultClassNames.month_caption 67 | ), 68 | dropdowns: cn( 69 | "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", 70 | defaultClassNames.dropdowns 71 | ), 72 | dropdown_root: cn( 73 | "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", 74 | defaultClassNames.dropdown_root 75 | ), 76 | dropdown: cn( 77 | "absolute bg-popover inset-0 opacity-0", 78 | defaultClassNames.dropdown 79 | ), 80 | caption_label: cn( 81 | "select-none font-medium", 82 | captionLayout === "label" 83 | ? "text-sm" 84 | : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", 85 | defaultClassNames.caption_label 86 | ), 87 | table: "w-full border-collapse", 88 | weekdays: cn("flex", defaultClassNames.weekdays), 89 | weekday: cn( 90 | "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", 91 | defaultClassNames.weekday 92 | ), 93 | week: cn("flex w-full mt-2", defaultClassNames.week), 94 | week_number_header: cn( 95 | "select-none w-(--cell-size)", 96 | defaultClassNames.week_number_header 97 | ), 98 | week_number: cn( 99 | "text-[0.8rem] select-none text-muted-foreground", 100 | defaultClassNames.week_number 101 | ), 102 | day: cn( 103 | "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", 104 | props.showWeekNumber 105 | ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" 106 | : "[&:first-child[data-selected=true]_button]:rounded-l-md", 107 | defaultClassNames.day 108 | ), 109 | range_start: cn( 110 | "rounded-l-md bg-accent", 111 | defaultClassNames.range_start 112 | ), 113 | range_middle: cn("rounded-none", defaultClassNames.range_middle), 114 | range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), 115 | today: cn( 116 | "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", 117 | defaultClassNames.today 118 | ), 119 | outside: cn( 120 | "text-muted-foreground aria-selected:text-muted-foreground", 121 | defaultClassNames.outside 122 | ), 123 | disabled: cn( 124 | "text-muted-foreground opacity-50", 125 | defaultClassNames.disabled 126 | ), 127 | hidden: cn("invisible", defaultClassNames.hidden), 128 | ...classNames, 129 | }} 130 | components={{ 131 | Root: ({ className, rootRef, ...props }) => { 132 | return ( 133 |
139 | ) 140 | }, 141 | Chevron: ({ className, orientation, ...props }) => { 142 | if (orientation === "left") { 143 | return ( 144 | 145 | ) 146 | } 147 | 148 | if (orientation === "right") { 149 | return ( 150 | 154 | ) 155 | } 156 | 157 | return ( 158 | 159 | ) 160 | }, 161 | DayButton: CalendarDayButton, 162 | WeekNumber: ({ children, ...props }) => { 163 | return ( 164 | 165 |
166 | {children} 167 |
168 | 169 | ) 170 | }, 171 | ...components, 172 | }} 173 | {...props} 174 | /> 175 | ) 176 | } 177 | 178 | function CalendarDayButton({ 179 | className, 180 | day, 181 | modifiers, 182 | ...props 183 | }: React.ComponentProps) { 184 | const defaultClassNames = getDefaultClassNames() 185 | 186 | const ref = React.useRef(null) 187 | React.useEffect(() => { 188 | if (modifiers.focused) ref.current?.focus() 189 | }, [modifiers.focused]) 190 | 191 | return ( 192 | 154 | 155 |
156 |
157 |
158 |
159 | 160 | 161 |
162 | 163 | Fazer Reserva 164 | 165 | 166 |
167 | 175 |
176 | 177 | {selectedDate && ( 178 | <> 179 | 180 | 181 |
182 | {availableTimeSlots?.data?.map((time) => ( 183 | 191 | ))} 192 |
193 | 194 | 195 | 196 |
197 |
198 |
199 |

200 | {service.name} 201 |

202 |

203 | R${priceInReaisInteger},00 204 |

205 |
206 | 207 |
208 |

Data

209 |

{formattedDate}

210 |
211 | 212 | {selectedTime && ( 213 |
214 |

Horário

215 |

{selectedTime}

216 |
217 | )} 218 | 219 |
220 |

Barbearia

221 |

{service.barbershop.name}

222 |
223 |
224 |
225 | 226 |
227 | 234 |
235 | 236 | )} 237 |
238 |
239 | 240 | ); 241 | } 242 | -------------------------------------------------------------------------------- /app/_components/booking-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Badge } from "./ui/badge"; 4 | import { Card } from "./ui/card"; 5 | import { Avatar } from "./ui/avatar"; 6 | import { AvatarImage } from "@radix-ui/react-avatar"; 7 | import { 8 | Sheet, 9 | SheetContent, 10 | SheetHeader, 11 | SheetTitle, 12 | SheetTrigger, 13 | } from "./ui/sheet"; 14 | import { useState } from "react"; 15 | import { PhoneItem } from "./phone-item"; 16 | import Image from "next/image"; 17 | import { Button } from "./ui/button"; 18 | import { useAction } from "next-safe-action/hooks"; 19 | import { cancelBooking } from "../_actions/cancel-booking"; 20 | import { toast } from "sonner"; 21 | import { X } from "lucide-react"; 22 | import { Separator } from "./ui/separator"; 23 | import { 24 | AlertDialog, 25 | AlertDialogAction, 26 | AlertDialogCancel, 27 | AlertDialogContent, 28 | AlertDialogDescription, 29 | AlertDialogFooter, 30 | AlertDialogHeader, 31 | AlertDialogTitle, 32 | AlertDialogTrigger, 33 | } from "./ui/alert-dialog"; 34 | import { Booking } from "@/generated/prisma/client"; 35 | 36 | interface BookingItemProps { 37 | booking: { 38 | id: string; 39 | date: Date; 40 | cancelled: boolean | null; 41 | service: { 42 | name: string; 43 | priceInCents: number; 44 | }; 45 | barbershop: { 46 | id: string; 47 | name: string; 48 | imageUrl: string; 49 | address: string; 50 | phones: string[]; 51 | }; 52 | }; 53 | } 54 | 55 | const getStatus = (booking: Pick) => { 56 | if (booking.cancelled) { 57 | return "cancelled"; 58 | } 59 | const date = new Date(booking.date); 60 | const now = new Date(); 61 | return date >= now ? "confirmed" : "finished"; 62 | }; 63 | 64 | const BookingItem = ({ booking }: BookingItemProps) => { 65 | const [sheetIsOpen, setSheetIsOpen] = useState(false); 66 | 67 | const { execute: executeCancelBooking } = useAction(cancelBooking, { 68 | onSuccess: () => { 69 | toast.success("Reserva cancelada com sucesso!"); 70 | setSheetIsOpen(false); 71 | }, 72 | onError: ({ error }) => { 73 | toast.error( 74 | error.serverError || "Erro ao cancelar reserva. Tente novamente.", 75 | ); 76 | }, 77 | }); 78 | 79 | const handleCancelBooking = () => { 80 | executeCancelBooking({ bookingId: booking.id }); 81 | }; 82 | 83 | const status = getStatus(booking); 84 | const isConfirmed = status === "confirmed"; 85 | 86 | return ( 87 | 88 | 89 | 90 |
91 | 98 | {status === "confirmed" 99 | ? "Confirmado" 100 | : status === "finished" 101 | ? "Finalizado" 102 | : "Cancelado"} 103 | 104 | 105 |
106 |

{booking.service.name}

107 |
108 | 109 | 110 | 111 |

{booking.barbershop.name}

112 |
113 |
114 |
115 | 116 |
117 |

118 | {booking.date.toLocaleDateString("pt-BR", { month: "long" })} 119 |

120 |

121 | {booking.date.toLocaleDateString("pt-BR", { day: "2-digit" })} 122 |

123 |

124 | {booking.date.toLocaleTimeString("pt-BR", { 125 | hour: "2-digit", 126 | minute: "2-digit", 127 | })} 128 |

129 |
130 |
131 |
132 | 133 | 134 | 135 |
136 | Informações da Reserva 137 |
138 |
139 | 140 |
141 | {/* Imagem do mapa com informações da barbearia */} 142 |
143 | Localização da barbearia 149 |
150 |
151 | 152 | 153 | 154 |
155 |

{booking.barbershop.name}

156 |

157 | {booking.barbershop.address} 158 |

159 |
160 |
161 |
162 |
163 | 164 | {/* Badge de status */} 165 | 172 | {isConfirmed ? "Confirmado" : "Finalizado"} 173 | 174 | 175 | {/* Card com informações da reserva */} 176 |
177 |
178 |

{booking.service.name}

179 |

180 | {Intl.NumberFormat("pt-BR", { 181 | style: "currency", 182 | currency: "BRL", 183 | }).format(booking.service.priceInCents / 100)} 184 |

185 |
186 |
187 |

Data

188 |

189 | {booking.date.toLocaleDateString("pt-BR", { 190 | day: "2-digit", 191 | month: "long", 192 | })} 193 |

194 |
195 |
196 |

Horário

197 |

198 | {booking.date.toLocaleTimeString("pt-BR", { 199 | hour: "2-digit", 200 | minute: "2-digit", 201 | })} 202 |

203 |
204 |
205 |

Barbearia

206 |

{booking.barbershop.name}

207 |
208 |
209 | 210 | {/* Telefones */} 211 | {booking.barbershop.phones.length > 0 && ( 212 |
213 | {booking.barbershop.phones.map((phone) => ( 214 | 215 | ))} 216 |
217 | )} 218 |
219 | 220 | {/* Botões no rodapé */} 221 |
222 | 229 | {isConfirmed && ( 230 | 231 | 232 | 235 | 236 | 237 | 238 | Cancelar reserva 239 | 240 | Tem certeza que deseja cancelar esta reserva? Esta ação não 241 | pode ser desfeita. 242 | 243 | 244 | 245 | Voltar 246 | 250 | Confirmar 251 | 252 | 253 | 254 | 255 | )} 256 |
257 |
258 |
259 | ); 260 | }; 261 | 262 | export default BookingItem; 263 | --------------------------------------------------------------------------------