From ed8d2809956796cd9e943be2d3de54650defcec1 Mon Sep 17 00:00:00 2001 From: Jose Selesan Date: Fri, 28 Aug 2026 17:03:53 -0300 Subject: [PATCH] feat: initialize monorepo structure with Bun and Turborepo - Add package.json for the root with workspace configuration and scripts - Create @gruperly/config package with TypeScript configuration - Establish shared package @gruperly/shared with Zod schemas for validation - Implement group, student, and payment schemas using Zod - Set up TypeScript configuration for shared package - Document stack architecture and technical specifications in stack.md - Configure Turborepo with build, dev, lint, and typecheck tasks --- .gitignore | 15 + README.md | 36 +- apps/api/.env.example | 6 + apps/api/package.json | 28 + apps/api/prisma/schema.prisma | 171 ++++++ apps/api/src/db/client.ts | 3 + apps/api/src/db/index.ts | 3 + apps/api/src/index.ts | 30 + apps/api/src/routes/auth.ts | 5 + apps/api/src/routes/groups.ts | 5 + apps/api/src/routes/payments.ts | 5 + apps/api/src/routes/students.ts | 5 + apps/api/src/routes/waitlist.ts | 5 + apps/api/tsconfig.json | 11 + apps/web/index.html | 12 + apps/web/package.json | 34 ++ apps/web/src/components/brand/Logo.tsx | 23 + apps/web/src/components/brand/LogoIcon.tsx | 39 ++ apps/web/src/components/brand/index.ts | 2 + apps/web/src/components/layout/BottomNav.tsx | 33 + apps/web/src/components/layout/RootLayout.tsx | 44 ++ apps/web/src/components/layout/Sidebar.tsx | 38 ++ apps/web/src/components/layout/index.ts | 4 + apps/web/src/components/layout/nav-items.ts | 14 + apps/web/src/components/ui/avatar.tsx | 40 ++ apps/web/src/components/ui/badge.tsx | 33 + apps/web/src/components/ui/button.tsx | 42 ++ apps/web/src/components/ui/index.ts | 3 + apps/web/src/index.css | 39 ++ apps/web/src/lib/utils.ts | 6 + apps/web/src/main.tsx | 11 + apps/web/src/router.tsx | 49 ++ apps/web/src/routes/groups.tsx | 8 + apps/web/src/routes/home.tsx | 8 + apps/web/src/routes/payments.tsx | 8 + apps/web/src/routes/settings.tsx | 8 + apps/web/tsconfig.json | 12 + apps/web/vite.config.ts | 10 + bun.lock | 570 ++++++++++++++++++ package.json | 24 + packages/config/package.json | 6 + packages/config/tsconfig.base.json | 17 + packages/shared/package.json | 22 + packages/shared/src/index.ts | 3 + packages/shared/src/schemas/group.schema.ts | 15 + packages/shared/src/schemas/payment.schema.ts | 17 + packages/shared/src/schemas/student.schema.ts | 16 + packages/shared/tsconfig.json | 10 + stack.md | 94 +++ turbo.json | 19 + 50 files changed, 1660 insertions(+), 1 deletion(-) create mode 100644 apps/api/.env.example create mode 100644 apps/api/package.json create mode 100644 apps/api/prisma/schema.prisma create mode 100644 apps/api/src/db/client.ts create mode 100644 apps/api/src/db/index.ts create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/routes/auth.ts create mode 100644 apps/api/src/routes/groups.ts create mode 100644 apps/api/src/routes/payments.ts create mode 100644 apps/api/src/routes/students.ts create mode 100644 apps/api/src/routes/waitlist.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/src/components/brand/Logo.tsx create mode 100644 apps/web/src/components/brand/LogoIcon.tsx create mode 100644 apps/web/src/components/brand/index.ts create mode 100644 apps/web/src/components/layout/BottomNav.tsx create mode 100644 apps/web/src/components/layout/RootLayout.tsx create mode 100644 apps/web/src/components/layout/Sidebar.tsx create mode 100644 apps/web/src/components/layout/index.ts create mode 100644 apps/web/src/components/layout/nav-items.ts create mode 100644 apps/web/src/components/ui/avatar.tsx create mode 100644 apps/web/src/components/ui/badge.tsx create mode 100644 apps/web/src/components/ui/button.tsx create mode 100644 apps/web/src/components/ui/index.ts create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/lib/utils.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/router.tsx create mode 100644 apps/web/src/routes/groups.tsx create mode 100644 apps/web/src/routes/home.tsx create mode 100644 apps/web/src/routes/payments.tsx create mode 100644 apps/web/src/routes/settings.tsx create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 bun.lock create mode 100644 package.json create mode 100644 packages/config/package.json create mode 100644 packages/config/tsconfig.base.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/schemas/group.schema.ts create mode 100644 packages/shared/src/schemas/payment.schema.ts create mode 100644 packages/shared/src/schemas/student.schema.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 stack.md create mode 100644 turbo.json diff --git a/.gitignore b/.gitignore index 2309cc8..0ca5cb4 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,18 @@ dist .yarn/install-state.gz .pnp.* +# Bun +.bun +bun.lockb + +# Turborepo +.turbo + +# Prisma +apps/api/prisma/dev.db +apps/api/prisma/migrations/dev/ +*.db + +# env +apps/api/.env + diff --git a/README.md b/README.md index e24577e..96431ee 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ -# gruperly +# Gruperly +Monorepo de Gruperly. Consulta [stack.md](./stack.md) para arquitectura y especificaciones. + +## Stack + +- Runtime & package manager: [Bun](https://bun.sh/) +- Monorepo: Bun Workspaces + Turborepo +- Backend (`apps/api`): Bun + Hono + Prisma + PostgreSQL + Better Auth +- Frontend (`apps/web`): React 19 + TypeScript + TanStack Router + TanStack Query + Tailwind v4 +- Shared (`packages/shared`): esquemas Zod y tipos compartidos + +## Estructura + +```text +apps/ + api/ # Backend (Bun + Hono + Prisma) + web/ # Frontend (React PWA) +packages/ + shared/ # Esquemas Zod y tipos compartidos + config/ # tsconfig base compartido +``` + +## Comandos + +```bash +bun install # Instala dependencias de todos los workspaces +bun run dev # Levanta la API y la web (vía Turborepo) +bun run db:push # Aplica el esquema de Prisma a la base de datos +bun run db:studio # Abre Prisma Studio +``` + +## Setup de base de datos + +1. Crea una base PostgreSQL y copia `apps/api/.env.example` → `apps/api/.env`, ajustando `DATABASE_URL`. +2. Ejecuta `bun run db:generate` y `bun run db:push` (o `bun run db:migrate` en desarrollo). diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..4860da3 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,6 @@ +# PostgreSQL connection string +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/gruperly?schema=public" + +# Better Auth +BETTER_AUTH_SECRET="replace-with-a-strong-secret" +BETTER_AUTH_URL="http://localhost:4000" diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..0bc6508 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,28 @@ +{ + "name": "@gruperly/api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "bun --watch src/index.ts", + "start": "bun src/index.ts", + "build": "bun build ./src/index.ts --target bun --outdir dist", + "typecheck": "tsc --noEmit", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:push": "prisma db push", + "db:studio": "prisma studio" + }, + "dependencies": { + "@gruperly/shared": "workspace:*", + "@hono/zod-validator": "^0.8.0", + "better-auth": "^1.1.0", + "hono": "^4.6.0" + }, + "devDependencies": { + "@types/bun": "^1.0.0", + "prisma": "^6.0.0", + "@prisma/client": "^6.0.0", + "typescript": "^5.7.0" + } +} diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma new file mode 100644 index 0000000..d7f9181 --- /dev/null +++ b/apps/api/prisma/schema.prisma @@ -0,0 +1,171 @@ +// Gruperly - Prisma Schema (PostgreSQL) + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// Auth (Better Auth) - required tables +model User { + id String @id @default(cuid()) + name String + email String @unique + emailVerified Boolean @default(false) + image String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + accounts Account[] + sessions Session[] + groupsMember Member[] + groupsOwner Group[] @relation("OwnerGroups") + waitlist WaitlistEntry[] +} + +model Account { + id String @id @default(cuid()) + userId String + providerId String + providerAccountId String + refreshToken String? + accessToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + idToken String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([providerId, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + userId String + token String @unique + expiresAt DateTime + ipAddress String? + userAgent String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model Verification { + id String @id @default(cuid()) + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([identifier, value]) +} + +// Domain models +model Group { + id String @id @default(cuid()) + name String + description String? + createdById String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + owner User @relation("OwnerGroups", fields: [createdById], references: [id], onDelete: Cascade) + members Member[] + students Student[] + payments Payment[] +} + +model Member { + id String @id @default(cuid()) + groupId String + userId String + role Role @default(MEMBER) // OWNER, ADMIN, MEMBER + joinedAt DateTime @default(now()) + + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([groupId, userId]) +} + +enum Role { + OWNER + ADMIN + MEMBER +} + +model Student { + id String @id @default(cuid()) + groupId String + fullName String + email String? + phone String? + guardianName String? + guardianPhone String? + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + payments Payment[] + + @@index([groupId]) +} + +model Payment { + id String @id @default(cuid()) + groupId String + studentId String + amount Decimal @db.Decimal(10, 2) + currency String @default("MXN") + status PaymentStatus @default(PENDING) // PENDING, PAID, OVERDUE, CANCELLED + dueDate DateTime + paidAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) + + @@index([groupId]) + @@index([studentId]) +} + +enum PaymentStatus { + PENDING + PAID + OVERDUE + CANCELLED +} + +model WaitlistEntry { + id String @id @default(cuid()) + email String + name String? + status WaitlistStatus @default(PENDING) // PENDING, INVITED, JOINED, DECLINED + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + userId String? @unique + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + + @@unique([email]) +} + +enum WaitlistStatus { + PENDING + INVITED + JOINED + DECLINED +} diff --git a/apps/api/src/db/client.ts b/apps/api/src/db/client.ts new file mode 100644 index 0000000..6260dd0 --- /dev/null +++ b/apps/api/src/db/client.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client' + +export const prisma = new PrismaClient() diff --git a/apps/api/src/db/index.ts b/apps/api/src/db/index.ts new file mode 100644 index 0000000..8c40b21 --- /dev/null +++ b/apps/api/src/db/index.ts @@ -0,0 +1,3 @@ +import { prisma } from './client' + +export { prisma } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..e6a09c5 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,30 @@ +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { authRoutes } from './routes/auth' +import { groupsRoutes } from './routes/groups' +import { studentsRoutes } from './routes/students' +import { paymentsRoutes } from './routes/payments' +import { waitlistRoutes } from './routes/waitlist' + +const app = new Hono() + +app.use('*', cors()) + +app.get('/', (c) => c.json({ name: 'Gruperly API', status: 'ok' })) + +app.route('/auth', authRoutes) +app.route('/groups', groupsRoutes) +app.route('/students', studentsRoutes) +app.route('/payments', paymentsRoutes) +app.route('/waitlist', waitlistRoutes) + +export default app + +const port = Number(Bun.env.PORT ?? 4000) + +Bun.serve({ + port, + fetch: app.fetch, +}) + +console.log(`Gruperly API running on http://localhost:${port}`) diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..f93a954 --- /dev/null +++ b/apps/api/src/routes/auth.ts @@ -0,0 +1,5 @@ +import { Hono } from 'hono' + +export const authRoutes = new Hono() + +authRoutes.get('/session', (c) => c.json({ message: 'placeholder' })) diff --git a/apps/api/src/routes/groups.ts b/apps/api/src/routes/groups.ts new file mode 100644 index 0000000..5d0bbe3 --- /dev/null +++ b/apps/api/src/routes/groups.ts @@ -0,0 +1,5 @@ +import { Hono } from 'hono' + +export const groupsRoutes = new Hono() + +groupsRoutes.get('/', (c) => c.json([])) diff --git a/apps/api/src/routes/payments.ts b/apps/api/src/routes/payments.ts new file mode 100644 index 0000000..167e140 --- /dev/null +++ b/apps/api/src/routes/payments.ts @@ -0,0 +1,5 @@ +import { Hono } from 'hono' + +export const paymentsRoutes = new Hono() + +paymentsRoutes.get('/', (c) => c.json([])) diff --git a/apps/api/src/routes/students.ts b/apps/api/src/routes/students.ts new file mode 100644 index 0000000..d14d4dd --- /dev/null +++ b/apps/api/src/routes/students.ts @@ -0,0 +1,5 @@ +import { Hono } from 'hono' + +export const studentsRoutes = new Hono() + +studentsRoutes.get('/', (c) => c.json([])) diff --git a/apps/api/src/routes/waitlist.ts b/apps/api/src/routes/waitlist.ts new file mode 100644 index 0000000..9ad7240 --- /dev/null +++ b/apps/api/src/routes/waitlist.ts @@ -0,0 +1,5 @@ +import { Hono } from 'hono' + +export const waitlistRoutes = new Hono() + +waitlistRoutes.get('/', (c) => c.json([])) diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..70e566a --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@gruperly/config/tsconfig.base.json", + "compilerOptions": { + "moduleResolution": "bundler", + "types": ["bun"], + "paths": { + "@gruperly/shared": ["../../packages/shared/src/index.ts"] + } + }, + "include": ["src/**/*.ts"] +} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..55e9213 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + Gruperly + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..db07fdd --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,34 @@ +{ + "name": "@gruperly/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@gruperly/shared": "workspace:*", + "@hookform/resolvers": "^3.9.0", + "@tanstack/react-query": "^5.62.0", + "@tanstack/react-router": "^1.90.0", + "clsx": "^2.1.1", + "lucide-react": "^1.34.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.0", + "tailwind-merge": "^3.6.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/apps/web/src/components/brand/Logo.tsx b/apps/web/src/components/brand/Logo.tsx new file mode 100644 index 0000000..514b481 --- /dev/null +++ b/apps/web/src/components/brand/Logo.tsx @@ -0,0 +1,23 @@ +import { forwardRef, type HTMLAttributes } from 'react' +import { cn } from '../../lib/utils' + +export type LogoProps = Omit, 'children'> & { + className?: string +} + +export const Logo = forwardRef( + ({ className, ...props }, ref) => { + return ( + + gruperl + y + + ) + }, +) + +Logo.displayName = 'Logo' diff --git a/apps/web/src/components/brand/LogoIcon.tsx b/apps/web/src/components/brand/LogoIcon.tsx new file mode 100644 index 0000000..2250e30 --- /dev/null +++ b/apps/web/src/components/brand/LogoIcon.tsx @@ -0,0 +1,39 @@ +import { forwardRef, type SVGProps } from 'react' +import { cn } from '../../lib/utils' + +export type LogoIconProps = SVGProps & { + className?: string +} + +export const LogoIcon = forwardRef( + ({ className, ...props }, ref) => { + return ( + + + + + + ) + }, +) + +LogoIcon.displayName = 'LogoIcon' diff --git a/apps/web/src/components/brand/index.ts b/apps/web/src/components/brand/index.ts new file mode 100644 index 0000000..75c8da4 --- /dev/null +++ b/apps/web/src/components/brand/index.ts @@ -0,0 +1,2 @@ +export { Logo, type LogoProps } from './Logo' +export { LogoIcon, type LogoIconProps } from './LogoIcon' diff --git a/apps/web/src/components/layout/BottomNav.tsx b/apps/web/src/components/layout/BottomNav.tsx new file mode 100644 index 0000000..5cd17a0 --- /dev/null +++ b/apps/web/src/components/layout/BottomNav.tsx @@ -0,0 +1,33 @@ +import { Link, useLocation } from '@tanstack/react-router' +import { cn } from '../../lib/utils' +import { NAV_ITEMS } from './nav-items' + +export function BottomNav() { + const location = useLocation() + + return ( + + ) +} diff --git a/apps/web/src/components/layout/RootLayout.tsx b/apps/web/src/components/layout/RootLayout.tsx new file mode 100644 index 0000000..b063559 --- /dev/null +++ b/apps/web/src/components/layout/RootLayout.tsx @@ -0,0 +1,44 @@ +import { Outlet } from '@tanstack/react-router' +import { Bell } from 'lucide-react' +import { Logo, LogoIcon } from '../brand' +import { Avatar, Button } from '../ui' +import { Sidebar } from './Sidebar' +import { BottomNav } from './BottomNav' + +export function RootLayout() { + return ( +
+ + +
+
+
+ + + + +
+ + +
+
+
+ +
+ +
+ + +
+
+ ) +} diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..4c4daef --- /dev/null +++ b/apps/web/src/components/layout/Sidebar.tsx @@ -0,0 +1,38 @@ +import { Link, useLocation } from '@tanstack/react-router' +import { cn } from '../../lib/utils' +import { Logo, LogoIcon } from '../brand' +import { NAV_ITEMS } from './nav-items' + +export function Sidebar() { + const location = useLocation() + + return ( + + ) +} diff --git a/apps/web/src/components/layout/index.ts b/apps/web/src/components/layout/index.ts new file mode 100644 index 0000000..d314b0e --- /dev/null +++ b/apps/web/src/components/layout/index.ts @@ -0,0 +1,4 @@ +export { RootLayout } from './RootLayout' +export { Sidebar } from './Sidebar' +export { BottomNav } from './BottomNav' +export { NAV_ITEMS, type NavItem } from './nav-items' diff --git a/apps/web/src/components/layout/nav-items.ts b/apps/web/src/components/layout/nav-items.ts new file mode 100644 index 0000000..f05970c --- /dev/null +++ b/apps/web/src/components/layout/nav-items.ts @@ -0,0 +1,14 @@ +import { Home, Users, Wallet, Settings, type LucideIcon } from 'lucide-react' + +export type NavItem = { + label: string + to: string + icon: LucideIcon +} + +export const NAV_ITEMS: NavItem[] = [ + { label: 'Inicio', to: '/', icon: Home }, + { label: 'Grupos', to: '/groups', icon: Users }, + { label: 'Cobros', to: '/payments', icon: Wallet }, + { label: 'Ajustes', to: '/settings', icon: Settings }, +] diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx new file mode 100644 index 0000000..80f2927 --- /dev/null +++ b/apps/web/src/components/ui/avatar.tsx @@ -0,0 +1,40 @@ +import { forwardRef, type HTMLAttributes } from 'react' +import { cn } from '../../lib/utils' + +export type AvatarProps = HTMLAttributes & { + name?: string + src?: string +} + +function initials(name?: string) { + if (!name) return '?' + return name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase() ?? '') + .join('') +} + +export const Avatar = forwardRef( + ({ className, name, src, ...props }, ref) => { + return ( +
+ {src ? ( + {name + ) : ( + initials(name) + )} +
+ ) + }, +) + +Avatar.displayName = 'Avatar' diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..211be5f --- /dev/null +++ b/apps/web/src/components/ui/badge.tsx @@ -0,0 +1,33 @@ +import { forwardRef, type HTMLAttributes } from 'react' +import { cn } from '../../lib/utils' + +export type BadgeVariant = 'success' | 'warning' | 'danger' | 'neutral' + +export type BadgeProps = HTMLAttributes & { + variant?: BadgeVariant +} + +const variants: Record = { + success: 'bg-success-soft text-success', + warning: 'bg-warning-soft text-warning', + danger: 'bg-danger-soft text-danger', + neutral: 'bg-primary-soft text-primary', +} + +export const Badge = forwardRef( + ({ className, variant = 'neutral', ...props }, ref) => { + return ( + + ) + }, +) + +Badge.displayName = 'Badge' diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx new file mode 100644 index 0000000..40fae4e --- /dev/null +++ b/apps/web/src/components/ui/button.tsx @@ -0,0 +1,42 @@ +import { forwardRef, type ButtonHTMLAttributes } from 'react' +import { cn } from '../../lib/utils' + +export type ButtonVariant = 'primary' | 'ghost' | 'outline' +export type ButtonSize = 'sm' | 'md' | 'icon' + +export type ButtonProps = ButtonHTMLAttributes & { + variant?: ButtonVariant + size?: ButtonSize +} + +const variants: Record = { + primary: 'bg-accent text-white hover:bg-accent-strong', + ghost: 'bg-transparent text-primary hover:bg-primary-soft', + outline: 'border border-border bg-white text-primary hover:bg-primary-soft', +} + +const sizes: Record = { + sm: 'h-8 px-3 text-sm', + md: 'h-10 px-4 text-sm', + icon: 'size-9', +} + +export const Button = forwardRef( + ({ className, variant = 'ghost', size = 'md', type = 'button', ...props }, ref) => { + return ( +