diff --git a/apps/backend/package.json b/apps/backend/package.json
index ff4cdf9..433e77f 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -3,6 +3,9 @@
"scripts": {
"dev": "bun run --hot src/server.ts",
"start": "bun src/server.ts",
+ "lint": "biome check .",
+ "lint:fix": "biome check --write .",
+ "format": "biome format --write .",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:migrate:deploy": "prisma migrate deploy",
diff --git a/apps/backend/prisma.config.ts b/apps/backend/prisma.config.ts
index a362ded..6bfd3b9 100644
--- a/apps/backend/prisma.config.ts
+++ b/apps/backend/prisma.config.ts
@@ -1,5 +1,5 @@
-import 'dotenv/config'
-import { defineConfig, env } from 'prisma/config'
+import 'dotenv/config';
+import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma',
@@ -10,4 +10,4 @@ export default defineConfig({
datasource: {
url: env('DATABASE_URL'),
},
-})
+});
diff --git a/apps/backend/prisma/plans.seed-data.ts b/apps/backend/prisma/plans.seed-data.ts
index ba65bb4..5660c11 100644
--- a/apps/backend/prisma/plans.seed-data.ts
+++ b/apps/backend/prisma/plans.seed-data.ts
@@ -74,4 +74,4 @@ export const planSeeds = [
},
},
},
-] as const
+] as const;
diff --git a/apps/backend/prisma/reset-plans.ts b/apps/backend/prisma/reset-plans.ts
index f654110..5e8f878 100644
--- a/apps/backend/prisma/reset-plans.ts
+++ b/apps/backend/prisma/reset-plans.ts
@@ -1,33 +1,33 @@
-import 'dotenv/config'
-import { PrismaPg } from '@prisma/adapter-pg'
-import { PrismaClient } from '../src/generated/prisma/client'
-import { planSeeds } from './plans.seed-data'
+import 'dotenv/config';
+import { PrismaPg } from '@prisma/adapter-pg';
+import { PrismaClient } from '../src/generated/prisma/client';
+import { planSeeds } from './plans.seed-data';
-const databaseUrl = process.env.DATABASE_URL
+const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
- throw new Error('Missing DATABASE_URL in environment for plan reset.')
+ throw new Error('Missing DATABASE_URL in environment for plan reset.');
}
const adapter = new PrismaPg({
connectionString: databaseUrl,
-})
+});
-const prisma = new PrismaClient({ adapter })
+const prisma = new PrismaClient({ adapter });
async function run() {
const complexesUsingPlans = await prisma.complex.count({
where: { planCode: { not: null } },
- })
+ });
if (complexesUsingPlans > 0) {
throw new Error(
- `No se puede resetear planes: hay ${complexesUsingPlans} complejos con plan asignado.`,
- )
+ `No se puede resetear planes: hay ${complexesUsingPlans} complejos con plan asignado.`
+ );
}
await prisma.$transaction(async (tx) => {
- await tx.plan.deleteMany({})
+ await tx.plan.deleteMany({});
for (const plan of planSeeds) {
await tx.plan.create({
@@ -37,19 +37,19 @@ async function run() {
price: plan.price,
rules: plan.rules,
},
- })
+ });
}
- })
+ });
- console.log('Planes reseteados: BASIC, ADVANCED, ENTERPRISE')
+ console.log('Planes reseteados: BASIC, ADVANCED, ENTERPRISE');
}
run()
.then(async () => {
- await prisma.$disconnect()
+ await prisma.$disconnect();
})
.catch(async (error) => {
- console.error(error)
- await prisma.$disconnect()
- process.exit(1)
- })
+ console.error(error);
+ await prisma.$disconnect();
+ process.exit(1);
+ });
diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts
index 3c7bddd..5a449c5 100644
--- a/apps/backend/prisma/seed.ts
+++ b/apps/backend/prisma/seed.ts
@@ -1,21 +1,21 @@
-import 'dotenv/config'
-import { PrismaPg } from '@prisma/adapter-pg'
-import { v7 as uuidv7 } from 'uuid'
-import { PrismaClient } from '../src/generated/prisma/client'
-import { planSeeds } from './plans.seed-data'
-import { sportSeeds } from './sports.seed-data'
+import 'dotenv/config';
+import { PrismaPg } from '@prisma/adapter-pg';
+import { v7 as uuidv7 } from 'uuid';
+import { PrismaClient } from '../src/generated/prisma/client';
+import { planSeeds } from './plans.seed-data';
+import { sportSeeds } from './sports.seed-data';
-const databaseUrl = process.env.DATABASE_URL
+const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
- throw new Error('Missing DATABASE_URL in environment for seeding.')
+ throw new Error('Missing DATABASE_URL in environment for seeding.');
}
const adapter = new PrismaPg({
connectionString: databaseUrl,
-})
+});
-const prisma = new PrismaClient({ adapter })
+const prisma = new PrismaClient({ adapter });
async function run() {
for (const plan of planSeeds) {
@@ -33,7 +33,7 @@ async function run() {
price: plan.price,
rules: plan.rules,
},
- })
+ });
}
for (const sport of sportSeeds) {
@@ -49,16 +49,16 @@ async function run() {
slug: sport.slug,
isActive: true,
},
- })
+ });
}
}
run()
.then(async () => {
- await prisma.$disconnect()
+ await prisma.$disconnect();
})
.catch(async (error) => {
- console.error(error)
- await prisma.$disconnect()
- process.exit(1)
- })
+ console.error(error);
+ await prisma.$disconnect();
+ process.exit(1);
+ });
diff --git a/apps/backend/prisma/sports.seed-data.ts b/apps/backend/prisma/sports.seed-data.ts
index 5f6e41e..ce84916 100644
--- a/apps/backend/prisma/sports.seed-data.ts
+++ b/apps/backend/prisma/sports.seed-data.ts
@@ -5,4 +5,4 @@ export const sportSeeds = [
{ name: 'Pickleball', slug: 'pickleball' },
{ name: 'Basquet', slug: 'basquet' },
{ name: 'Basquet 3', slug: 'basquet-3' },
-] as const
+] as const;
diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts
index 94f2590..a7bb377 100644
--- a/apps/backend/src/app.ts
+++ b/apps/backend/src/app.ts
@@ -1,36 +1,33 @@
-import { Hono } from 'hono'
-import { cors } from 'hono/cors'
-import { logger } from '@/lib/logger'
-import type { AppEnv } from '@/types/hono'
-import { requestId } from 'hono/request-id'
+import { logger } from '@/lib/logger';
+import type { AppEnv } from '@/types/hono';
+import { Hono } from 'hono';
+import { cors } from 'hono/cors';
+import { requestId } from 'hono/request-id';
export function createApp() {
- const app = new Hono()
+ const app = new Hono();
- const allowedOrigins = (
- Bun.env.CORS_ORIGIN ??
- 'http://localhost:5173,http://127.0.0.1:5173'
- )
+ const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173')
.split(',')
.map((value) => value.trim())
- .filter(Boolean)
+ .filter(Boolean);
app.use(
'*',
cors({
origin: (origin) => {
- if (!origin) return allowedOrigins[0] ?? ''
- return allowedOrigins.includes(origin) ? origin : ''
+ if (!origin) return allowedOrigins[0] ?? '';
+ return allowedOrigins.includes(origin) ? origin : '';
},
allowHeaders: ['Authorization', 'Content-Type'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
- }),
- )
+ })
+ );
app.use('*', async (c, next) => {
- const start = performance.now()
- await next()
- const durationMs = Number((performance.now() - start).toFixed(1))
+ const start = performance.now();
+ await next();
+ const durationMs = Number((performance.now() - start).toFixed(1));
logger.info(
{
@@ -38,11 +35,11 @@ export function createApp() {
path: c.req.path,
status: c.res.status,
durationMs,
- requestId
+ requestId,
},
- 'http_request',
- )
- })
+ 'http_request'
+ );
+ });
- return app
+ return app;
}
diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts
index 1f3f7f4..f54a07b 100644
--- a/apps/backend/src/index.ts
+++ b/apps/backend/src/index.ts
@@ -1,8 +1,8 @@
-import { createApp } from '@/app'
-import { registerRoutes } from '@/register-routes'
+import { createApp } from '@/app';
+import { registerRoutes } from '@/register-routes';
-const app = createApp()
-registerRoutes(app)
+const app = createApp();
+registerRoutes(app);
-export type AppType = typeof app
-export default app
+export type AppType = typeof app;
+export default app;
diff --git a/apps/backend/src/lib/logger.ts b/apps/backend/src/lib/logger.ts
index 6df31f9..e36958f 100644
--- a/apps/backend/src/lib/logger.ts
+++ b/apps/backend/src/lib/logger.ts
@@ -1,6 +1,6 @@
-import pino from 'pino'
+import pino from 'pino';
-const isProd = Bun.env.NODE_ENV === 'production'
+const isProd = Bun.env.NODE_ENV === 'production';
export const logger = pino({
level: Bun.env.LOG_LEVEL ?? (isProd ? 'info' : 'debug'),
@@ -15,4 +15,4 @@ export const logger = pino({
ignore: 'pid,hostname',
},
},
-})
+});
diff --git a/apps/backend/src/lib/mailer.ts b/apps/backend/src/lib/mailer.ts
index 57cc9f3..d10d38f 100644
--- a/apps/backend/src/lib/mailer.ts
+++ b/apps/backend/src/lib/mailer.ts
@@ -1,19 +1,17 @@
-import nodemailer from 'nodemailer'
+import nodemailer from 'nodemailer';
-let cachedTransporter: ReturnType | null = null
+let cachedTransporter: ReturnType | null = null;
function getTransporter() {
- if (cachedTransporter) return cachedTransporter
+ if (cachedTransporter) return cachedTransporter;
- const smtpHost = Bun.env.SMTP_HOST
- const smtpPort = Number(Bun.env.SMTP_PORT ?? 587)
- const smtpUser = Bun.env.SMTP_USER
- const smtpPass = Bun.env.SMTP_PASS
+ const smtpHost = Bun.env.SMTP_HOST;
+ const smtpPort = Number(Bun.env.SMTP_PORT ?? 587);
+ const smtpUser = Bun.env.SMTP_USER;
+ const smtpPass = Bun.env.SMTP_PASS;
if (!smtpHost || !smtpUser || !smtpPass) {
- throw new Error(
- 'Missing SMTP env vars. Set SMTP_HOST, SMTP_PORT, SMTP_USER and SMTP_PASS.',
- )
+ throw new Error('Missing SMTP env vars. Set SMTP_HOST, SMTP_PORT, SMTP_USER and SMTP_PASS.');
}
cachedTransporter = nodemailer.createTransport({
@@ -24,21 +22,21 @@ function getTransporter() {
user: smtpUser,
pass: smtpPass,
},
- })
+ });
- return cachedTransporter
+ return cachedTransporter;
}
export async function sendMail(input: {
- to: string
- subject: string
- html: string
- text: string
+ to: string;
+ subject: string;
+ html: string;
+ text: string;
}) {
- const smtpFrom = Bun.env.SMTP_FROM
+ const smtpFrom = Bun.env.SMTP_FROM;
if (!smtpFrom) {
- throw new Error('Missing SMTP_FROM env var.')
+ throw new Error('Missing SMTP_FROM env var.');
}
await getTransporter().sendMail({
@@ -47,5 +45,5 @@ export async function sendMail(input: {
subject: input.subject,
html: input.html,
text: input.text,
- })
+ });
}
diff --git a/apps/backend/src/lib/prisma.ts b/apps/backend/src/lib/prisma.ts
index 25d96a5..623676e 100644
--- a/apps/backend/src/lib/prisma.ts
+++ b/apps/backend/src/lib/prisma.ts
@@ -1,50 +1,50 @@
-import { PrismaPg } from '@prisma/adapter-pg'
-import { PrismaClient } from '@/generated/prisma/client'
+import { PrismaClient } from '@/generated/prisma/client';
+import { PrismaPg } from '@prisma/adapter-pg';
-const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
+const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
function hasExpectedDelegates(client: PrismaClient) {
const prismaClient = client as unknown as {
- court?: { findMany?: unknown }
- sport?: { findMany?: unknown }
- }
+ court?: { findMany?: unknown };
+ sport?: { findMany?: unknown };
+ };
return (
typeof prismaClient.court?.findMany === 'function' &&
typeof prismaClient.sport?.findMany === 'function'
- )
+ );
}
function buildPrismaClient() {
- const databaseUrl = Bun.env.DATABASE_URL
+ const databaseUrl = Bun.env.DATABASE_URL;
if (!databaseUrl) {
- throw new Error('Missing DATABASE_URL in backend environment.')
+ throw new Error('Missing DATABASE_URL in backend environment.');
}
const adapter = new PrismaPg({
connectionString: databaseUrl,
- })
+ });
- return new PrismaClient({ adapter })
+ return new PrismaClient({ adapter });
}
export function getPrismaClient() {
if (globalForPrisma.prisma && hasExpectedDelegates(globalForPrisma.prisma)) {
- return globalForPrisma.prisma
+ return globalForPrisma.prisma;
}
if (globalForPrisma.prisma && !hasExpectedDelegates(globalForPrisma.prisma)) {
- void globalForPrisma.prisma.$disconnect()
+ void globalForPrisma.prisma.$disconnect();
}
- const prisma = buildPrismaClient()
+ const prisma = buildPrismaClient();
if (Bun.env.NODE_ENV !== 'production') {
- globalForPrisma.prisma = prisma
+ globalForPrisma.prisma = prisma;
}
- return prisma
+ return prisma;
}
-export const db = getPrismaClient()
+export const db = getPrismaClient();
diff --git a/apps/backend/src/lib/supabase-admin.ts b/apps/backend/src/lib/supabase-admin.ts
index f6cb5ba..f46eb4a 100644
--- a/apps/backend/src/lib/supabase-admin.ts
+++ b/apps/backend/src/lib/supabase-admin.ts
@@ -1,17 +1,17 @@
-import { createClient } from '@supabase/supabase-js'
+import { createClient } from '@supabase/supabase-js';
-let cachedClient: ReturnType | null = null
+let cachedClient: ReturnType | null = null;
export function getSupabaseAdminClient() {
- if (cachedClient) return cachedClient
+ if (cachedClient) return cachedClient;
- const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL
- const supabaseServiceRoleKey = Bun.env.SUPABASE_SERVICE_ROLE_KEY
+ const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL;
+ const supabaseServiceRoleKey = Bun.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error(
- 'Missing Supabase admin env vars. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in backend environment.',
- )
+ 'Missing Supabase admin env vars. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in backend environment.'
+ );
}
cachedClient = createClient(supabaseUrl, supabaseServiceRoleKey, {
@@ -20,7 +20,7 @@ export function getSupabaseAdminClient() {
persistSession: false,
detectSessionInUrl: false,
},
- })
+ });
- return cachedClient
+ return cachedClient;
}
diff --git a/apps/backend/src/lib/supabase.ts b/apps/backend/src/lib/supabase.ts
index 82b5992..7c8e13e 100644
--- a/apps/backend/src/lib/supabase.ts
+++ b/apps/backend/src/lib/supabase.ts
@@ -1,13 +1,12 @@
-import { createClient } from '@supabase/supabase-js'
+import { createClient } from '@supabase/supabase-js';
-const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL
-const supabaseAnonKey =
- Bun.env.SUPABASE_ANON_KEY ?? Bun.env.VITE_SUPABASE_ANON_KEY
+const supabaseUrl = Bun.env.SUPABASE_URL ?? Bun.env.VITE_SUPABASE_URL;
+const supabaseAnonKey = Bun.env.SUPABASE_ANON_KEY ?? Bun.env.VITE_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
- 'Missing Supabase env vars. Set SUPABASE_URL/SUPABASE_ANON_KEY (or VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY) in backend environment.',
- )
+ 'Missing Supabase env vars. Set SUPABASE_URL/SUPABASE_ANON_KEY (or VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY) in backend environment.'
+ );
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
@@ -16,4 +15,4 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
persistSession: false,
detectSessionInUrl: false,
},
-})
+});
diff --git a/apps/backend/src/middlewares/require-auth.middleware.ts b/apps/backend/src/middlewares/require-auth.middleware.ts
index c4a0132..ab4e306 100644
--- a/apps/backend/src/middlewares/require-auth.middleware.ts
+++ b/apps/backend/src/middlewares/require-auth.middleware.ts
@@ -1,35 +1,32 @@
-import type { MiddlewareHandler } from 'hono'
-import { db } from '@/lib/prisma'
-import { supabase } from '@/lib/supabase'
-import type { AppEnv } from '@/types/hono'
+import { db } from '@/lib/prisma';
+import { supabase } from '@/lib/supabase';
+import type { AppEnv } from '@/types/hono';
+import type { MiddlewareHandler } from 'hono';
function getBearerToken(value: string | undefined): string | null {
- if (!value) return null
- const [scheme, token] = value.split(' ')
- if (scheme?.toLowerCase() !== 'bearer' || !token) return null
- return token
+ if (!value) return null;
+ const [scheme, token] = value.split(' ');
+ if (scheme?.toLowerCase() !== 'bearer' || !token) return null;
+ return token;
}
-export const requireAuth: MiddlewareHandler = async (
- c,
- next,
-) => {
- const token = getBearerToken(c.req.header('authorization'))
+export const requireAuth: MiddlewareHandler = async (c, next) => {
+ const token = getBearerToken(c.req.header('authorization'));
if (!token) {
- return c.json({ message: 'Missing bearer token.' }, 403)
+ return c.json({ message: 'Missing bearer token.' }, 403);
}
- const { data, error } = await supabase.auth.getUser(token)
+ const { data, error } = await supabase.auth.getUser(token);
if (error || !data.user) {
- return c.json({ message: 'Invalid or expired token.' }, 403)
+ return c.json({ message: 'Invalid or expired token.' }, 403);
}
- const email = data.user.email?.toLowerCase()
+ const email = data.user.email?.toLowerCase();
if (!email) {
- return c.json({ message: 'Auth user does not contain email.' }, 403)
+ return c.json({ message: 'Auth user does not contain email.' }, 403);
}
const appUser = await db.user.findFirst({
@@ -38,13 +35,13 @@ export const requireAuth: MiddlewareHandler = async (
email,
},
select: { id: true },
- })
+ });
if (!appUser) {
- return c.json({ message: 'User not provisioned in backend.' }, 403)
+ return c.json({ message: 'User not provisioned in backend.' }, 403);
}
- c.set('authUser', data.user)
- c.set('appUserId', appUser.id)
- await next()
-}
+ c.set('authUser', data.user);
+ c.set('appUserId', appUser.id);
+ await next();
+};
diff --git a/apps/backend/src/middlewares/require-super-admin.middleware.ts b/apps/backend/src/middlewares/require-super-admin.middleware.ts
index a7b6569..9aac239 100644
--- a/apps/backend/src/middlewares/require-super-admin.middleware.ts
+++ b/apps/backend/src/middlewares/require-super-admin.middleware.ts
@@ -1,35 +1,29 @@
-import type { MiddlewareHandler } from 'hono'
-import type { AppEnv } from '@/types/hono'
+import type { AppEnv } from '@/types/hono';
+import type { MiddlewareHandler } from 'hono';
function getSuperAdminEmails(): Set {
- const raw = Bun.env.SUPER_ADMIN_EMAILS ?? ''
+ const raw = Bun.env.SUPER_ADMIN_EMAILS ?? '';
const emails = raw
.split(',')
.map((value) => value.trim().toLowerCase())
- .filter(Boolean)
- return new Set(emails)
+ .filter(Boolean);
+ return new Set(emails);
}
-export const requireSuperAdmin: MiddlewareHandler = async (
- c,
- next,
-) => {
- const authUser = c.get('authUser')
- const role =
- typeof authUser.app_metadata?.role === 'string'
- ? authUser.app_metadata.role
- : null
- const email = authUser.email?.toLowerCase()
+export const requireSuperAdmin: MiddlewareHandler = async (c, next) => {
+ const authUser = c.get('authUser');
+ const role = typeof authUser.app_metadata?.role === 'string' ? authUser.app_metadata.role : null;
+ const email = authUser.email?.toLowerCase();
if (role === 'super_admin') {
- await next()
- return
+ await next();
+ return;
}
if (email && getSuperAdminEmails().has(email)) {
- await next()
- return
+ await next();
+ return;
}
- return c.json({ message: 'Este recurso requiere permisos de super admin.' }, 403)
-}
+ return c.json({ message: 'Este recurso requiere permisos de super admin.' }, 403);
+};
diff --git a/apps/backend/src/modules/admin-booking/admin-booking.routes.ts b/apps/backend/src/modules/admin-booking/admin-booking.routes.ts
index e9e283e..a3e3a08 100644
--- a/apps/backend/src/modules/admin-booking/admin-booking.routes.ts
+++ b/apps/backend/src/modules/admin-booking/admin-booking.routes.ts
@@ -1,40 +1,40 @@
-import { zValidator } from '@hono/zod-validator'
+import { requireAuth } from '@/middlewares/require-auth.middleware';
+import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
+import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
+import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
+import type { AppEnv } from '@/types/hono';
+import { zValidator } from '@hono/zod-validator';
import {
createAdminBookingSchema,
listAdminBookingsQuerySchema,
updateAdminBookingStatusSchema,
-} from '@repo/api-contract'
-import { Hono } from 'hono'
-import { z } from 'zod'
-import { requireAuth } from '@/middlewares/require-auth.middleware'
-import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler'
-import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler'
-import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler'
-import type { AppEnv } from '@/types/hono'
+} from '@repo/api-contract';
+import { Hono } from 'hono';
+import { z } from 'zod';
-export const adminBookingRoutes = new Hono()
-const complexIdParamsSchema = z.object({ complexId: z.uuid() })
-const bookingIdParamsSchema = z.object({ id: z.uuid() })
+export const adminBookingRoutes = new Hono();
+const complexIdParamsSchema = z.object({ complexId: z.uuid() });
+const bookingIdParamsSchema = z.object({ id: z.uuid() });
-adminBookingRoutes.use('*', requireAuth)
+adminBookingRoutes.use('*', requireAuth);
adminBookingRoutes.get(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
zValidator('query', listAdminBookingsQuerySchema),
- listAdminBookingsHandler,
-)
+ listAdminBookingsHandler
+);
adminBookingRoutes.post(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
zValidator('json', createAdminBookingSchema),
- createAdminBookingHandler,
-)
+ createAdminBookingHandler
+);
adminBookingRoutes.patch(
'/:id/status',
zValidator('param', bookingIdParamsSchema),
zValidator('json', updateAdminBookingStatusSchema),
- updateAdminBookingStatusHandler,
-)
+ updateAdminBookingStatusHandler
+);
diff --git a/apps/backend/src/modules/admin-booking/handlers/create-admin-booking.handler.ts b/apps/backend/src/modules/admin-booking/handlers/create-admin-booking.handler.ts
index 1c28b4b..9953fd4 100644
--- a/apps/backend/src/modules/admin-booking/handlers/create-admin-booking.handler.ts
+++ b/apps/backend/src/modules/admin-booking/handlers/create-admin-booking.handler.ts
@@ -1,24 +1,24 @@
-import type { CreateAdminBookingInput } from '@repo/api-contract'
import {
AdminBookingServiceError,
createAdminBooking,
-} from '@/modules/admin-booking/services/admin-booking.service'
-import type { AppContext } from '@/types/hono'
+} from '@/modules/admin-booking/services/admin-booking.service';
+import type { AppContext } from '@/types/hono';
+import type { CreateAdminBookingInput } from '@repo/api-contract';
-type ComplexIdParams = { complexId: string }
+type ComplexIdParams = { complexId: string };
export async function createAdminBookingHandler(c: AppContext) {
- const { complexId } = c.req.valid('param' as never) as ComplexIdParams
- const payload = c.req.valid('json' as never) as CreateAdminBookingInput
- const appUserId = c.get('appUserId')
+ const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
+ const payload = c.req.valid('json' as never) as CreateAdminBookingInput;
+ const appUserId = c.get('appUserId');
try {
- const booking = await createAdminBooking(appUserId, complexId, payload)
- return c.json(booking, 201)
+ const booking = await createAdminBooking(appUserId, complexId, payload);
+ return c.json(booking, 201);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/admin-booking/handlers/list-admin-bookings.handler.ts b/apps/backend/src/modules/admin-booking/handlers/list-admin-bookings.handler.ts
index f5bcd66..85c486f 100644
--- a/apps/backend/src/modules/admin-booking/handlers/list-admin-bookings.handler.ts
+++ b/apps/backend/src/modules/admin-booking/handlers/list-admin-bookings.handler.ts
@@ -1,24 +1,24 @@
-import type { ListAdminBookingsQuery } from '@repo/api-contract'
import {
AdminBookingServiceError,
listAdminBookings,
-} from '@/modules/admin-booking/services/admin-booking.service'
-import type { AppContext } from '@/types/hono'
+} from '@/modules/admin-booking/services/admin-booking.service';
+import type { AppContext } from '@/types/hono';
+import type { ListAdminBookingsQuery } from '@repo/api-contract';
-type ComplexIdParams = { complexId: string }
+type ComplexIdParams = { complexId: string };
export async function listAdminBookingsHandler(c: AppContext) {
- const { complexId } = c.req.valid('param' as never) as ComplexIdParams
- const query = c.req.valid('query' as never) as ListAdminBookingsQuery
- const appUserId = c.get('appUserId')
+ const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
+ const query = c.req.valid('query' as never) as ListAdminBookingsQuery;
+ const appUserId = c.get('appUserId');
try {
- const response = await listAdminBookings(appUserId, complexId, query)
- return c.json(response)
+ const response = await listAdminBookings(appUserId, complexId, query);
+ return c.json(response);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/admin-booking/handlers/update-admin-booking-status.handler.ts b/apps/backend/src/modules/admin-booking/handlers/update-admin-booking-status.handler.ts
index 32362f3..c943f9a 100644
--- a/apps/backend/src/modules/admin-booking/handlers/update-admin-booking-status.handler.ts
+++ b/apps/backend/src/modules/admin-booking/handlers/update-admin-booking-status.handler.ts
@@ -1,24 +1,24 @@
-import type { UpdateAdminBookingStatusInput } from '@repo/api-contract'
import {
AdminBookingServiceError,
updateAdminBookingStatus,
-} from '@/modules/admin-booking/services/admin-booking.service'
-import type { AppContext } from '@/types/hono'
+} from '@/modules/admin-booking/services/admin-booking.service';
+import type { AppContext } from '@/types/hono';
+import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
-type BookingIdParams = { id: string }
+type BookingIdParams = { id: string };
export async function updateAdminBookingStatusHandler(c: AppContext) {
- const { id } = c.req.valid('param' as never) as BookingIdParams
- const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput
- const appUserId = c.get('appUserId')
+ const { id } = c.req.valid('param' as never) as BookingIdParams;
+ const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput;
+ const appUserId = c.get('appUserId');
try {
- const booking = await updateAdminBookingStatus(appUserId, id, payload)
- return c.json(booking)
+ const booking = await updateAdminBookingStatus(appUserId, id, payload);
+ return c.json(booking);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts b/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts
index 8f583dc..1fc4706 100644
--- a/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts
+++ b/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts
@@ -1,19 +1,19 @@
+import { randomInt } from 'node:crypto';
+import { CourtBookingStatus } from '@/generated/prisma/enums';
+import { db } from '@/lib/prisma';
+import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type {
AdminBooking,
CreateAdminBookingInput,
ListAdminBookingsQuery,
UpdateAdminBookingStatusInput,
-} from '@repo/api-contract'
-import { randomInt } from 'node:crypto'
-import { v7 as uuidv7 } from 'uuid'
-import { CourtBookingStatus } from '@/generated/prisma/enums'
-import { db } from '@/lib/prisma'
-import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service'
+} from '@repo/api-contract';
+import { v7 as uuidv7 } from 'uuid';
type Slot = {
- startTime: string
- endTime: string
-}
+ startTime: string;
+ endTime: string;
+};
const DAY_OF_WEEK_BY_INDEX = [
'SUNDAY',
@@ -23,44 +23,44 @@ const DAY_OF_WEEK_BY_INDEX = [
'THURSDAY',
'FRIDAY',
'SATURDAY',
-] as const
-const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
-const BOOKING_CODE_LENGTH = 6
+] as const;
+const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
+const BOOKING_CODE_LENGTH = 6;
export class AdminBookingServiceError extends Error {
- status: 400 | 403 | 404 | 409
+ status: 400 | 403 | 404 | 409;
constructor(message: string, status: 400 | 403 | 404 | 409) {
- super(message)
- this.name = 'AdminBookingServiceError'
- this.status = status
+ super(message);
+ this.name = 'AdminBookingServiceError';
+ this.status = status;
}
}
function toMinutes(value: string): number {
- const [hours, minutes] = value.split(':').map((part) => Number(part))
- return hours * 60 + minutes
+ const [hours, minutes] = value.split(':').map((part) => Number(part));
+ return hours * 60 + minutes;
}
function minutesToTime(minutes: number): string {
- const safeMinutes = Math.max(0, minutes)
- const hours = Math.floor(safeMinutes / 60)
- const mins = safeMinutes % 60
- return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`
+ const safeMinutes = Math.max(0, minutes);
+ const hours = Math.floor(safeMinutes / 60);
+ const mins = safeMinutes % 60;
+ return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
function parseIsoDate(date: string): Date {
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date)
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
if (!match) {
- throw new AdminBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400)
+ throw new AdminBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
}
- const year = Number(match[1])
- const month = Number(match[2])
- const day = Number(match[3])
+ const year = Number(match[1]);
+ const month = Number(match[2]);
+ const day = Number(match[3]);
- const bookingDate = new Date(Date.UTC(year, month - 1, day))
+ const bookingDate = new Date(Date.UTC(year, month - 1, day));
if (
Number.isNaN(bookingDate.getTime()) ||
@@ -68,48 +68,48 @@ function parseIsoDate(date: string): Date {
bookingDate.getUTCMonth() + 1 !== month ||
bookingDate.getUTCDate() !== day
) {
- throw new AdminBookingServiceError('La fecha enviada no es valida.', 400)
+ throw new AdminBookingServiceError('La fecha enviada no es valida.', 400);
}
- return bookingDate
+ return bookingDate;
}
function getDayOfWeek(date: Date) {
- const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()]
+ const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
if (!dayOfWeek) {
- throw new AdminBookingServiceError('No se pudo resolver el dia de la semana.', 400)
+ throw new AdminBookingServiceError('No se pudo resolver el dia de la semana.', 400);
}
- return dayOfWeek
+ return dayOfWeek;
}
function formatIsoDate(date: Date): string {
- return date.toISOString().slice(0, 10)
+ return date.toISOString().slice(0, 10);
}
function generateBookingCode(): string {
- let code = ''
+ let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
- code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)]
+ code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
- return code
+ return code;
}
function buildSlots(
availability: Array<{
- startTime: string
- endTime: string
+ startTime: string;
+ endTime: string;
}>,
- slotDurationMinutes: number,
+ slotDurationMinutes: number
): Slot[] {
- const slots: Slot[] = []
+ const slots: Slot[] = [];
for (const range of availability) {
- const start = toMinutes(range.startTime)
- const end = toMinutes(range.endTime)
+ const start = toMinutes(range.startTime);
+ const end = toMinutes(range.endTime);
for (
let current = start;
@@ -119,11 +119,11 @@ function buildSlots(
slots.push({
startTime: minutesToTime(current),
endTime: minutesToTime(current + slotDurationMinutes),
- })
+ });
}
}
- return slots
+ return slots;
}
async function ensureComplexAccess(complexId: string, appUserId: string) {
@@ -147,39 +147,39 @@ async function ensureComplexAccess(complexId: string, appUserId: string) {
},
},
},
- })
+ });
if (!complexUser) {
- throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403)
+ throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
}
- return complexUser.complex
+ return complexUser.complex;
}
function mapBookingResponse(booking: {
- id: string
- bookingCode: string
- bookingDate: Date
- startTime: string
- endTime: string
- customerName: string
- customerPhone: string
- status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'
- createdAt: Date
- updatedAt: Date
+ id: string;
+ bookingCode: string;
+ bookingDate: Date;
+ startTime: string;
+ endTime: string;
+ customerName: string;
+ customerPhone: string;
+ status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED';
+ createdAt: Date;
+ updatedAt: Date;
court: {
- id: string
- name: string
+ id: string;
+ name: string;
complex: {
- id: string
- complexName: string
- }
+ id: string;
+ complexName: string;
+ };
sport: {
- id: string
- name: string
- slug: string
- }
- }
+ id: string;
+ name: string;
+ slug: string;
+ };
+ };
}): AdminBooking {
return {
id: booking.id,
@@ -201,16 +201,16 @@ function mapBookingResponse(booking: {
status: booking.status,
createdAt: booking.createdAt.toISOString(),
updatedAt: booking.updatedAt.toISOString(),
- }
+ };
}
export async function listAdminBookings(
appUserId: string,
complexId: string,
- query: ListAdminBookingsQuery,
+ query: ListAdminBookingsQuery
) {
- await ensureComplexAccess(complexId, appUserId)
- const fromDate = parseIsoDate(query.fromDate)
+ await ensureComplexAccess(complexId, appUserId);
+ const fromDate = parseIsoDate(query.fromDate);
const bookings = await db.courtBooking.findMany({
where: {
@@ -243,21 +243,21 @@ export async function listAdminBookings(
},
},
orderBy: [{ bookingDate: 'asc' }, { startTime: 'asc' }, { createdAt: 'asc' }],
- })
+ });
return {
bookings: bookings.map((booking) => mapBookingResponse(booking)),
- }
+ };
}
export async function createAdminBooking(
appUserId: string,
complexId: string,
- input: CreateAdminBookingInput,
+ input: CreateAdminBookingInput
) {
- const complex = await ensureComplexAccess(complexId, appUserId)
- const bookingDate = parseIsoDate(input.date)
- const dayOfWeek = getDayOfWeek(bookingDate)
+ const complex = await ensureComplexAccess(complexId, appUserId);
+ const bookingDate = parseIsoDate(input.date);
+ const dayOfWeek = getDayOfWeek(bookingDate);
const court = await db.court.findFirst({
where: {
@@ -281,36 +281,36 @@ export async function createAdminBooking(
},
},
},
- })
+ });
if (!court) {
- throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404)
+ throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
}
- const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes)
- const selectedStartMinutes = toMinutes(input.startTime)
- const selectedEndMinutes = selectedStartMinutes + court.slotDurationMinutes
+ const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
+ const selectedStartMinutes = toMinutes(input.startTime);
+ const selectedEndMinutes = selectedStartMinutes + court.slotDurationMinutes;
const selectedSlot = {
startTime: input.startTime,
endTime: minutesToTime(selectedEndMinutes),
- }
+ };
const slotExists = validSlots.some(
- (slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime,
- )
+ (slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
+ );
if (!slotExists) {
throw new AdminBookingServiceError(
'El horario seleccionado no esta disponible para esa cancha.',
- 409,
- )
+ 409
+ );
}
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const booking = await db.$transaction(async (tx) => {
if (complex.plan) {
- const rules = parsePlanRules(complex.plan.rules)
+ const rules = parsePlanRules(complex.plan.rules);
const bookingsForDate = await tx.courtBooking.count({
where: {
bookingDate,
@@ -319,25 +319,25 @@ export async function createAdminBooking(
complexId,
},
},
- })
+ });
const courtsCount = await tx.court.count({
where: {
complexId,
},
- })
+ });
const violations = evaluatePlanUsage(rules, {
courtsCount,
bookingsToday: bookingsForDate,
- })
+ });
const maxBookingsViolation = violations.find(
- (violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED',
- )
+ (violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
+ );
if (maxBookingsViolation) {
- throw new AdminBookingServiceError(maxBookingsViolation.message, 409)
+ throw new AdminBookingServiceError(maxBookingsViolation.message, 409);
}
}
@@ -353,10 +353,10 @@ export async function createAdminBooking(
gt: selectedSlot.startTime,
},
},
- })
+ });
if (overlappingBooking) {
- throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409)
+ throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
return tx.courtBooking.create({
@@ -392,51 +392,51 @@ export async function createAdminBooking(
},
},
},
- })
- })
+ });
+ });
- return mapBookingResponse(booking)
+ return mapBookingResponse(booking);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
- throw error
+ throw error;
}
const prismaError = error as {
- code?: string
+ code?: string;
meta?: {
- target?: string[] | string
- }
- }
+ target?: string[] | string;
+ };
+ };
if (prismaError.code === 'P2002') {
const targets = Array.isArray(prismaError.meta?.target)
? prismaError.meta?.target
- : [prismaError.meta?.target]
+ : [prismaError.meta?.target];
const isBookingCodeCollision = targets.some((target) =>
- String(target).includes('booking_code'),
- )
+ String(target).includes('booking_code')
+ );
if (isBookingCodeCollision) {
- continue
+ continue;
}
- throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409)
+ throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
- throw error
+ throw error;
}
}
throw new AdminBookingServiceError(
'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
- 409,
- )
+ 409
+ );
}
export async function updateAdminBookingStatus(
appUserId: string,
bookingId: string,
- input: UpdateAdminBookingStatusInput,
+ input: UpdateAdminBookingStatusInput
) {
const booking = await db.courtBooking.findFirst({
where: {
@@ -472,33 +472,31 @@ export async function updateAdminBookingStatus(
},
},
},
- })
+ });
if (!booking) {
- throw new AdminBookingServiceError('Reserva no encontrada.', 404)
+ throw new AdminBookingServiceError('Reserva no encontrada.', 404);
}
if (input.status === 'CANCELLED' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden cancelar reservas en estado confirmada.',
- 409,
- )
+ 409
+ );
}
if (input.status === 'COMPLETED' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden marcar como cumplidas las reservas confirmadas.',
- 409,
- )
+ 409
+ );
}
const updated = await db.courtBooking.update({
where: { id: booking.id },
data: {
status:
- input.status === 'COMPLETED'
- ? CourtBookingStatus.COMPLETED
- : CourtBookingStatus.CANCELLED,
+ input.status === 'COMPLETED' ? CourtBookingStatus.COMPLETED : CourtBookingStatus.CANCELLED,
},
include: {
court: {
@@ -521,7 +519,7 @@ export async function updateAdminBookingStatus(
},
},
},
- })
+ });
- return mapBookingResponse(updated)
+ return mapBookingResponse(updated);
}
diff --git a/apps/backend/src/modules/complex/complex.routes.ts b/apps/backend/src/modules/complex/complex.routes.ts
index ac3946d..af9581c 100644
--- a/apps/backend/src/modules/complex/complex.routes.ts
+++ b/apps/backend/src/modules/complex/complex.routes.ts
@@ -1,37 +1,33 @@
-import { zValidator } from '@hono/zod-validator'
-import { createComplexSchema, updateComplexSchema } from '@repo/api-contract'
-import { Hono } from 'hono'
-import { z } from 'zod'
-import { requireAuth } from '@/middlewares/require-auth.middleware'
-import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler'
-import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler'
-import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler'
-import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler'
-import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler'
-import type { AppEnv } from '@/types/hono'
+import { requireAuth } from '@/middlewares/require-auth.middleware';
+import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler';
+import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler';
+import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler';
+import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler';
+import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler';
+import type { AppEnv } from '@/types/hono';
+import { zValidator } from '@hono/zod-validator';
+import { createComplexSchema, updateComplexSchema } from '@repo/api-contract';
+import { Hono } from 'hono';
+import { z } from 'zod';
-export const complexRoutes = new Hono()
-const complexIdParamsSchema = z.object({ id: z.uuid() })
-const complexSlugParamsSchema = z.object({ slug: z.string().trim().min(1) })
+export const complexRoutes = new Hono();
+const complexIdParamsSchema = z.object({ id: z.uuid() });
+const complexSlugParamsSchema = z.object({ slug: z.string().trim().min(1) });
-complexRoutes.use('*', requireAuth)
+complexRoutes.use('*', requireAuth);
-complexRoutes.post('/', zValidator('json', createComplexSchema), createComplexHandler)
-complexRoutes.get('/mine', listMyComplexesHandler)
+complexRoutes.post('/', zValidator('json', createComplexSchema), createComplexHandler);
+complexRoutes.get('/mine', listMyComplexesHandler);
complexRoutes.get(
'/slug/:slug',
zValidator('param', complexSlugParamsSchema),
- getComplexBySlugHandler,
-)
+ getComplexBySlugHandler
+);
-complexRoutes.get(
- '/:id',
- zValidator('param', complexIdParamsSchema),
- getComplexByIdHandler,
-)
+complexRoutes.get('/:id', zValidator('param', complexIdParamsSchema), getComplexByIdHandler);
complexRoutes.patch(
'/:id',
zValidator('param', complexIdParamsSchema),
zValidator('json', updateComplexSchema),
- updateComplexHandler,
-)
+ updateComplexHandler
+);
diff --git a/apps/backend/src/modules/complex/handlers/create-complex.handler.ts b/apps/backend/src/modules/complex/handlers/create-complex.handler.ts
index 3f90ba7..6977264 100644
--- a/apps/backend/src/modules/complex/handlers/create-complex.handler.ts
+++ b/apps/backend/src/modules/complex/handlers/create-complex.handler.ts
@@ -1,15 +1,15 @@
-import type { CreateComplexInput } from '@repo/api-contract'
-import type { AppContext } from '@/types/hono'
-import { createComplex } from '@/modules/complex/services/complex.service'
+import { createComplex } from '@/modules/complex/services/complex.service';
+import type { AppContext } from '@/types/hono';
+import type { CreateComplexInput } from '@repo/api-contract';
export async function createComplexHandler(c: AppContext) {
- const payload = c.req.valid('json' as never) as CreateComplexInput
+ const payload = c.req.valid('json' as never) as CreateComplexInput;
- const authUser = c.get('authUser')
- const adminEmail = authUser.email
+ const authUser = c.get('authUser');
+ const adminEmail = authUser.email;
if (!adminEmail) {
- return c.json({ message: 'El usuario autenticado no tiene email.' }, 400)
+ return c.json({ message: 'El usuario autenticado no tiene email.' }, 400);
}
const complex = await createComplex({
@@ -20,7 +20,7 @@ export async function createComplexHandler(c: AppContext) {
city: payload.city,
state: payload.state,
country: payload.country,
- })
+ });
- return c.json(complex, 201)
+ return c.json(complex, 201);
}
diff --git a/apps/backend/src/modules/complex/handlers/get-complex-by-id.handler.ts b/apps/backend/src/modules/complex/handlers/get-complex-by-id.handler.ts
index 43b07fe..4df8248 100644
--- a/apps/backend/src/modules/complex/handlers/get-complex-by-id.handler.ts
+++ b/apps/backend/src/modules/complex/handlers/get-complex-by-id.handler.ts
@@ -1,16 +1,16 @@
-import type { AppContext } from '@/types/hono'
-import { getComplexById } from '@/modules/complex/services/complex.service'
+import { getComplexById } from '@/modules/complex/services/complex.service';
+import type { AppContext } from '@/types/hono';
-type ComplexIdParams = { id: string }
+type ComplexIdParams = { id: string };
export async function getComplexByIdHandler(c: AppContext) {
- const { id } = c.req.valid('param' as never) as ComplexIdParams
+ const { id } = c.req.valid('param' as never) as ComplexIdParams;
- const complex = await getComplexById(id)
+ const complex = await getComplexById(id);
if (!complex) {
- return c.json({ message: 'Complejo no encontrado.' }, 404)
+ return c.json({ message: 'Complejo no encontrado.' }, 404);
}
- return c.json(complex)
+ return c.json(complex);
}
diff --git a/apps/backend/src/modules/complex/handlers/get-complex-by-slug.handler.ts b/apps/backend/src/modules/complex/handlers/get-complex-by-slug.handler.ts
index 22514d8..7990f6f 100644
--- a/apps/backend/src/modules/complex/handlers/get-complex-by-slug.handler.ts
+++ b/apps/backend/src/modules/complex/handlers/get-complex-by-slug.handler.ts
@@ -1,16 +1,16 @@
-import type { AppContext } from '@/types/hono'
-import { getComplexBySlug } from '@/modules/complex/services/complex.service'
+import { getComplexBySlug } from '@/modules/complex/services/complex.service';
+import type { AppContext } from '@/types/hono';
-type ComplexSlugParams = { slug: string }
+type ComplexSlugParams = { slug: string };
export async function getComplexBySlugHandler(c: AppContext) {
- const { slug } = c.req.valid('param' as never) as ComplexSlugParams
+ const { slug } = c.req.valid('param' as never) as ComplexSlugParams;
- const complex = await getComplexBySlug(slug)
+ const complex = await getComplexBySlug(slug);
if (!complex) {
- return c.json({ message: 'Complejo no encontrado.' }, 404)
+ return c.json({ message: 'Complejo no encontrado.' }, 404);
}
- return c.json(complex)
+ return c.json(complex);
}
diff --git a/apps/backend/src/modules/complex/handlers/list-my-complexes.handler.ts b/apps/backend/src/modules/complex/handlers/list-my-complexes.handler.ts
index 744f192..1760b6c 100644
--- a/apps/backend/src/modules/complex/handlers/list-my-complexes.handler.ts
+++ b/apps/backend/src/modules/complex/handlers/list-my-complexes.handler.ts
@@ -1,8 +1,8 @@
-import type { AppContext } from '@/types/hono'
-import { listMyComplexes } from '@/modules/complex/services/complex.service'
+import { listMyComplexes } from '@/modules/complex/services/complex.service';
+import type { AppContext } from '@/types/hono';
export async function listMyComplexesHandler(c: AppContext) {
- const appUserId = c.get('appUserId')
- const complexes = await listMyComplexes(appUserId)
- return c.json(complexes)
+ const appUserId = c.get('appUserId');
+ const complexes = await listMyComplexes(appUserId);
+ return c.json(complexes);
}
diff --git a/apps/backend/src/modules/complex/handlers/update-complex.handler.ts b/apps/backend/src/modules/complex/handlers/update-complex.handler.ts
index c699f6e..42f9e2e 100644
--- a/apps/backend/src/modules/complex/handlers/update-complex.handler.ts
+++ b/apps/backend/src/modules/complex/handlers/update-complex.handler.ts
@@ -1,27 +1,27 @@
-import type { UpdateComplexInput } from '@repo/api-contract'
-import { Prisma } from '@/generated/prisma/client'
-import type { AppContext } from '@/types/hono'
-import { getComplexById, updateComplex } from '@/modules/complex/services/complex.service'
+import { Prisma } from '@/generated/prisma/client';
+import { getComplexById, updateComplex } from '@/modules/complex/services/complex.service';
+import type { AppContext } from '@/types/hono';
+import type { UpdateComplexInput } from '@repo/api-contract';
-type ComplexIdParams = { id: string }
+type ComplexIdParams = { id: string };
export async function updateComplexHandler(c: AppContext) {
- const { id } = c.req.valid('param' as never) as ComplexIdParams
- const payload = c.req.valid('json' as never) as UpdateComplexInput
+ const { id } = c.req.valid('param' as never) as ComplexIdParams;
+ const payload = c.req.valid('json' as never) as UpdateComplexInput;
- const existing = await getComplexById(id)
+ const existing = await getComplexById(id);
if (!existing) {
- return c.json({ message: 'Complejo no encontrado.' }, 404)
+ return c.json({ message: 'Complejo no encontrado.' }, 404);
}
try {
- const complex = await updateComplex(id, payload)
- return c.json(complex)
+ const complex = await updateComplex(id, payload);
+ return c.json(complex);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
- return c.json({ message: 'No se pudo actualizar el complejo.' }, 409)
+ return c.json({ message: 'No se pudo actualizar el complejo.' }, 409);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/complex/services/complex.service.ts b/apps/backend/src/modules/complex/services/complex.service.ts
index 7f89397..4a74015 100644
--- a/apps/backend/src/modules/complex/services/complex.service.ts
+++ b/apps/backend/src/modules/complex/services/complex.service.ts
@@ -1,27 +1,27 @@
-import { v7 as uuidv7 } from 'uuid'
-import { Prisma } from '@/generated/prisma/client'
-import { db } from '@/lib/prisma'
+import { Prisma } from '@/generated/prisma/client';
+import { db } from '@/lib/prisma';
+import { v7 as uuidv7 } from 'uuid';
export type CreateComplexInput = {
- complexName: string
- physicalAddress: string
- adminEmail: string
- planCode?: string
- city?: string
- state?: string
- country?: string
-}
+ complexName: string;
+ physicalAddress: string;
+ adminEmail: string;
+ planCode?: string;
+ city?: string;
+ state?: string;
+ country?: string;
+};
export type UpdateComplexInput = {
- complexName?: string
- physicalAddress?: string | null
- complexSlug?: string
- adminEmail?: string
- planCode?: string | null
- city?: string | null
- state?: string | null
- country?: string | null
-}
+ complexName?: string;
+ physicalAddress?: string | null;
+ complexSlug?: string;
+ adminEmail?: string;
+ planCode?: string | null;
+ city?: string | null;
+ state?: string | null;
+ country?: string | null;
+};
function slugify(value: string): string {
return value
@@ -31,18 +31,15 @@ function slugify(value: string): string {
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
- .replace(/-+/g, '-')
+ .replace(/-+/g, '-');
}
-async function buildUniqueSlug(
- source: string,
- excludeComplexId?: string,
-): Promise {
- const base = slugify(source)
- const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`
+async function buildUniqueSlug(source: string, excludeComplexId?: string): Promise {
+ const base = slugify(source);
+ const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`;
- let candidate = fallback
- let index = 1
+ let candidate = fallback;
+ let index = 1;
while (true) {
const existing = await db.complex.findFirst({
@@ -57,17 +54,17 @@ async function buildUniqueSlug(
: {}),
},
select: { id: true },
- })
+ });
- if (!existing) return candidate
+ if (!existing) return candidate;
- index += 1
- candidate = `${fallback}-${index}`
+ index += 1;
+ candidate = `${fallback}-${index}`;
}
}
export async function createComplex(input: CreateComplexInput) {
- const complexSlug = await buildUniqueSlug(input.complexName)
+ const complexSlug = await buildUniqueSlug(input.complexName);
return db.complex.create({
data: {
@@ -81,19 +78,19 @@ export async function createComplex(input: CreateComplexInput) {
adminEmail: input.adminEmail,
planCode: input.planCode,
},
- })
+ });
}
export async function getComplexById(id: string) {
return db.complex.findUnique({
where: { id },
- })
+ });
}
export async function getComplexBySlug(slug: string) {
return db.complex.findUnique({
where: { complexSlug: slug },
- })
+ });
}
export async function listMyComplexes(appUserId: string) {
@@ -105,49 +102,49 @@ export async function listMyComplexes(appUserId: string) {
orderBy: {
createdAt: 'asc',
},
- })
+ });
- return complexUsers.map((complexUser) => complexUser.complex)
+ return complexUsers.map((complexUser) => complexUser.complex);
}
export async function updateComplex(id: string, input: UpdateComplexInput) {
- const data: Record = {}
+ const data: Record = {};
if (input.complexName) {
- data.complexName = input.complexName
- data.complexSlug = await buildUniqueSlug(input.complexName, id)
+ data.complexName = input.complexName;
+ data.complexSlug = await buildUniqueSlug(input.complexName, id);
}
if (input.complexSlug) {
- data.complexSlug = await buildUniqueSlug(input.complexSlug, id)
+ data.complexSlug = await buildUniqueSlug(input.complexSlug, id);
}
if (input.adminEmail) {
- data.adminEmail = input.adminEmail
+ data.adminEmail = input.adminEmail;
}
if (input.planCode !== undefined) {
- data.planCode = input.planCode
+ data.planCode = input.planCode;
}
if (input.physicalAddress !== undefined) {
- data.physicalAddress = input.physicalAddress?.trim() ?? null
+ data.physicalAddress = input.physicalAddress?.trim() ?? null;
}
if (input.city !== undefined) {
- data.city = input.city?.trim() ?? null
+ data.city = input.city?.trim() ?? null;
}
if (input.state !== undefined) {
- data.state = input.state?.trim() ?? null
+ data.state = input.state?.trim() ?? null;
}
if (input.country !== undefined) {
- data.country = input.country?.trim() ?? null
+ data.country = input.country?.trim() ?? null;
}
return db.complex.update({
where: { id },
data: data as Prisma.ComplexUncheckedUpdateInput,
- })
+ });
}
diff --git a/apps/backend/src/modules/court/court.routes.ts b/apps/backend/src/modules/court/court.routes.ts
index afff7ff..0d63c58 100644
--- a/apps/backend/src/modules/court/court.routes.ts
+++ b/apps/backend/src/modules/court/court.routes.ts
@@ -1,33 +1,33 @@
-import { zValidator } from '@hono/zod-validator'
-import { createCourtSchema, updateCourtSchema } from '@repo/api-contract'
-import { Hono } from 'hono'
-import { z } from 'zod'
-import { requireAuth } from '@/middlewares/require-auth.middleware'
-import { createCourtHandler } from '@/modules/court/handlers/create-court.handler'
-import { listCourtsByComplexHandler } from '@/modules/court/handlers/list-courts-by-complex.handler'
-import { updateCourtHandler } from '@/modules/court/handlers/update-court.handler'
-import type { AppEnv } from '@/types/hono'
+import { requireAuth } from '@/middlewares/require-auth.middleware';
+import { createCourtHandler } from '@/modules/court/handlers/create-court.handler';
+import { listCourtsByComplexHandler } from '@/modules/court/handlers/list-courts-by-complex.handler';
+import { updateCourtHandler } from '@/modules/court/handlers/update-court.handler';
+import type { AppEnv } from '@/types/hono';
+import { zValidator } from '@hono/zod-validator';
+import { createCourtSchema, updateCourtSchema } from '@repo/api-contract';
+import { Hono } from 'hono';
+import { z } from 'zod';
-export const courtRoutes = new Hono()
-const complexIdParamsSchema = z.object({ complexId: z.uuid() })
-const courtIdParamsSchema = z.object({ id: z.uuid() })
+export const courtRoutes = new Hono();
+const complexIdParamsSchema = z.object({ complexId: z.uuid() });
+const courtIdParamsSchema = z.object({ id: z.uuid() });
-courtRoutes.use('*', requireAuth)
+courtRoutes.use('*', requireAuth);
courtRoutes.get(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
- listCourtsByComplexHandler,
-)
+ listCourtsByComplexHandler
+);
courtRoutes.post(
'/complex/:complexId',
zValidator('param', complexIdParamsSchema),
zValidator('json', createCourtSchema),
- createCourtHandler,
-)
+ createCourtHandler
+);
courtRoutes.patch(
'/:id',
zValidator('param', courtIdParamsSchema),
zValidator('json', updateCourtSchema),
- updateCourtHandler,
-)
+ updateCourtHandler
+);
diff --git a/apps/backend/src/modules/court/handlers/create-court.handler.ts b/apps/backend/src/modules/court/handlers/create-court.handler.ts
index 850fa7d..b910522 100644
--- a/apps/backend/src/modules/court/handlers/create-court.handler.ts
+++ b/apps/backend/src/modules/court/handlers/create-court.handler.ts
@@ -1,21 +1,21 @@
-import type { CreateCourtInput } from '@repo/api-contract'
-import { CourtServiceError, createCourt } from '@/modules/court/services/court.service'
-import type { AppContext } from '@/types/hono'
+import { CourtServiceError, createCourt } from '@/modules/court/services/court.service';
+import type { AppContext } from '@/types/hono';
+import type { CreateCourtInput } from '@repo/api-contract';
-type ComplexIdParams = { complexId: string }
+type ComplexIdParams = { complexId: string };
export async function createCourtHandler(c: AppContext) {
- const { complexId } = c.req.valid('param' as never) as ComplexIdParams
- const payload = c.req.valid('json' as never) as CreateCourtInput
- const appUserId = c.get('appUserId')
+ const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
+ const payload = c.req.valid('json' as never) as CreateCourtInput;
+ const appUserId = c.get('appUserId');
try {
- const court = await createCourt(appUserId, complexId, payload)
- return c.json(court, 201)
+ const court = await createCourt(appUserId, complexId, payload);
+ return c.json(court, 201);
} catch (error) {
if (error instanceof CourtServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/court/handlers/list-courts-by-complex.handler.ts b/apps/backend/src/modules/court/handlers/list-courts-by-complex.handler.ts
index 19d5fe8..c436103 100644
--- a/apps/backend/src/modules/court/handlers/list-courts-by-complex.handler.ts
+++ b/apps/backend/src/modules/court/handlers/list-courts-by-complex.handler.ts
@@ -1,19 +1,19 @@
-import { CourtServiceError, listCourtsByComplex } from '@/modules/court/services/court.service'
-import type { AppContext } from '@/types/hono'
+import { CourtServiceError, listCourtsByComplex } from '@/modules/court/services/court.service';
+import type { AppContext } from '@/types/hono';
-type ComplexIdParams = { complexId: string }
+type ComplexIdParams = { complexId: string };
export async function listCourtsByComplexHandler(c: AppContext) {
- const { complexId } = c.req.valid('param' as never) as ComplexIdParams
- const appUserId = c.get('appUserId')
+ const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
+ const appUserId = c.get('appUserId');
try {
- const courts = await listCourtsByComplex(complexId, appUserId)
- return c.json(courts)
+ const courts = await listCourtsByComplex(complexId, appUserId);
+ return c.json(courts);
} catch (error) {
if (error instanceof CourtServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/court/handlers/update-court.handler.ts b/apps/backend/src/modules/court/handlers/update-court.handler.ts
index 466065a..6ff5784 100644
--- a/apps/backend/src/modules/court/handlers/update-court.handler.ts
+++ b/apps/backend/src/modules/court/handlers/update-court.handler.ts
@@ -1,21 +1,21 @@
-import type { UpdateCourtInput } from '@repo/api-contract'
-import { CourtServiceError, updateCourt } from '@/modules/court/services/court.service'
-import type { AppContext } from '@/types/hono'
+import { CourtServiceError, updateCourt } from '@/modules/court/services/court.service';
+import type { AppContext } from '@/types/hono';
+import type { UpdateCourtInput } from '@repo/api-contract';
-type CourtIdParams = { id: string }
+type CourtIdParams = { id: string };
export async function updateCourtHandler(c: AppContext) {
- const { id } = c.req.valid('param' as never) as CourtIdParams
- const payload = c.req.valid('json' as never) as UpdateCourtInput
- const appUserId = c.get('appUserId')
+ const { id } = c.req.valid('param' as never) as CourtIdParams;
+ const payload = c.req.valid('json' as never) as UpdateCourtInput;
+ const appUserId = c.get('appUserId');
try {
- const court = await updateCourt(appUserId, id, payload)
- return c.json(court)
+ const court = await updateCourt(appUserId, id, payload);
+ return c.json(court);
} catch (error) {
if (error instanceof CourtServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/court/services/court.service.ts b/apps/backend/src/modules/court/services/court.service.ts
index 290f993..f8b02af 100644
--- a/apps/backend/src/modules/court/services/court.service.ts
+++ b/apps/backend/src/modules/court/services/court.service.ts
@@ -1,72 +1,61 @@
-import type { CreateCourtInput, DayOfWeek, UpdateCourtInput } from '@repo/api-contract'
-import { v7 as uuidv7 } from 'uuid'
-import type {
- Court,
- CourtAvailability,
- CourtPriceRule,
- Sport,
-} from '@/generated/prisma/client'
-import { db } from '@/lib/prisma'
-import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service'
+import type { Court, CourtAvailability, CourtPriceRule, Sport } from '@/generated/prisma/client';
+import { db } from '@/lib/prisma';
+import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
+import type { CreateCourtInput, DayOfWeek, UpdateCourtInput } from '@repo/api-contract';
+import { v7 as uuidv7 } from 'uuid';
type CourtWithRelations = Court & {
- sport: Sport
- availabilities: CourtAvailability[]
- priceRules: CourtPriceRule[]
-}
+ sport: Sport;
+ availabilities: CourtAvailability[];
+ priceRules: CourtPriceRule[];
+};
export class CourtServiceError extends Error {
- status: 400 | 403 | 404 | 409
+ status: 400 | 403 | 404 | 409;
constructor(message: string, status: 400 | 403 | 404 | 409) {
- super(message)
- this.name = 'CourtServiceError'
- this.status = status
+ super(message);
+ this.name = 'CourtServiceError';
+ this.status = status;
}
}
function toMinutes(value: string): number {
- const [hours, minutes] = value.split(':').map((part) => Number(part))
- return hours * 60 + minutes
+ const [hours, minutes] = value.split(':').map((part) => Number(part));
+ return hours * 60 + minutes;
}
function assertAvailabilityRanges(
availability: Array<{
- dayOfWeek: DayOfWeek
- startTime: string
- endTime: string
- }>,
+ dayOfWeek: DayOfWeek;
+ startTime: string;
+ endTime: string;
+ }>
) {
- const grouped = new Map>()
+ const grouped = new Map>();
for (const range of availability) {
- const start = toMinutes(range.startTime)
- const end = toMinutes(range.endTime)
+ const start = toMinutes(range.startTime);
+ const end = toMinutes(range.endTime);
if (start >= end) {
- throw new CourtServiceError(
- `El rango ${range.startTime}-${range.endTime} es invalido.`,
- 400,
- )
+ throw new CourtServiceError(`El rango ${range.startTime}-${range.endTime} es invalido.`, 400);
}
- const current = grouped.get(range.dayOfWeek) ?? []
- current.push({ start, end })
- grouped.set(range.dayOfWeek, current)
+ const current = grouped.get(range.dayOfWeek) ?? [];
+ current.push({ start, end });
+ grouped.set(range.dayOfWeek, current);
}
for (const ranges of grouped.values()) {
- ranges.sort((a, b) => a.start - b.start)
+ ranges.sort((a, b) => a.start - b.start);
for (let index = 1; index < ranges.length; index += 1) {
- const previous = ranges[index - 1]
- const current = ranges[index]
+ const previous = ranges[index - 1];
+ const current = ranges[index];
if (previous.end > current.start) {
- throw new CourtServiceError(
- 'Hay rangos horarios superpuestos para el mismo dia.',
- 400,
- )
+ throw new CourtServiceError('Hay rangos horarios superpuestos para el mismo dia.', 400);
}
}
}
@@ -92,16 +81,13 @@ async function ensureComplexAccess(complexId: string, appUserId: string) {
},
},
},
- })
+ });
if (!complexUser) {
- throw new CourtServiceError(
- 'No tienes permisos para administrar este complejo.',
- 403,
- )
+ throw new CourtServiceError('No tienes permisos para administrar este complejo.', 403);
}
- return complexUser.complex
+ return complexUser.complex;
}
async function ensureActiveSport(sportId: string) {
@@ -111,10 +97,10 @@ async function ensureActiveSport(sportId: string) {
isActive: true,
},
select: { id: true },
- })
+ });
if (!sport) {
- throw new CourtServiceError('El deporte seleccionado no existe o esta inactivo.', 400)
+ throw new CourtServiceError('El deporte seleccionado no existe o esta inactivo.', 400);
}
}
@@ -128,28 +114,26 @@ async function enforcePlanCourtLimit(complexId: string) {
},
},
},
- })
+ });
if (!complex?.plan) {
- return
+ return;
}
- const rules = parsePlanRules(complex.plan.rules)
+ const rules = parsePlanRules(complex.plan.rules);
const courtsCount = await db.court.count({
where: { complexId },
- })
+ });
const violations = evaluatePlanUsage(rules, {
courtsCount,
bookingsToday: 0,
- })
+ });
- const maxCourtViolation = violations.find(
- (violation) => violation.code === 'MAX_COURTS_REACHED',
- )
+ const maxCourtViolation = violations.find((violation) => violation.code === 'MAX_COURTS_REACHED');
if (maxCourtViolation) {
- throw new CourtServiceError(maxCourtViolation.message, 409)
+ throw new CourtServiceError(maxCourtViolation.message, 409);
}
}
@@ -175,7 +159,7 @@ async function getCourtByIdForUser(courtId: string, appUserId: string) {
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
},
- })
+ });
}
function mapCourtResponse(court: CourtWithRelations) {
@@ -208,11 +192,11 @@ function mapCourtResponse(court: CourtWithRelations) {
hasCustomPricing: court.priceRules.length > 0,
createdAt: court.createdAt.toISOString(),
updatedAt: court.updatedAt.toISOString(),
- }
+ };
}
export async function listCourtsByComplex(complexId: string, appUserId: string) {
- await ensureComplexAccess(complexId, appUserId)
+ await ensureComplexAccess(complexId, appUserId);
const courts = await db.court.findMany({
where: { complexId },
@@ -229,20 +213,16 @@ export async function listCourtsByComplex(complexId: string, appUserId: string)
orderBy: {
createdAt: 'asc',
},
- })
+ });
- return courts.map((court) => mapCourtResponse(court))
+ return courts.map((court) => mapCourtResponse(court));
}
-export async function createCourt(
- appUserId: string,
- complexId: string,
- input: CreateCourtInput,
-) {
- await ensureComplexAccess(complexId, appUserId)
- await ensureActiveSport(input.sportId)
- await enforcePlanCourtLimit(complexId)
- assertAvailabilityRanges(input.availability)
+export async function createCourt(appUserId: string, complexId: string, input: CreateCourtInput) {
+ await ensureComplexAccess(complexId, appUserId);
+ await ensureActiveSport(input.sportId);
+ await enforcePlanCourtLimit(complexId);
+ assertAvailabilityRanges(input.availability);
const createdCourt = await db.$transaction(async (tx) => {
const court = await tx.court.create({
@@ -254,7 +234,7 @@ export async function createCourt(
slotDurationMinutes: input.slotDurationMinutes,
basePrice: input.basePrice,
},
- })
+ });
await tx.courtAvailability.createMany({
data: input.availability.map((availability) => ({
@@ -264,37 +244,33 @@ export async function createCourt(
startTime: availability.startTime,
endTime: availability.endTime,
})),
- })
+ });
- return court
- })
+ return court;
+ });
- const court = await getCourtByIdForUser(createdCourt.id, appUserId)
+ const court = await getCourtByIdForUser(createdCourt.id, appUserId);
if (!court) {
- throw new CourtServiceError('No se pudo obtener la cancha creada.', 404)
+ throw new CourtServiceError('No se pudo obtener la cancha creada.', 404);
}
- return mapCourtResponse(court)
+ return mapCourtResponse(court);
}
-export async function updateCourt(
- appUserId: string,
- courtId: string,
- input: UpdateCourtInput,
-) {
- const existingCourt = await getCourtByIdForUser(courtId, appUserId)
+export async function updateCourt(appUserId: string, courtId: string, input: UpdateCourtInput) {
+ const existingCourt = await getCourtByIdForUser(courtId, appUserId);
if (!existingCourt) {
- throw new CourtServiceError('Cancha no encontrada.', 404)
+ throw new CourtServiceError('Cancha no encontrada.', 404);
}
if (input.sportId) {
- await ensureActiveSport(input.sportId)
+ await ensureActiveSport(input.sportId);
}
if (input.availability) {
- assertAvailabilityRanges(input.availability)
+ assertAvailabilityRanges(input.availability);
}
await db.$transaction(async (tx) => {
@@ -310,12 +286,12 @@ export async function updateCourt(
: {}),
...(input.basePrice !== undefined ? { basePrice: input.basePrice } : {}),
},
- })
+ });
if (input.availability) {
await tx.courtAvailability.deleteMany({
where: { courtId },
- })
+ });
await tx.courtAvailability.createMany({
data: input.availability.map((availability) => ({
@@ -325,15 +301,15 @@ export async function updateCourt(
startTime: availability.startTime,
endTime: availability.endTime,
})),
- })
+ });
}
- })
+ });
- const updatedCourt = await getCourtByIdForUser(courtId, appUserId)
+ const updatedCourt = await getCourtByIdForUser(courtId, appUserId);
if (!updatedCourt) {
- throw new CourtServiceError('Cancha no encontrada.', 404)
+ throw new CourtServiceError('Cancha no encontrada.', 404);
}
- return mapCourtResponse(updatedCourt)
+ return mapCourtResponse(updatedCourt);
}
diff --git a/apps/backend/src/modules/health-check/health-check.routes.ts b/apps/backend/src/modules/health-check/health-check.routes.ts
index 2142a86..33222f5 100644
--- a/apps/backend/src/modules/health-check/health-check.routes.ts
+++ b/apps/backend/src/modules/health-check/health-check.routes.ts
@@ -1,12 +1,14 @@
-import { AppEnv } from "@/types/hono";
-import { Hono } from "hono";
-
-export const healthCheckRoutes = new Hono()
+import { AppEnv } from '@/types/hono';
+import { Hono } from 'hono';
+export const healthCheckRoutes = new Hono();
healthCheckRoutes.get('/', (c) => {
- return c.json({
- status: 'healthy',
- timeStamp: new Date()
- }, 200)
-})
\ No newline at end of file
+ return c.json(
+ {
+ status: 'healthy',
+ timeStamp: new Date(),
+ },
+ 200
+ );
+});
diff --git a/apps/backend/src/modules/onboarding/handlers/complete-onboarding.handler.ts b/apps/backend/src/modules/onboarding/handlers/complete-onboarding.handler.ts
index af1a4b8..9b42b21 100644
--- a/apps/backend/src/modules/onboarding/handlers/complete-onboarding.handler.ts
+++ b/apps/backend/src/modules/onboarding/handlers/complete-onboarding.handler.ts
@@ -1,27 +1,27 @@
-import type { OnboardingCompleteInput } from '@repo/api-contract'
-import type { AppContext } from '@/types/hono'
import {
OnboardingError,
completeOnboarding,
-} from '@/modules/onboarding/services/onboarding.service'
+} from '@/modules/onboarding/services/onboarding.service';
+import type { AppContext } from '@/types/hono';
+import type { OnboardingCompleteInput } from '@repo/api-contract';
export async function completeOnboardingHandler(c: AppContext) {
- const payload = c.req.valid('json' as never) as OnboardingCompleteInput
+ const payload = c.req.valid('json' as never) as OnboardingCompleteInput;
try {
- const result = await completeOnboarding(payload)
+ const result = await completeOnboarding(payload);
return c.json({
message: 'Onboarding completado correctamente.',
...result,
- })
+ });
} catch (error) {
if (error instanceof OnboardingError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
if (error instanceof Error) {
- return c.json({ message: error.message }, 400)
+ return c.json({ message: error.message }, 400);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/onboarding/handlers/resend-otp.handler.ts b/apps/backend/src/modules/onboarding/handlers/resend-otp.handler.ts
index e51147f..98a5376 100644
--- a/apps/backend/src/modules/onboarding/handlers/resend-otp.handler.ts
+++ b/apps/backend/src/modules/onboarding/handlers/resend-otp.handler.ts
@@ -1,25 +1,25 @@
-import type { OnboardingResendOtpInput } from '@repo/api-contract'
-import type { AppContext } from '@/types/hono'
import {
OnboardingError,
resendOnboardingOtp,
-} from '@/modules/onboarding/services/onboarding.service'
+} from '@/modules/onboarding/services/onboarding.service';
+import type { AppContext } from '@/types/hono';
+import type { OnboardingResendOtpInput } from '@repo/api-contract';
export async function resendOtpHandler(c: AppContext) {
- const payload = c.req.valid('json' as never) as OnboardingResendOtpInput
+ const payload = c.req.valid('json' as never) as OnboardingResendOtpInput;
try {
- const result = await resendOnboardingOtp(payload)
- return c.json(result)
+ const result = await resendOnboardingOtp(payload);
+ return c.json(result);
} catch (error) {
if (error instanceof OnboardingError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
if (error instanceof Error) {
- return c.json({ message: error.message }, 400)
+ return c.json({ message: error.message }, 400);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/onboarding/handlers/start-onboarding.handler.ts b/apps/backend/src/modules/onboarding/handlers/start-onboarding.handler.ts
index 88bb3dd..598152f 100644
--- a/apps/backend/src/modules/onboarding/handlers/start-onboarding.handler.ts
+++ b/apps/backend/src/modules/onboarding/handlers/start-onboarding.handler.ts
@@ -1,10 +1,10 @@
-import type { OnboardingStartInput } from '@repo/api-contract'
-import type { AppContext } from '@/types/hono'
-import { startOnboarding } from '@/modules/onboarding/services/onboarding.service'
+import { startOnboarding } from '@/modules/onboarding/services/onboarding.service';
+import type { AppContext } from '@/types/hono';
+import type { OnboardingStartInput } from '@repo/api-contract';
export async function startOnboardingHandler(c: AppContext) {
- const payload = c.req.valid('json' as never) as OnboardingStartInput
+ const payload = c.req.valid('json' as never) as OnboardingStartInput;
- const result = await startOnboarding(payload)
- return c.json(result, 202)
+ const result = await startOnboarding(payload);
+ return c.json(result, 202);
}
diff --git a/apps/backend/src/modules/onboarding/handlers/verify-otp.handler.ts b/apps/backend/src/modules/onboarding/handlers/verify-otp.handler.ts
index 44927f0..285d3c3 100644
--- a/apps/backend/src/modules/onboarding/handlers/verify-otp.handler.ts
+++ b/apps/backend/src/modules/onboarding/handlers/verify-otp.handler.ts
@@ -1,10 +1,10 @@
-import type { OnboardingVerifyOtpInput } from '@repo/api-contract'
-import type { AppContext } from '@/types/hono'
-import { verifyOnboardingOtp } from '@/modules/onboarding/services/onboarding.service'
+import { verifyOnboardingOtp } from '@/modules/onboarding/services/onboarding.service';
+import type { AppContext } from '@/types/hono';
+import type { OnboardingVerifyOtpInput } from '@repo/api-contract';
export async function verifyOtpHandler(c: AppContext) {
- const payload = c.req.valid('json' as never) as OnboardingVerifyOtpInput
- const result = await verifyOnboardingOtp(payload)
+ const payload = c.req.valid('json' as never) as OnboardingVerifyOtpInput;
+ const result = await verifyOnboardingOtp(payload);
- return c.json(result)
+ return c.json(result);
}
diff --git a/apps/backend/src/modules/onboarding/onboarding.routes.ts b/apps/backend/src/modules/onboarding/onboarding.routes.ts
index a32a177..62c1029 100644
--- a/apps/backend/src/modules/onboarding/onboarding.routes.ts
+++ b/apps/backend/src/modules/onboarding/onboarding.routes.ts
@@ -1,39 +1,35 @@
-import { zValidator } from '@hono/zod-validator'
+import { completeOnboardingHandler } from '@/modules/onboarding/handlers/complete-onboarding.handler';
+import { resendOtpHandler } from '@/modules/onboarding/handlers/resend-otp.handler';
+import { startOnboardingHandler } from '@/modules/onboarding/handlers/start-onboarding.handler';
+import { verifyOtpHandler } from '@/modules/onboarding/handlers/verify-otp.handler';
+import type { AppEnv } from '@/types/hono';
+import { zValidator } from '@hono/zod-validator';
import {
onboardingCompleteSchema,
onboardingResendOtpSchema,
onboardingStartSchema,
onboardingVerifyOtpSchema,
-} from '@repo/api-contract'
-import { Hono } from 'hono'
-import { completeOnboardingHandler } from '@/modules/onboarding/handlers/complete-onboarding.handler'
-import { resendOtpHandler } from '@/modules/onboarding/handlers/resend-otp.handler'
-import { startOnboardingHandler } from '@/modules/onboarding/handlers/start-onboarding.handler'
-import { verifyOtpHandler } from '@/modules/onboarding/handlers/verify-otp.handler'
-import type { AppEnv } from '@/types/hono'
+} from '@repo/api-contract';
+import { Hono } from 'hono';
-export const onboardingRoutes = new Hono()
+export const onboardingRoutes = new Hono();
-onboardingRoutes.post(
- '/start',
- zValidator('json', onboardingStartSchema),
- startOnboardingHandler,
-)
+onboardingRoutes.post('/start', zValidator('json', onboardingStartSchema), startOnboardingHandler);
onboardingRoutes.post(
'/verify-otp',
zValidator('json', onboardingVerifyOtpSchema),
- verifyOtpHandler,
-)
+ verifyOtpHandler
+);
onboardingRoutes.post(
'/resend-otp',
zValidator('json', onboardingResendOtpSchema),
- resendOtpHandler,
-)
+ resendOtpHandler
+);
onboardingRoutes.post(
'/complete',
zValidator('json', onboardingCompleteSchema),
- completeOnboardingHandler,
-)
+ completeOnboardingHandler
+);
diff --git a/apps/backend/src/modules/onboarding/services/onboarding.service.ts b/apps/backend/src/modules/onboarding/services/onboarding.service.ts
index 5c713d0..773c3ed 100644
--- a/apps/backend/src/modules/onboarding/services/onboarding.service.ts
+++ b/apps/backend/src/modules/onboarding/services/onboarding.service.ts
@@ -1,86 +1,86 @@
-import { createHash, randomInt } from 'node:crypto'
-import { v7 as uuidv7 } from 'uuid'
-import type { User as SupabaseAuthUser } from '@supabase/supabase-js'
+import { createHash, randomInt } from 'node:crypto';
+import { sendMail } from '@/lib/mailer';
+import { db } from '@/lib/prisma';
+import { getSupabaseAdminClient } from '@/lib/supabase-admin';
import type {
OnboardingCompleteInput,
OnboardingResendOtpInput,
OnboardingStartInput,
OnboardingVerifyOtpInput,
-} from '@repo/api-contract'
-import { db } from '@/lib/prisma'
-import { sendMail } from '@/lib/mailer'
-import { getSupabaseAdminClient } from '@/lib/supabase-admin'
+} from '@repo/api-contract';
+import type { User as SupabaseAuthUser } from '@supabase/supabase-js';
+import { v7 as uuidv7 } from 'uuid';
-const OTP_LENGTH = 6
-const OTP_TTL_MINUTES = Number(Bun.env.ONBOARDING_OTP_TTL_MINUTES ?? 10)
-const OTP_MAX_ATTEMPTS = Number(Bun.env.ONBOARDING_OTP_MAX_ATTEMPTS ?? 5)
-const OTP_RESEND_COOLDOWN_SECONDS = Number(
- Bun.env.ONBOARDING_OTP_RESEND_COOLDOWN_SECONDS ?? 30,
-)
+const OTP_LENGTH = 6;
+const OTP_TTL_MINUTES = Number(Bun.env.ONBOARDING_OTP_TTL_MINUTES ?? 10);
+const OTP_MAX_ATTEMPTS = Number(Bun.env.ONBOARDING_OTP_MAX_ATTEMPTS ?? 5);
+const OTP_RESEND_COOLDOWN_SECONDS = Number(Bun.env.ONBOARDING_OTP_RESEND_COOLDOWN_SECONDS ?? 30);
type VerifyOtpResult = {
- message: string
- verified: boolean
- requestId: string
- email: string | null
- expiresAt: string | null
- remainingAttempts: number
- cooldownSeconds: number
-}
+ message: string;
+ verified: boolean;
+ requestId: string;
+ email: string | null;
+ expiresAt: string | null;
+ remainingAttempts: number;
+ cooldownSeconds: number;
+};
type ResendOtpResult = {
- message: string
- requestId: string
- email: string
- expiresAt: string
- cooldownSeconds: number
- remainingAttempts: number
-}
+ message: string;
+ requestId: string;
+ email: string;
+ expiresAt: string;
+ cooldownSeconds: number;
+ remainingAttempts: number;
+};
type StartOnboardingResult = {
- message: string
- requestId: string
- email: string
- expiresAt: string
- cooldownSeconds: number
-}
+ message: string;
+ requestId: string;
+ email: string;
+ expiresAt: string;
+ cooldownSeconds: number;
+};
export class OnboardingError extends Error {
- status: 400 | 404 | 409 | 429
+ status: 400 | 404 | 409 | 429;
constructor(message: string, status: 400 | 404 | 409 | 429 = 400) {
- super(message)
- this.name = 'OnboardingError'
- this.status = status
+ super(message);
+ this.name = 'OnboardingError';
+ this.status = status;
}
}
function nowPlusMinutes(minutes: number): Date {
- const date = new Date()
- date.setMinutes(date.getMinutes() + minutes)
- return date
+ const date = new Date();
+ date.setMinutes(date.getMinutes() + minutes);
+ return date;
}
function normalizeEmail(email: string): string {
- return email.trim().toLowerCase()
+ return email.trim().toLowerCase();
}
function createOtpCode(): string {
- return randomInt(0, 10 ** OTP_LENGTH).toString().padStart(OTP_LENGTH, '0')
+ return randomInt(0, 10 ** OTP_LENGTH)
+ .toString()
+ .padStart(OTP_LENGTH, '0');
}
function hashValue(value: string): string {
- return createHash('sha256').update(value).digest('hex')
+ return createHash('sha256').update(value).digest('hex');
}
function getRemainingAttempts(otpAttempts: number): number {
- return Math.max(0, OTP_MAX_ATTEMPTS - otpAttempts)
+ return Math.max(0, OTP_MAX_ATTEMPTS - otpAttempts);
}
function getCooldownSeconds(otpLastSentAt: Date): number {
- const elapsedMs = Date.now() - otpLastSentAt.getTime()
- const remainingMs = OTP_RESEND_COOLDOWN_SECONDS * 1000 - elapsedMs
- return Math.max(0, Math.ceil(remainingMs / 1000))
+ const elapsedMs = Date.now() - otpLastSentAt.getTime();
+ const remainingMs = OTP_RESEND_COOLDOWN_SECONDS * 1000 - elapsedMs;
+ return Math.max(0, Math.ceil(remainingMs / 1000));
}
function slugify(value: string): string {
@@ -91,28 +91,28 @@ function slugify(value: string): string {
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
- .replace(/-+/g, '-')
+ .replace(/-+/g, '-');
}
async function buildUniqueComplexSlug(complexName: string): Promise {
- const base = slugify(complexName)
- const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`
+ const base = slugify(complexName);
+ const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`;
- let candidate = fallback
- let index = 1
+ let candidate = fallback;
+ let index = 1;
while (true) {
const existing = await db.complex.findFirst({
where: { complexSlug: candidate },
select: { id: true },
- })
+ });
if (!existing) {
- return candidate
+ return candidate;
}
- index += 1
- candidate = `${fallback}-${index}`
+ index += 1;
+ candidate = `${fallback}-${index}`;
}
}
@@ -122,70 +122,61 @@ async function sendOtpEmail(email: string, otpCode: string) {
subject: 'Codigo OTP para validar tu email',
text: `Tu codigo OTP es ${otpCode}. Vence en ${OTP_TTL_MINUTES} minutos.`,
html: `Tu codigo OTP es ${otpCode} .
Vence en ${OTP_TTL_MINUTES} minutos.
`,
- })
+ });
}
-async function findSupabaseUserByEmail(
- email: string,
-): Promise {
- let page = 1
- const perPage = 100
+async function findSupabaseUserByEmail(email: string): Promise {
+ let page = 1;
+ const perPage = 100;
while (page <= 10) {
const { data, error } = await getSupabaseAdminClient().auth.admin.listUsers({
page,
perPage,
- })
+ });
if (error) {
- throw new Error(`No se pudo consultar usuarios en Supabase: ${error.message}`)
+ throw new Error(`No se pudo consultar usuarios en Supabase: ${error.message}`);
}
- const found = data.users.find(
- (user) => user.email?.toLowerCase() === email.toLowerCase(),
- )
+ const found = data.users.find((user) => user.email?.toLowerCase() === email.toLowerCase());
if (found) {
- return found
+ return found;
}
if (data.users.length < perPage) {
- return null
+ return null;
}
- page += 1
+ page += 1;
}
- return null
+ return null;
}
async function ensureSupabaseUser(params: {
- email: string
- fullName: string
- password: string
+ email: string;
+ fullName: string;
+ password: string;
}): Promise {
- const existingUser = await findSupabaseUserByEmail(params.email)
+ const existingUser = await findSupabaseUserByEmail(params.email);
if (existingUser) {
- const { error } = await getSupabaseAdminClient().auth.admin.updateUserById(
- existingUser.id,
- {
- password: params.password,
- email_confirm: true,
- user_metadata: {
- full_name: params.fullName,
- name: params.fullName,
- },
+ const { error } = await getSupabaseAdminClient().auth.admin.updateUserById(existingUser.id, {
+ password: params.password,
+ email_confirm: true,
+ user_metadata: {
+ full_name: params.fullName,
+ name: params.fullName,
},
- )
+ });
if (error) {
- throw new Error(
- `No se pudo actualizar usuario existente en Supabase: ${error.message}`,
- )
+ throw new Error(`No se pudo actualizar usuario existente en Supabase: ${error.message}`);
}
- return existingUser.id
+ return existingUser.id;
}
const { data, error } = await getSupabaseAdminClient().auth.admin.createUser({
@@ -196,24 +187,20 @@ async function ensureSupabaseUser(params: {
full_name: params.fullName,
name: params.fullName,
},
- })
+ });
if (error || !data.user) {
- throw new Error(
- `No se pudo crear usuario en Supabase: ${error?.message ?? 'unknown error'}`,
- )
+ throw new Error(`No se pudo crear usuario en Supabase: ${error?.message ?? 'unknown error'}`);
}
- return data.user.id
+ return data.user.id;
}
-export async function startOnboarding(
- input: OnboardingStartInput,
-): Promise {
- const email = normalizeEmail(input.email)
- const otpCode = createOtpCode()
- const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES)
- const now = new Date()
+export async function startOnboarding(input: OnboardingStartInput): Promise {
+ const email = normalizeEmail(input.email);
+ const otpCode = createOtpCode();
+ const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
+ const now = new Date();
const request = await db.onboardingRequest.create({
data: {
@@ -231,9 +218,9 @@ export async function startOnboarding(
email: true,
otpExpiresAt: true,
},
- })
+ });
- await sendOtpEmail(email, otpCode)
+ await sendOtpEmail(email, otpCode);
return {
message: 'Te enviamos un codigo OTP para validar tu direccion.',
@@ -241,15 +228,15 @@ export async function startOnboarding(
email: request.email,
expiresAt: request.otpExpiresAt.toISOString(),
cooldownSeconds: OTP_RESEND_COOLDOWN_SECONDS,
- }
+ };
}
export async function verifyOnboardingOtp(
- input: OnboardingVerifyOtpInput,
+ input: OnboardingVerifyOtpInput
): Promise {
const request = await db.onboardingRequest.findUnique({
where: { id: input.requestId },
- })
+ });
if (!request) {
return {
@@ -260,7 +247,7 @@ export async function verifyOnboardingOtp(
expiresAt: null,
remainingAttempts: 0,
cooldownSeconds: 0,
- }
+ };
}
if (request.completedAt) {
@@ -272,7 +259,7 @@ export async function verifyOnboardingOtp(
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: getRemainingAttempts(request.otpAttempts),
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
- }
+ };
}
if (request.otpExpiresAt < new Date()) {
@@ -284,7 +271,7 @@ export async function verifyOnboardingOtp(
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: getRemainingAttempts(request.otpAttempts),
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
- }
+ };
}
if (request.otpAttempts >= OTP_MAX_ATTEMPTS) {
@@ -296,10 +283,10 @@ export async function verifyOnboardingOtp(
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: 0,
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
- }
+ };
}
- const otpMatches = hashValue(input.otp) === request.otpHash
+ const otpMatches = hashValue(input.otp) === request.otpHash;
if (!otpMatches) {
const updated = await db.onboardingRequest.update({
@@ -316,9 +303,9 @@ export async function verifyOnboardingOtp(
otpExpiresAt: true,
otpLastSentAt: true,
},
- })
+ });
- const remainingAttempts = getRemainingAttempts(updated.otpAttempts)
+ const remainingAttempts = getRemainingAttempts(updated.otpAttempts);
return {
message:
@@ -331,14 +318,14 @@ export async function verifyOnboardingOtp(
expiresAt: updated.otpExpiresAt.toISOString(),
remainingAttempts,
cooldownSeconds: getCooldownSeconds(updated.otpLastSentAt),
- }
+ };
}
if (!request.emailVerifiedAt) {
await db.onboardingRequest.update({
where: { id: request.id },
data: { emailVerifiedAt: new Date() },
- })
+ });
}
return {
@@ -349,39 +336,36 @@ export async function verifyOnboardingOtp(
expiresAt: request.otpExpiresAt.toISOString(),
remainingAttempts: getRemainingAttempts(request.otpAttempts),
cooldownSeconds: getCooldownSeconds(request.otpLastSentAt),
- }
+ };
}
export async function resendOnboardingOtp(
- input: OnboardingResendOtpInput,
+ input: OnboardingResendOtpInput
): Promise {
const request = await db.onboardingRequest.findUnique({
where: { id: input.requestId },
- })
+ });
if (!request) {
- throw new OnboardingError('Solicitud de onboarding invalida o expirada.', 404)
+ throw new OnboardingError('Solicitud de onboarding invalida o expirada.', 404);
}
if (request.completedAt) {
- throw new OnboardingError('Este onboarding ya fue completado.', 400)
+ throw new OnboardingError('Este onboarding ya fue completado.', 400);
}
if (request.emailVerifiedAt) {
- throw new OnboardingError('El email ya fue verificado.', 400)
+ throw new OnboardingError('El email ya fue verificado.', 400);
}
- const cooldownSeconds = getCooldownSeconds(request.otpLastSentAt)
+ const cooldownSeconds = getCooldownSeconds(request.otpLastSentAt);
if (cooldownSeconds > 0) {
- throw new OnboardingError(
- `Debes esperar ${cooldownSeconds}s antes de reenviar el OTP.`,
- 429,
- )
+ throw new OnboardingError(`Debes esperar ${cooldownSeconds}s antes de reenviar el OTP.`, 429);
}
- const otpCode = createOtpCode()
- const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES)
- const now = new Date()
+ const otpCode = createOtpCode();
+ const expiresAt = nowPlusMinutes(OTP_TTL_MINUTES);
+ const now = new Date();
const updated = await db.onboardingRequest.update({
where: { id: request.id },
@@ -400,9 +384,9 @@ export async function resendOnboardingOtp(
otpExpiresAt: true,
otpAttempts: true,
},
- })
+ });
- await sendOtpEmail(updated.email, otpCode)
+ await sendOtpEmail(updated.email, otpCode);
return {
message: 'Te enviamos un nuevo codigo OTP.',
@@ -411,49 +395,46 @@ export async function resendOnboardingOtp(
expiresAt: updated.otpExpiresAt.toISOString(),
cooldownSeconds: OTP_RESEND_COOLDOWN_SECONDS,
remainingAttempts: getRemainingAttempts(updated.otpAttempts),
- }
+ };
}
export async function completeOnboarding(input: OnboardingCompleteInput) {
const onboardingRequest = await db.onboardingRequest.findUnique({
where: { id: input.onboardingRequestId },
- })
+ });
if (!onboardingRequest) {
- throw new OnboardingError('Solicitud de onboarding invalida o expirada.', 400)
+ throw new OnboardingError('Solicitud de onboarding invalida o expirada.', 400);
}
if (onboardingRequest.otpExpiresAt < new Date()) {
- throw new OnboardingError(
- 'La sesion de onboarding expiro. Solicita un nuevo OTP.',
- 400,
- )
+ throw new OnboardingError('La sesion de onboarding expiro. Solicita un nuevo OTP.', 400);
}
if (!onboardingRequest.emailVerifiedAt) {
- throw new OnboardingError('Debes verificar el email antes de continuar.', 400)
+ throw new OnboardingError('Debes verificar el email antes de continuar.', 400);
}
if (onboardingRequest.completedAt) {
- throw new OnboardingError('Este onboarding ya fue completado.', 400)
+ throw new OnboardingError('Este onboarding ya fue completado.', 400);
}
const plan = await db.plan.findUnique({
where: { code: input.planCode },
select: { code: true },
- })
+ });
if (!plan) {
- throw new OnboardingError('El plan seleccionado no existe.', 400)
+ throw new OnboardingError('El plan seleccionado no existe.', 400);
}
const supabaseUserId = await ensureSupabaseUser({
email: onboardingRequest.email,
fullName: onboardingRequest.fullName,
password: input.password,
- })
+ });
- const complexSlug = await buildUniqueComplexSlug(input.complexName)
+ const complexSlug = await buildUniqueComplexSlug(input.complexName);
const result = await db.$transaction(async (tx) => {
const user = await tx.user.upsert({
@@ -468,7 +449,7 @@ export async function completeOnboarding(input: OnboardingCompleteInput) {
email: onboardingRequest.email,
supabaseUserId,
},
- })
+ });
const complex = await tx.complex.create({
data: {
@@ -482,7 +463,7 @@ export async function completeOnboarding(input: OnboardingCompleteInput) {
adminEmail: onboardingRequest.email,
planCode: input.planCode,
},
- })
+ });
await tx.complexUser.create({
data: {
@@ -490,24 +471,24 @@ export async function completeOnboarding(input: OnboardingCompleteInput) {
userId: user.id,
role: 'ADMIN',
},
- })
+ });
await tx.onboardingRequest.update({
where: { id: onboardingRequest.id },
data: {
completedAt: new Date(),
},
- })
+ });
return {
userId: user.id,
complexId: complex.id,
complexSlug: complex.complexSlug,
- }
- })
+ };
+ });
return {
...result,
supabaseUserId,
- }
+ };
}
diff --git a/apps/backend/src/modules/plan/handlers/list-plans.handler.ts b/apps/backend/src/modules/plan/handlers/list-plans.handler.ts
index c110348..ec2f6e9 100644
--- a/apps/backend/src/modules/plan/handlers/list-plans.handler.ts
+++ b/apps/backend/src/modules/plan/handlers/list-plans.handler.ts
@@ -1,5 +1,5 @@
-import type { AppContext } from '@/types/hono'
-import { db } from '@/lib/prisma'
+import { db } from '@/lib/prisma';
+import type { AppContext } from '@/types/hono';
export async function listPlansHandler(c: AppContext) {
const plans = await db.plan.findMany({
@@ -11,13 +11,13 @@ export async function listPlansHandler(c: AppContext) {
orderBy: {
price: 'asc',
},
- })
+ });
return c.json(
plans.map((plan) => ({
code: plan.code,
name: plan.name,
price: Number(plan.price),
- })),
- )
+ }))
+ );
}
diff --git a/apps/backend/src/modules/plan/plan.routes.ts b/apps/backend/src/modules/plan/plan.routes.ts
index 8a3b16b..5bf26dd 100644
--- a/apps/backend/src/modules/plan/plan.routes.ts
+++ b/apps/backend/src/modules/plan/plan.routes.ts
@@ -1,7 +1,7 @@
-import { Hono } from 'hono'
-import { listPlansHandler } from '@/modules/plan/handlers/list-plans.handler'
-import type { AppEnv } from '@/types/hono'
+import { listPlansHandler } from '@/modules/plan/handlers/list-plans.handler';
+import type { AppEnv } from '@/types/hono';
+import { Hono } from 'hono';
-export const planRoutes = new Hono()
+export const planRoutes = new Hono();
-planRoutes.get('/', listPlansHandler)
+planRoutes.get('/', listPlansHandler);
diff --git a/apps/backend/src/modules/plan/services/plan-rules.service.ts b/apps/backend/src/modules/plan/services/plan-rules.service.ts
index c3a21a0..7975056 100644
--- a/apps/backend/src/modules/plan/services/plan-rules.service.ts
+++ b/apps/backend/src/modules/plan/services/plan-rules.service.ts
@@ -1,44 +1,41 @@
-import type { PlanRules } from '@repo/api-contract'
-import { planRulesSchema } from '@repo/api-contract'
+import type { PlanRules } from '@repo/api-contract';
+import { planRulesSchema } from '@repo/api-contract';
export type PlanUsageSnapshot = {
- courtsCount: number
- bookingsToday: number
- activeUsersCount?: number
-}
+ courtsCount: number;
+ bookingsToday: number;
+ activeUsersCount?: number;
+};
export type PlanViolationCode =
| 'MAX_COURTS_REACHED'
| 'MAX_BOOKINGS_PER_DAY_REACHED'
- | 'MAX_ACTIVE_USERS_REACHED'
+ | 'MAX_ACTIVE_USERS_REACHED';
export type PlanViolation = {
- code: PlanViolationCode
- message: string
-}
+ code: PlanViolationCode;
+ message: string;
+};
export function parsePlanRules(input: unknown): PlanRules {
- return planRulesSchema.parse(input)
+ return planRulesSchema.parse(input);
}
-export function evaluatePlanUsage(
- rules: PlanRules,
- usage: PlanUsageSnapshot,
-): PlanViolation[] {
- const violations: PlanViolation[] = []
+export function evaluatePlanUsage(rules: PlanRules, usage: PlanUsageSnapshot): PlanViolation[] {
+ const violations: PlanViolation[] = [];
if (usage.courtsCount >= rules.limits.maxCourts) {
violations.push({
code: 'MAX_COURTS_REACHED',
message: `El plan permite hasta ${rules.limits.maxCourts} canchas.`,
- })
+ });
}
if (usage.bookingsToday >= rules.limits.maxBookingsPerDay) {
violations.push({
code: 'MAX_BOOKINGS_PER_DAY_REACHED',
message: `El plan permite hasta ${rules.limits.maxBookingsPerDay} turnos por dia.`,
- })
+ });
}
if (
@@ -49,15 +46,12 @@ export function evaluatePlanUsage(
violations.push({
code: 'MAX_ACTIVE_USERS_REACHED',
message: `El plan permite hasta ${rules.limits.maxActiveUsers} usuarios activos.`,
- })
+ });
}
- return violations
+ return violations;
}
-export function isFeatureEnabled(
- rules: PlanRules,
- feature: keyof PlanRules['features'],
-): boolean {
- return rules.features[feature]
+export function isFeatureEnabled(rules: PlanRules, feature: keyof PlanRules['features']): boolean {
+ return rules.features[feature];
}
diff --git a/apps/backend/src/modules/public-booking/handlers/create-public-booking.handler.ts b/apps/backend/src/modules/public-booking/handlers/create-public-booking.handler.ts
index e52ed5a..4c11372 100644
--- a/apps/backend/src/modules/public-booking/handlers/create-public-booking.handler.ts
+++ b/apps/backend/src/modules/public-booking/handlers/create-public-booking.handler.ts
@@ -1,24 +1,24 @@
-import type { CreatePublicBookingInput } from '@repo/api-contract'
import {
PublicBookingServiceError,
createPublicBooking,
-} from '@/modules/public-booking/services/public-booking.service'
-import type { AppContext } from '@/types/hono'
+} from '@/modules/public-booking/services/public-booking.service';
+import type { AppContext } from '@/types/hono';
+import type { CreatePublicBookingInput } from '@repo/api-contract';
-type ComplexSlugParams = { complexSlug: string }
+type ComplexSlugParams = { complexSlug: string };
export async function createPublicBookingHandler(c: AppContext) {
- const { complexSlug } = c.req.valid('param' as never) as ComplexSlugParams
- const payload = c.req.valid('json' as never) as CreatePublicBookingInput
+ const { complexSlug } = c.req.valid('param' as never) as ComplexSlugParams;
+ const payload = c.req.valid('json' as never) as CreatePublicBookingInput;
try {
- const booking = await createPublicBooking(complexSlug, payload)
- return c.json(booking, 201)
+ const booking = await createPublicBooking(complexSlug, payload);
+ return c.json(booking, 201);
} catch (error) {
if (error instanceof PublicBookingServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/public-booking/handlers/get-public-booking-confirmation.handler.ts b/apps/backend/src/modules/public-booking/handlers/get-public-booking-confirmation.handler.ts
index 471ca6b..3911cc7 100644
--- a/apps/backend/src/modules/public-booking/handlers/get-public-booking-confirmation.handler.ts
+++ b/apps/backend/src/modules/public-booking/handlers/get-public-booking-confirmation.handler.ts
@@ -1,25 +1,25 @@
import {
PublicBookingServiceError,
getPublicBookingConfirmation,
-} from '@/modules/public-booking/services/public-booking.service'
-import type { AppContext } from '@/types/hono'
+} from '@/modules/public-booking/services/public-booking.service';
+import type { AppContext } from '@/types/hono';
type BookingCodeParams = {
- complexSlug: string
- bookingCode: string
-}
+ complexSlug: string;
+ bookingCode: string;
+};
export async function getPublicBookingConfirmationHandler(c: AppContext) {
- const { complexSlug, bookingCode } = c.req.valid('param' as never) as BookingCodeParams
+ const { complexSlug, bookingCode } = c.req.valid('param' as never) as BookingCodeParams;
try {
- const booking = await getPublicBookingConfirmation(complexSlug, bookingCode)
- return c.json(booking)
+ const booking = await getPublicBookingConfirmation(complexSlug, bookingCode);
+ return c.json(booking);
} catch (error) {
if (error instanceof PublicBookingServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/public-booking/handlers/list-public-availability.handler.ts b/apps/backend/src/modules/public-booking/handlers/list-public-availability.handler.ts
index 283d946..8ac4490 100644
--- a/apps/backend/src/modules/public-booking/handlers/list-public-availability.handler.ts
+++ b/apps/backend/src/modules/public-booking/handlers/list-public-availability.handler.ts
@@ -1,24 +1,24 @@
-import type { PublicAvailabilityQuery } from '@repo/api-contract'
import {
PublicBookingServiceError,
listPublicAvailability,
-} from '@/modules/public-booking/services/public-booking.service'
-import type { AppContext } from '@/types/hono'
+} from '@/modules/public-booking/services/public-booking.service';
+import type { AppContext } from '@/types/hono';
+import type { PublicAvailabilityQuery } from '@repo/api-contract';
-type ComplexSlugParams = { complexSlug: string }
+type ComplexSlugParams = { complexSlug: string };
export async function listPublicAvailabilityHandler(c: AppContext) {
- const { complexSlug } = c.req.valid('param' as never) as ComplexSlugParams
- const query = c.req.valid('query' as never) as PublicAvailabilityQuery
+ const { complexSlug } = c.req.valid('param' as never) as ComplexSlugParams;
+ const query = c.req.valid('query' as never) as PublicAvailabilityQuery;
try {
- const availability = await listPublicAvailability(complexSlug, query)
- return c.json(availability)
+ const availability = await listPublicAvailability(complexSlug, query);
+ return c.json(availability);
} catch (error) {
if (error instanceof PublicBookingServiceError) {
- return c.json({ message: error.message }, error.status)
+ return c.json({ message: error.message }, error.status);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/public-booking/public-booking.routes.ts b/apps/backend/src/modules/public-booking/public-booking.routes.ts
index 17845a0..ab5732c 100644
--- a/apps/backend/src/modules/public-booking/public-booking.routes.ts
+++ b/apps/backend/src/modules/public-booking/public-booking.routes.ts
@@ -1,38 +1,35 @@
-import { zValidator } from '@hono/zod-validator'
-import {
- createPublicBookingSchema,
- publicAvailabilityQuerySchema,
-} from '@repo/api-contract'
-import { Hono } from 'hono'
-import { z } from 'zod'
-import { createPublicBookingHandler } from '@/modules/public-booking/handlers/create-public-booking.handler'
-import { getPublicBookingConfirmationHandler } from '@/modules/public-booking/handlers/get-public-booking-confirmation.handler'
-import { listPublicAvailabilityHandler } from '@/modules/public-booking/handlers/list-public-availability.handler'
-import type { AppEnv } from '@/types/hono'
+import { createPublicBookingHandler } from '@/modules/public-booking/handlers/create-public-booking.handler';
+import { getPublicBookingConfirmationHandler } from '@/modules/public-booking/handlers/get-public-booking-confirmation.handler';
+import { listPublicAvailabilityHandler } from '@/modules/public-booking/handlers/list-public-availability.handler';
+import type { AppEnv } from '@/types/hono';
+import { zValidator } from '@hono/zod-validator';
+import { createPublicBookingSchema, publicAvailabilityQuerySchema } from '@repo/api-contract';
+import { Hono } from 'hono';
+import { z } from 'zod';
-export const publicBookingRoutes = new Hono()
-const complexSlugParamsSchema = z.object({ complexSlug: z.string().trim().min(1) })
+export const publicBookingRoutes = new Hono();
+const complexSlugParamsSchema = z.object({ complexSlug: z.string().trim().min(1) });
const confirmationParamsSchema = z.object({
complexSlug: z.string().trim().min(1),
bookingCode: z.string().trim().min(6).max(8),
-})
+});
publicBookingRoutes.get(
'/complex/:complexSlug/availability',
zValidator('param', complexSlugParamsSchema),
zValidator('query', publicAvailabilityQuerySchema),
- listPublicAvailabilityHandler,
-)
+ listPublicAvailabilityHandler
+);
publicBookingRoutes.post(
'/complex/:complexSlug',
zValidator('param', complexSlugParamsSchema),
zValidator('json', createPublicBookingSchema),
- createPublicBookingHandler,
-)
+ createPublicBookingHandler
+);
publicBookingRoutes.get(
'/complex/:complexSlug/confirmation/:bookingCode',
zValidator('param', confirmationParamsSchema),
- getPublicBookingConfirmationHandler,
-)
+ getPublicBookingConfirmationHandler
+);
diff --git a/apps/backend/src/modules/public-booking/services/public-booking.service.ts b/apps/backend/src/modules/public-booking/services/public-booking.service.ts
index 941b524..b27c4de 100644
--- a/apps/backend/src/modules/public-booking/services/public-booking.service.ts
+++ b/apps/backend/src/modules/public-booking/services/public-booking.service.ts
@@ -1,19 +1,19 @@
+import { randomInt } from 'node:crypto';
+import { db } from '@/lib/prisma';
+import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type {
CreatePublicBookingInput,
DayOfWeek,
PublicAvailabilityQuery,
-} from '@repo/api-contract'
-import { randomInt } from 'node:crypto'
-import { v7 as uuidv7 } from 'uuid'
-import { db } from '@/lib/prisma'
-import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service'
+} from '@repo/api-contract';
+import { v7 as uuidv7 } from 'uuid';
type Slot = {
- startTime: string
- endTime: string
-}
+ startTime: string;
+ endTime: string;
+};
-type ComplexWithPublicBookingData = Awaited>
+type ComplexWithPublicBookingData = Awaited>;
const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
'SUNDAY',
@@ -23,44 +23,44 @@ const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
'THURSDAY',
'FRIDAY',
'SATURDAY',
-]
-const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
-const BOOKING_CODE_LENGTH = 6
+];
+const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
+const BOOKING_CODE_LENGTH = 6;
export class PublicBookingServiceError extends Error {
- status: 400 | 403 | 404 | 409
+ status: 400 | 403 | 404 | 409;
constructor(message: string, status: 400 | 403 | 404 | 409) {
- super(message)
- this.name = 'PublicBookingServiceError'
- this.status = status
+ super(message);
+ this.name = 'PublicBookingServiceError';
+ this.status = status;
}
}
function toMinutes(value: string): number {
- const [hours, minutes] = value.split(':').map((part) => Number(part))
- return hours * 60 + minutes
+ const [hours, minutes] = value.split(':').map((part) => Number(part));
+ return hours * 60 + minutes;
}
function minutesToTime(minutes: number): string {
- const safeMinutes = Math.max(0, minutes)
- const hours = Math.floor(safeMinutes / 60)
- const mins = safeMinutes % 60
- return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`
+ const safeMinutes = Math.max(0, minutes);
+ const hours = Math.floor(safeMinutes / 60);
+ const mins = safeMinutes % 60;
+ return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
function parseIsoDate(date: string): { bookingDate: Date; dayOfWeek: DayOfWeek } {
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date)
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
if (!match) {
- throw new PublicBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400)
+ throw new PublicBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
}
- const year = Number(match[1])
- const month = Number(match[2])
- const day = Number(match[3])
+ const year = Number(match[1]);
+ const month = Number(match[2]);
+ const day = Number(match[3]);
- const bookingDate = new Date(Date.UTC(year, month - 1, day))
+ const bookingDate = new Date(Date.UTC(year, month - 1, day));
if (
Number.isNaN(bookingDate.getTime()) ||
@@ -68,48 +68,48 @@ function parseIsoDate(date: string): { bookingDate: Date; dayOfWeek: DayOfWeek }
bookingDate.getUTCMonth() + 1 !== month ||
bookingDate.getUTCDate() !== day
) {
- throw new PublicBookingServiceError('La fecha enviada no es valida.', 400)
+ throw new PublicBookingServiceError('La fecha enviada no es valida.', 400);
}
- const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()]
+ const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()];
if (!dayOfWeek) {
- throw new PublicBookingServiceError('No se pudo resolver el dia de la semana.', 400)
+ throw new PublicBookingServiceError('No se pudo resolver el dia de la semana.', 400);
}
- return { bookingDate, dayOfWeek }
+ return { bookingDate, dayOfWeek };
}
function formatIsoDate(date: Date): string {
- return date.toISOString().slice(0, 10)
+ return date.toISOString().slice(0, 10);
}
function generateBookingCode(): string {
- let code = ''
+ let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
- code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)]
+ code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
- return code
+ return code;
}
function hasOverlap(slot: Slot, existing: Slot): boolean {
- return slot.startTime < existing.endTime && slot.endTime > existing.startTime
+ return slot.startTime < existing.endTime && slot.endTime > existing.startTime;
}
function buildSlots(
availability: Array<{
- startTime: string
- endTime: string
+ startTime: string;
+ endTime: string;
}>,
- slotDurationMinutes: number,
+ slotDurationMinutes: number
): Slot[] {
- const slots: Slot[] = []
+ const slots: Slot[] = [];
for (const range of availability) {
- const start = toMinutes(range.startTime)
- const end = toMinutes(range.endTime)
+ const start = toMinutes(range.startTime);
+ const end = toMinutes(range.endTime);
for (
let current = start;
@@ -119,19 +119,19 @@ function buildSlots(
slots.push({
startTime: minutesToTime(current),
endTime: minutesToTime(current + slotDurationMinutes),
- })
+ });
}
}
- return slots
+ return slots;
}
function resolveSports(data: ComplexWithPublicBookingData) {
- const map = new Map()
+ const map = new Map();
for (const court of data.courts) {
if (!court.sport.isActive) {
- continue
+ continue;
}
if (!map.has(court.sport.id)) {
@@ -139,41 +139,41 @@ function resolveSports(data: ComplexWithPublicBookingData) {
id: court.sport.id,
name: court.sport.name,
slug: court.sport.slug,
- })
+ });
}
}
- return [...map.values()].sort((a, b) => a.name.localeCompare(b.name, 'es'))
+ return [...map.values()].sort((a, b) => a.name.localeCompare(b.name, 'es'));
}
function validateSportSelection(
availableSports: Array<{ id: string }>,
- sportId: string | undefined,
+ sportId: string | undefined
): { sportSelectionRequired: boolean; selectedSportId: string | undefined } {
- const sportSelectionRequired = availableSports.length > 1
+ const sportSelectionRequired = availableSports.length > 1;
if (sportSelectionRequired && !sportId) {
throw new PublicBookingServiceError(
'Debes seleccionar un deporte para consultar disponibilidad.',
- 400,
- )
+ 400
+ );
}
if (sportId && !availableSports.some((sport) => sport.id === sportId)) {
- throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400)
+ throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400);
}
if (sportSelectionRequired) {
return {
sportSelectionRequired,
selectedSportId: sportId,
- }
+ };
}
return {
sportSelectionRequired,
selectedSportId: sportId ?? availableSports[0]?.id,
- }
+ };
}
async function getComplexWithBookingData(complexSlug: string) {
@@ -205,48 +205,48 @@ async function getComplexWithBookingData(complexSlug: string) {
orderBy: { createdAt: 'asc' },
},
},
- })
+ });
if (!complex) {
- throw new PublicBookingServiceError('Complejo no encontrado.', 404)
+ throw new PublicBookingServiceError('Complejo no encontrado.', 404);
}
if (complex.plan) {
- const rules = parsePlanRules(complex.plan.rules)
+ const rules = parsePlanRules(complex.plan.rules);
if (!rules.features.publicBookingPage) {
throw new PublicBookingServiceError(
'El complejo no tiene habilitada la reserva publica.',
- 403,
- )
+ 403
+ );
}
}
- return complex
+ return complex;
}
function mapBookingResponse(input: {
- bookingId: string
- bookingCode: string
- bookingDate: Date
- createdAt: Date
- startTime: string
- endTime: string
- customerName: string
- customerPhone: string
- status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'
+ bookingId: string;
+ bookingCode: string;
+ bookingDate: Date;
+ createdAt: Date;
+ startTime: string;
+ endTime: string;
+ customerName: string;
+ customerPhone: string;
+ status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED';
court: {
- id: string
- name: string
- complexId: string
- complexName: string
- complexSlug: string
+ id: string;
+ name: string;
+ complexId: string;
+ complexName: string;
+ complexSlug: string;
sport: {
- id: string
- name: string
- slug: string
- }
- }
+ id: string;
+ name: string;
+ slug: string;
+ };
+ };
}) {
return {
id: input.bookingId,
@@ -264,11 +264,11 @@ function mapBookingResponse(input: {
customerPhone: input.customerPhone,
status: input.status,
createdAt: input.createdAt.toISOString(),
- }
+ };
}
export async function getPublicBookingConfirmation(complexSlug: string, bookingCode: string) {
- const normalizedBookingCode = bookingCode.toUpperCase()
+ const normalizedBookingCode = bookingCode.toUpperCase();
const booking = await db.courtBooking.findFirst({
where: {
bookingCode: normalizedBookingCode,
@@ -304,10 +304,10 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
},
},
},
- })
+ });
if (!booking) {
- throw new PublicBookingServiceError('Reserva no encontrada.', 404)
+ throw new PublicBookingServiceError('Reserva no encontrada.', 404);
}
return {
@@ -325,39 +325,34 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
},
status: booking.status,
createdAt: booking.createdAt.toISOString(),
- }
+ };
}
-export async function listPublicAvailability(
- complexSlug: string,
- query: PublicAvailabilityQuery,
-) {
- const { bookingDate, dayOfWeek } = parseIsoDate(query.date)
- const complex = await getComplexWithBookingData(complexSlug)
- const sports = resolveSports(complex)
- const sportSelectionRequired = sports.length > 1
+export async function listPublicAvailability(complexSlug: string, query: PublicAvailabilityQuery) {
+ const { bookingDate, dayOfWeek } = parseIsoDate(query.date);
+ const complex = await getComplexWithBookingData(complexSlug);
+ const sports = resolveSports(complex);
+ const sportSelectionRequired = sports.length > 1;
if (query.sportId && !sports.some((sport) => sport.id === query.sportId)) {
- throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400)
+ throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400);
}
- const selectedSportId = sportSelectionRequired
- ? query.sportId
- : (query.sportId ?? sports[0]?.id)
+ const selectedSportId = sportSelectionRequired ? query.sportId : (query.sportId ?? sports[0]?.id);
const candidateCourts = complex.courts.filter((court) => {
if (!court.sport.isActive) {
- return false
+ return false;
}
if (selectedSportId) {
- return court.sportId === selectedSportId
+ return court.sportId === selectedSportId;
}
- return true
- })
+ return true;
+ });
- const courtIds = candidateCourts.map((court) => court.id)
+ const courtIds = candidateCourts.map((court) => court.id);
const bookings =
courtIds.length > 0
@@ -375,31 +370,31 @@ export async function listPublicAvailability(
endTime: true,
},
})
- : []
+ : [];
- const bookingsByCourt = new Map()
+ const bookingsByCourt = new Map();
for (const booking of bookings) {
- const current = bookingsByCourt.get(booking.courtId) ?? []
+ const current = bookingsByCourt.get(booking.courtId) ?? [];
current.push({
startTime: booking.startTime,
endTime: booking.endTime,
- })
- bookingsByCourt.set(booking.courtId, current)
+ });
+ bookingsByCourt.set(booking.courtId, current);
}
const courts = candidateCourts
.map((court) => {
const dayAvailability = court.availabilities.filter(
- (availability) => availability.dayOfWeek === dayOfWeek,
- )
+ (availability) => availability.dayOfWeek === dayOfWeek
+ );
- const allSlots = buildSlots(dayAvailability, court.slotDurationMinutes)
- const occupiedSlots = bookingsByCourt.get(court.id) ?? []
+ const allSlots = buildSlots(dayAvailability, court.slotDurationMinutes);
+ const occupiedSlots = bookingsByCourt.get(court.id) ?? [];
const availableSlots = allSlots.filter(
- (slot) => !occupiedSlots.some((occupied) => hasOverlap(slot, occupied)),
- )
+ (slot) => !occupiedSlots.some((occupied) => hasOverlap(slot, occupied))
+ );
return {
courtId: court.id,
@@ -412,9 +407,9 @@ export async function listPublicAvailability(
slotDurationMinutes: court.slotDurationMinutes,
availabilityDay: dayOfWeek,
availableSlots,
- }
+ };
})
- .filter((court) => court.availableSlots.length > 0)
+ .filter((court) => court.availableSlots.length > 0);
return {
complexId: complex.id,
@@ -424,69 +419,66 @@ export async function listPublicAvailability(
sportSelectionRequired,
sports,
courts,
- }
+ };
}
-export async function createPublicBooking(
- complexSlug: string,
- input: CreatePublicBookingInput,
-) {
- const { bookingDate, dayOfWeek } = parseIsoDate(input.date)
- const complex = await getComplexWithBookingData(complexSlug)
- const sports = resolveSports(complex)
- const { sportSelectionRequired } = validateSportSelection(sports, input.sportId)
+export async function createPublicBooking(complexSlug: string, input: CreatePublicBookingInput) {
+ const { bookingDate, dayOfWeek } = parseIsoDate(input.date);
+ const complex = await getComplexWithBookingData(complexSlug);
+ const sports = resolveSports(complex);
+ const { sportSelectionRequired } = validateSportSelection(sports, input.sportId);
const selectedCourt = complex.courts.find(
- (court) => court.id === input.courtId && court.sport.isActive,
- )
+ (court) => court.id === input.courtId && court.sport.isActive
+ );
if (!selectedCourt) {
- throw new PublicBookingServiceError('La cancha seleccionada no existe en el complejo.', 404)
+ throw new PublicBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
}
if (sportSelectionRequired && !input.sportId) {
throw new PublicBookingServiceError(
'Debes seleccionar un deporte para reservar en este complejo.',
- 400,
- )
+ 400
+ );
}
if (input.sportId && input.sportId !== selectedCourt.sportId) {
throw new PublicBookingServiceError(
'La cancha seleccionada no corresponde al deporte indicado.',
- 400,
- )
+ 400
+ );
}
const dayAvailability = selectedCourt.availabilities.filter(
- (availability) => availability.dayOfWeek === dayOfWeek,
- )
+ (availability) => availability.dayOfWeek === dayOfWeek
+ );
- const validSlots = buildSlots(dayAvailability, selectedCourt.slotDurationMinutes)
- const selectedStartMinutes = toMinutes(input.startTime)
- const selectedEndMinutes = selectedStartMinutes + selectedCourt.slotDurationMinutes
+ const validSlots = buildSlots(dayAvailability, selectedCourt.slotDurationMinutes);
+ const selectedStartMinutes = toMinutes(input.startTime);
+ const selectedEndMinutes = selectedStartMinutes + selectedCourt.slotDurationMinutes;
const selectedSlot = {
startTime: input.startTime,
endTime: minutesToTime(selectedEndMinutes),
- }
+ };
const slotExists = validSlots.some(
- (slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime,
- )
+ (slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
+ );
if (!slotExists) {
throw new PublicBookingServiceError(
'El horario seleccionado no esta disponible para esa cancha.',
- 409,
- )
+ 409
+ );
}
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const booking = await db.$transaction(async (tx) => {
if (complex.plan) {
- const rules = parsePlanRules(complex.plan.rules)
+ const rules = parsePlanRules(complex.plan.rules);
const bookingsForDate = await tx.courtBooking.count({
where: {
bookingDate,
@@ -495,19 +487,19 @@ export async function createPublicBooking(
complexId: complex.id,
},
},
- })
+ });
const violations = evaluatePlanUsage(rules, {
courtsCount: complex.courts.length,
bookingsToday: bookingsForDate,
- })
+ });
const maxBookingsViolation = violations.find(
- (violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED',
- )
+ (violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
+ );
if (maxBookingsViolation) {
- throw new PublicBookingServiceError(maxBookingsViolation.message, 409)
+ throw new PublicBookingServiceError(maxBookingsViolation.message, 409);
}
}
@@ -523,10 +515,10 @@ export async function createPublicBooking(
gt: selectedSlot.startTime,
},
},
- })
+ });
if (overlappingBooking) {
- throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409)
+ throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
return tx.courtBooking.create({
@@ -552,8 +544,8 @@ export async function createPublicBooking(
customerPhone: true,
status: true,
},
- })
- })
+ });
+ });
return mapBookingResponse({
bookingId: booking.id,
@@ -577,40 +569,40 @@ export async function createPublicBooking(
slug: selectedCourt.sport.slug,
},
},
- })
+ });
} catch (error) {
if (error instanceof PublicBookingServiceError) {
- throw error
+ throw error;
}
const prismaError = error as {
- code?: string
+ code?: string;
meta?: {
- target?: string[] | string
- }
- }
+ target?: string[] | string;
+ };
+ };
if (prismaError.code === 'P2002') {
const targets = Array.isArray(prismaError.meta?.target)
? prismaError.meta?.target
- : [prismaError.meta?.target]
+ : [prismaError.meta?.target];
const isBookingCodeCollision = targets.some((target) =>
- String(target).includes('booking_code'),
- )
+ String(target).includes('booking_code')
+ );
if (isBookingCodeCollision) {
- continue
+ continue;
}
- throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409)
+ throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
- throw error
+ throw error;
}
}
throw new PublicBookingServiceError(
'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
- 409,
- )
+ 409
+ );
}
diff --git a/apps/backend/src/modules/sport/handlers/create-sport.handler.ts b/apps/backend/src/modules/sport/handlers/create-sport.handler.ts
index 2b5ea5f..b8b1c29 100644
--- a/apps/backend/src/modules/sport/handlers/create-sport.handler.ts
+++ b/apps/backend/src/modules/sport/handlers/create-sport.handler.ts
@@ -1,18 +1,18 @@
-import type { CreateSportInput } from '@repo/api-contract'
-import { Prisma } from '@/generated/prisma/client'
-import type { AppContext } from '@/types/hono'
-import { createSport } from '@/modules/sport/services/sport.service'
+import { Prisma } from '@/generated/prisma/client';
+import { createSport } from '@/modules/sport/services/sport.service';
+import type { AppContext } from '@/types/hono';
+import type { CreateSportInput } from '@repo/api-contract';
export async function createSportHandler(c: AppContext) {
- const payload = c.req.valid('json' as never) as CreateSportInput
+ const payload = c.req.valid('json' as never) as CreateSportInput;
try {
- const sport = await createSport(payload)
- return c.json(sport, 201)
+ const sport = await createSport(payload);
+ return c.json(sport, 201);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
- return c.json({ message: 'No se pudo crear el deporte.' }, 409)
+ return c.json({ message: 'No se pudo crear el deporte.' }, 409);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/sport/handlers/list-sports.handler.ts b/apps/backend/src/modules/sport/handlers/list-sports.handler.ts
index 415b3e0..337d7ff 100644
--- a/apps/backend/src/modules/sport/handlers/list-sports.handler.ts
+++ b/apps/backend/src/modules/sport/handlers/list-sports.handler.ts
@@ -1,7 +1,7 @@
-import type { AppContext } from '@/types/hono'
-import { listSports } from '@/modules/sport/services/sport.service'
+import { listSports } from '@/modules/sport/services/sport.service';
+import type { AppContext } from '@/types/hono';
export async function listSportsHandler(c: AppContext) {
- const sports = await listSports()
- return c.json(sports)
+ const sports = await listSports();
+ return c.json(sports);
}
diff --git a/apps/backend/src/modules/sport/handlers/update-sport.handler.ts b/apps/backend/src/modules/sport/handlers/update-sport.handler.ts
index ebf3517..467a0ff 100644
--- a/apps/backend/src/modules/sport/handlers/update-sport.handler.ts
+++ b/apps/backend/src/modules/sport/handlers/update-sport.handler.ts
@@ -1,27 +1,27 @@
-import type { UpdateSportInput } from '@repo/api-contract'
-import { Prisma } from '@/generated/prisma/client'
-import type { AppContext } from '@/types/hono'
-import { getSportById, updateSport } from '@/modules/sport/services/sport.service'
+import { Prisma } from '@/generated/prisma/client';
+import { getSportById, updateSport } from '@/modules/sport/services/sport.service';
+import type { AppContext } from '@/types/hono';
+import type { UpdateSportInput } from '@repo/api-contract';
-type SportIdParams = { id: string }
+type SportIdParams = { id: string };
export async function updateSportHandler(c: AppContext) {
- const { id } = c.req.valid('param' as never) as SportIdParams
- const payload = c.req.valid('json' as never) as UpdateSportInput
+ const { id } = c.req.valid('param' as never) as SportIdParams;
+ const payload = c.req.valid('json' as never) as UpdateSportInput;
- const existing = await getSportById(id)
+ const existing = await getSportById(id);
if (!existing) {
- return c.json({ message: 'Deporte no encontrado.' }, 404)
+ return c.json({ message: 'Deporte no encontrado.' }, 404);
}
try {
- const sport = await updateSport(id, payload)
- return c.json(sport)
+ const sport = await updateSport(id, payload);
+ return c.json(sport);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
- return c.json({ message: 'No se pudo actualizar el deporte.' }, 409)
+ return c.json({ message: 'No se pudo actualizar el deporte.' }, 409);
}
- throw error
+ throw error;
}
}
diff --git a/apps/backend/src/modules/sport/services/sport.service.ts b/apps/backend/src/modules/sport/services/sport.service.ts
index ff49d26..4b4d90c 100644
--- a/apps/backend/src/modules/sport/services/sport.service.ts
+++ b/apps/backend/src/modules/sport/services/sport.service.ts
@@ -1,15 +1,15 @@
-import { v7 as uuidv7 } from 'uuid'
-import { Prisma } from '@/generated/prisma/client'
-import { db } from '@/lib/prisma'
+import { Prisma } from '@/generated/prisma/client';
+import { db } from '@/lib/prisma';
+import { v7 as uuidv7 } from 'uuid';
export type CreateSportInput = {
- name: string
-}
+ name: string;
+};
export type UpdateSportInput = {
- name?: string
- isActive?: boolean
-}
+ name?: string;
+ isActive?: boolean;
+};
function slugify(value: string): string {
return value
@@ -19,18 +19,15 @@ function slugify(value: string): string {
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
- .replace(/-+/g, '-')
+ .replace(/-+/g, '-');
}
-async function buildUniqueSlug(
- source: string,
- excludeSportId?: string,
-): Promise {
- const base = slugify(source)
- const fallback = base.length > 0 ? base : `sport-${uuidv7().slice(0, 8)}`
+async function buildUniqueSlug(source: string, excludeSportId?: string): Promise {
+ const base = slugify(source);
+ const fallback = base.length > 0 ? base : `sport-${uuidv7().slice(0, 8)}`;
- let candidate = fallback
- let index = 1
+ let candidate = fallback;
+ let index = 1;
while (true) {
const existing = await db.sport.findFirst({
@@ -45,23 +42,23 @@ async function buildUniqueSlug(
: {}),
},
select: { id: true },
- })
+ });
- if (!existing) return candidate
+ if (!existing) return candidate;
- index += 1
- candidate = `${fallback}-${index}`
+ index += 1;
+ candidate = `${fallback}-${index}`;
}
}
export async function listSports() {
return db.sport.findMany({
orderBy: { name: 'asc' },
- })
+ });
}
export async function createSport(input: CreateSportInput) {
- const slug = await buildUniqueSlug(input.name)
+ const slug = await buildUniqueSlug(input.name);
return db.sport.create({
data: {
@@ -70,27 +67,27 @@ export async function createSport(input: CreateSportInput) {
slug,
isActive: true,
},
- })
+ });
}
export async function getSportById(id: string) {
- return db.sport.findUnique({ where: { id } })
+ return db.sport.findUnique({ where: { id } });
}
export async function updateSport(id: string, input: UpdateSportInput) {
- const data: Prisma.SportUncheckedUpdateInput = {}
+ const data: Prisma.SportUncheckedUpdateInput = {};
if (input.name) {
- data.name = input.name
- data.slug = await buildUniqueSlug(input.name, id)
+ data.name = input.name;
+ data.slug = await buildUniqueSlug(input.name, id);
}
if (input.isActive !== undefined) {
- data.isActive = input.isActive
+ data.isActive = input.isActive;
}
return db.sport.update({
where: { id },
data,
- })
+ });
}
diff --git a/apps/backend/src/modules/sport/sport.routes.ts b/apps/backend/src/modules/sport/sport.routes.ts
index 05fffa5..bd23be1 100644
--- a/apps/backend/src/modules/sport/sport.routes.ts
+++ b/apps/backend/src/modules/sport/sport.routes.ts
@@ -1,30 +1,25 @@
-import { zValidator } from '@hono/zod-validator'
-import { createSportSchema, updateSportSchema } from '@repo/api-contract'
-import { Hono } from 'hono'
-import { z } from 'zod'
-import { requireAuth } from '@/middlewares/require-auth.middleware'
-import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware'
-import { createSportHandler } from '@/modules/sport/handlers/create-sport.handler'
-import { listSportsHandler } from '@/modules/sport/handlers/list-sports.handler'
-import { updateSportHandler } from '@/modules/sport/handlers/update-sport.handler'
-import type { AppEnv } from '@/types/hono'
+import { requireAuth } from '@/middlewares/require-auth.middleware';
+import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware';
+import { createSportHandler } from '@/modules/sport/handlers/create-sport.handler';
+import { listSportsHandler } from '@/modules/sport/handlers/list-sports.handler';
+import { updateSportHandler } from '@/modules/sport/handlers/update-sport.handler';
+import type { AppEnv } from '@/types/hono';
+import { zValidator } from '@hono/zod-validator';
+import { createSportSchema, updateSportSchema } from '@repo/api-contract';
+import { Hono } from 'hono';
+import { z } from 'zod';
-export const sportRoutes = new Hono()
-const sportIdParamsSchema = z.object({ id: z.uuid() })
+export const sportRoutes = new Hono();
+const sportIdParamsSchema = z.object({ id: z.uuid() });
-sportRoutes.use('*', requireAuth)
+sportRoutes.use('*', requireAuth);
-sportRoutes.get('/', listSportsHandler)
-sportRoutes.post(
- '/',
- requireSuperAdmin,
- zValidator('json', createSportSchema),
- createSportHandler,
-)
+sportRoutes.get('/', listSportsHandler);
+sportRoutes.post('/', requireSuperAdmin, zValidator('json', createSportSchema), createSportHandler);
sportRoutes.patch(
'/:id',
requireSuperAdmin,
zValidator('param', sportIdParamsSchema),
zValidator('json', updateSportSchema),
- updateSportHandler,
-)
+ updateSportHandler
+);
diff --git a/apps/backend/src/modules/user/handlers/get-user-profile.handler.ts b/apps/backend/src/modules/user/handlers/get-user-profile.handler.ts
index 42e9cb6..f409544 100644
--- a/apps/backend/src/modules/user/handlers/get-user-profile.handler.ts
+++ b/apps/backend/src/modules/user/handlers/get-user-profile.handler.ts
@@ -1,8 +1,8 @@
-import type { AppContext } from '@/types/hono'
-import { getUserProfile } from '@/modules/user/services/user.service'
+import { getUserProfile } from '@/modules/user/services/user.service';
+import type { AppContext } from '@/types/hono';
export function getUserProfileHandler(c: AppContext) {
- const authUser = c.get('authUser')
- const profile = getUserProfile(authUser)
- return c.json(profile)
+ const authUser = c.get('authUser');
+ const profile = getUserProfile(authUser);
+ return c.json(profile);
}
diff --git a/apps/backend/src/modules/user/services/user.service.ts b/apps/backend/src/modules/user/services/user.service.ts
index 97079ea..df5923d 100644
--- a/apps/backend/src/modules/user/services/user.service.ts
+++ b/apps/backend/src/modules/user/services/user.service.ts
@@ -1,21 +1,16 @@
-import type { AuthUser } from '@/types/auth-user'
-import type { UserProfile } from '@repo/api-contract'
+import type { AuthUser } from '@/types/auth-user';
+import type { UserProfile } from '@repo/api-contract';
export function getUserProfile(user: AuthUser): UserProfile {
const fullName =
- (typeof user.user_metadata?.full_name === 'string' &&
- user.user_metadata.full_name) ||
+ (typeof user.user_metadata?.full_name === 'string' && user.user_metadata.full_name) ||
(typeof user.user_metadata?.name === 'string' && user.user_metadata.name) ||
- (user.email ?? 'Usuario')
+ (user.email ?? 'Usuario');
- const role =
- (typeof user.app_metadata?.role === 'string' && user.app_metadata.role) ||
- 'member'
+ const role = (typeof user.app_metadata?.role === 'string' && user.app_metadata.role) || 'member';
const avatarUrl =
- (typeof user.user_metadata?.avatar_url === 'string' &&
- user.user_metadata.avatar_url) ||
- null
+ (typeof user.user_metadata?.avatar_url === 'string' && user.user_metadata.avatar_url) || null;
return {
id: user.id,
@@ -24,5 +19,5 @@ export function getUserProfile(user: AuthUser): UserProfile {
role,
avatarUrl,
createdAt: user.created_at,
- }
+ };
}
diff --git a/apps/backend/src/modules/user/user.routes.ts b/apps/backend/src/modules/user/user.routes.ts
index 80f7eba..a1a5af2 100644
--- a/apps/backend/src/modules/user/user.routes.ts
+++ b/apps/backend/src/modules/user/user.routes.ts
@@ -1,9 +1,9 @@
-import { Hono } from 'hono'
-import { requireAuth } from '@/middlewares/require-auth.middleware'
-import type { AppEnv } from '@/types/hono'
-import { getUserProfileHandler } from '@/modules/user/handlers/get-user-profile.handler'
+import { requireAuth } from '@/middlewares/require-auth.middleware';
+import { getUserProfileHandler } from '@/modules/user/handlers/get-user-profile.handler';
+import type { AppEnv } from '@/types/hono';
+import { Hono } from 'hono';
-export const userRoutes = new Hono()
+export const userRoutes = new Hono();
-userRoutes.use('*', requireAuth)
-userRoutes.get('/profile', getUserProfileHandler)
+userRoutes.use('*', requireAuth);
+userRoutes.get('/profile', getUserProfileHandler);
diff --git a/apps/backend/src/register-routes.ts b/apps/backend/src/register-routes.ts
index 684f284..2500289 100644
--- a/apps/backend/src/register-routes.ts
+++ b/apps/backend/src/register-routes.ts
@@ -1,16 +1,16 @@
-import type { Hono } from 'hono'
-import { serveStatic } from 'hono/bun'
-import path from 'node:path'
-import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes'
-import { complexRoutes } from '@/modules/complex/complex.routes'
-import { courtRoutes } from '@/modules/court/court.routes'
-import { onboardingRoutes } from '@/modules/onboarding/onboarding.routes'
-import { planRoutes } from '@/modules/plan/plan.routes'
-import { publicBookingRoutes } from '@/modules/public-booking/public-booking.routes'
-import { sportRoutes } from '@/modules/sport/sport.routes'
-import { userRoutes } from '@/modules/user/user.routes'
-import type { AppEnv } from '@/types/hono'
-import { healthCheckRoutes } from './modules/health-check/health-check.routes'
+import path from 'node:path';
+import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
+import { complexRoutes } from '@/modules/complex/complex.routes';
+import { courtRoutes } from '@/modules/court/court.routes';
+import { onboardingRoutes } from '@/modules/onboarding/onboarding.routes';
+import { planRoutes } from '@/modules/plan/plan.routes';
+import { publicBookingRoutes } from '@/modules/public-booking/public-booking.routes';
+import { sportRoutes } from '@/modules/sport/sport.routes';
+import { userRoutes } from '@/modules/user/user.routes';
+import type { AppEnv } from '@/types/hono';
+import type { Hono } from 'hono';
+import { serveStatic } from 'hono/bun';
+import { healthCheckRoutes } from './modules/health-check/health-check.routes';
export function registerRoutes(app: Hono) {
app
@@ -22,24 +22,24 @@ export function registerRoutes(app: Hono) {
.route('/api/courts', courtRoutes)
.route('/api/public-bookings', publicBookingRoutes)
.route('/api/admin-bookings', adminBookingRoutes)
- .route('/api/health', healthCheckRoutes)
+ .route('/api/health', healthCheckRoutes);
- app.use('*', serveStatic({ root: './public' }))
+ app.use('*', serveStatic({ root: './public' }));
app.get('*', async (c) => {
if (c.req.path === '/api' || c.req.path.startsWith('/api/')) {
- return c.notFound()
+ return c.notFound();
}
// Keep real asset URLs as 404s if the file does not exist.
if (path.extname(c.req.path)) {
- return c.notFound()
+ return c.notFound();
}
- const indexFile = Bun.file('./public/index.html')
+ const indexFile = Bun.file('./public/index.html');
if (!(await indexFile.exists())) {
- return c.notFound()
+ return c.notFound();
}
- return c.html(await indexFile.text())
- })
+ return c.html(await indexFile.text());
+ });
}
diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts
index 239560a..380766f 100644
--- a/apps/backend/src/server.ts
+++ b/apps/backend/src/server.ts
@@ -1,16 +1,13 @@
-import app from '@/index'
-import { logger } from '@/lib/logger'
+import app from '@/index';
+import { logger } from '@/lib/logger';
-const port = Number(Bun.env.PORT ?? 3000)
-const hostname = Bun.env.HOST ?? '0.0.0.0'
+const port = Number(Bun.env.PORT ?? 3000);
+const hostname = Bun.env.HOST ?? '0.0.0.0';
const server = Bun.serve({
hostname,
port,
fetch: app.fetch,
-})
+});
-logger.info(
- { url: `http://${server.hostname}:${server.port}` },
- 'backend_listening',
-)
+logger.info({ url: `http://${server.hostname}:${server.port}` }, 'backend_listening');
diff --git a/apps/backend/src/types/auth-user.ts b/apps/backend/src/types/auth-user.ts
index cf414ca..edd5e59 100644
--- a/apps/backend/src/types/auth-user.ts
+++ b/apps/backend/src/types/auth-user.ts
@@ -1,3 +1,3 @@
-import type { User } from '@supabase/supabase-js'
+import type { User } from '@supabase/supabase-js';
-export type AuthUser = User
+export type AuthUser = User;
diff --git a/apps/backend/src/types/hono.ts b/apps/backend/src/types/hono.ts
index 84c8448..3c548f3 100644
--- a/apps/backend/src/types/hono.ts
+++ b/apps/backend/src/types/hono.ts
@@ -1,13 +1,13 @@
-import type { Context } from 'hono'
-import type { AuthUser } from '@/types/auth-user'
+import type { AuthUser } from '@/types/auth-user';
+import type { Context } from 'hono';
export type AppVariables = {
- authUser: AuthUser
- appUserId: string
-}
+ authUser: AuthUser;
+ appUserId: string;
+};
export type AppEnv = {
- Variables: AppVariables
-}
+ Variables: AppVariables;
+};
-export type AppContext = Context
+export type AppContext = Context;
diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js
deleted file mode 100644
index 5e6b472..0000000
--- a/apps/frontend/eslint.config.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-import { defineConfig, globalIgnores } from 'eslint/config'
-
-export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{ts,tsx}'],
- extends: [
- js.configs.recommended,
- tseslint.configs.recommended,
- reactHooks.configs.flat.recommended,
- reactRefresh.configs.vite,
- ],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- },
- },
-])
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index dab34ee..940f2f8 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -6,7 +6,9 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
- "lint": "eslint .",
+ "lint": "biome check .",
+ "lint:fix": "biome check --write .",
+ "format": "biome format --write .",
"preview": "vite preview"
},
"dependencies": {
@@ -38,20 +40,14 @@
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
- "@eslint/js": "^9.39.4",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/router-plugin": "^1.167.12",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
- "eslint": "^9.39.4",
- "eslint-plugin-react-hooks": "^7.0.1",
- "eslint-plugin-react-refresh": "^0.5.2",
- "globals": "^17.4.0",
"tailwindcss": "^4.2.2",
"typescript": "~5.9.3",
- "typescript-eslint": "^8.57.0",
"vite": "^8.0.1"
}
}
diff --git a/apps/frontend/src/App.css b/apps/frontend/src/App.css
index f90339d..e87c16a 100644
--- a/apps/frontend/src/App.css
+++ b/apps/frontend/src/App.css
@@ -42,8 +42,7 @@
z-index: 1;
top: 34px;
height: 28px;
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
- scale(1.4);
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) scale(1.4);
}
.vite {
@@ -51,8 +50,7 @@
top: 107px;
height: 26px;
width: auto;
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
- scale(0.8);
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) scale(0.8);
}
}
@@ -167,7 +165,7 @@
&::before,
&::after {
- content: '';
+ content: "";
position: absolute;
top: -4.5px;
border: 5px solid transparent;
diff --git a/apps/frontend/src/components/ui/avatar.tsx b/apps/frontend/src/components/ui/avatar.tsx
index 99f3ed2..fcd0f70 100644
--- a/apps/frontend/src/components/ui/avatar.tsx
+++ b/apps/frontend/src/components/ui/avatar.tsx
@@ -1,42 +1,36 @@
-import * as React from "react"
-import { Avatar as AvatarPrimitive } from "radix-ui"
+import { Avatar as AvatarPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
function Avatar({
className,
- size = "default",
+ size = 'default',
...props
}: React.ComponentProps & {
- size?: "default" | "sm" | "lg"
+ size?: 'default' | 'sm' | 'lg';
}) {
return (
- )
+ );
}
-function AvatarImage({
- className,
- ...props
-}: React.ComponentProps) {
+function AvatarImage({ className, ...props }: React.ComponentProps) {
return (
- )
+ );
}
function AvatarFallback({
@@ -47,64 +41,54 @@ function AvatarFallback({
- )
+ );
}
-function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
+function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
return (
svg]:hidden",
- "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
- "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
+ 'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none',
+ 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
+ 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
+ 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className
)}
{...props}
/>
- )
+ );
}
-function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
+function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
-function AvatarGroupCount({
- className,
- ...props
-}: React.ComponentProps<"div">) {
+function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) {
return (
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
+ 'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className
)}
{...props}
/>
- )
+ );
}
-export {
- Avatar,
- AvatarImage,
- AvatarFallback,
- AvatarGroup,
- AvatarGroupCount,
- AvatarBadge,
-}
+export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge };
diff --git a/apps/frontend/src/components/ui/button.tsx b/apps/frontend/src/components/ui/button.tsx
index 6138844..008b34b 100644
--- a/apps/frontend/src/components/ui/button.tsx
+++ b/apps/frontend/src/components/ui/button.tsx
@@ -1,57 +1,57 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { Slot } from "radix-ui"
+import { type VariantProps, cva } from 'class-variance-authority';
+import { Slot } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
- default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
+ default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline:
- "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
+ 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
- "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive:
- "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
- link: "text-primary underline-offset-4 hover:underline",
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
+ link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default:
- "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
- lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- icon: "size-8",
- "icon-xs":
+ lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
+ icon: 'size-8',
+ 'icon-xs':
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
- "icon-sm":
- "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
- "icon-lg": "size-9",
+ 'icon-sm':
+ 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
+ 'icon-lg': 'size-9',
},
},
defaultVariants: {
- variant: "default",
- size: "default",
+ variant: 'default',
+ size: 'default',
},
}
-)
+);
function Button({
className,
- variant = "default",
- size = "default",
+ variant = 'default',
+ size = 'default',
asChild = false,
...props
-}: React.ComponentProps<"button"> &
+}: React.ComponentProps<'button'> &
VariantProps
& {
- asChild?: boolean
+ asChild?: boolean;
}) {
- const Comp = asChild ? Slot.Root : "button"
+ const Comp = asChild ? Slot.Root : 'button';
return (
- )
+ );
}
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/apps/frontend/src/components/ui/calendar.tsx b/apps/frontend/src/components/ui/calendar.tsx
index fc70708..5f8ad29 100644
--- a/apps/frontend/src/components/ui/calendar.tsx
+++ b/apps/frontend/src/components/ui/calendar.tsx
@@ -1,54 +1,47 @@
-import * as React from "react"
-import { DayPicker } from "react-day-picker"
-import { ChevronLeft, ChevronRight } from "lucide-react"
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+import * as React from 'react';
+import { DayPicker } from 'react-day-picker';
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
+import { buttonVariants } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
-export type CalendarProps = React.ComponentProps
+export type CalendarProps = React.ComponentProps;
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
-}: CalendarProps) {
+function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
- )
+ );
}
-Calendar.displayName = "Calendar"
+Calendar.displayName = 'Calendar';
-export { Calendar }
+export { Calendar };
diff --git a/apps/frontend/src/components/ui/date-picker.tsx b/apps/frontend/src/components/ui/date-picker.tsx
index c218e48..46289b6 100644
--- a/apps/frontend/src/components/ui/date-picker.tsx
+++ b/apps/frontend/src/components/ui/date-picker.tsx
@@ -1,26 +1,22 @@
-"use client"
+'use client';
-import * as React from "react"
-import { format } from "date-fns"
-import { es } from "date-fns/locale"
-import { Calendar as CalendarIcon } from "lucide-react"
+import { format } from 'date-fns';
+import { es } from 'date-fns/locale';
+import { Calendar as CalendarIcon } from 'lucide-react';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { Calendar } from "@/components/ui/calendar"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover"
+import { Button } from '@/components/ui/button';
+import { Calendar } from '@/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { cn } from '@/lib/utils';
interface DatePickerProps {
- value?: Date
- onChange?: (date: Date | undefined) => void
- disabled?: (date: Date) => boolean
- className?: string
- placeholder?: string
- minDate?: Date
+ value?: Date;
+ onChange?: (date: Date | undefined) => void;
+ disabled?: (date: Date) => boolean;
+ className?: string;
+ placeholder?: string;
+ minDate?: Date;
}
export function DatePicker({
@@ -28,19 +24,19 @@ export function DatePicker({
onChange,
disabled,
className,
- placeholder = "Selecciona una fecha",
+ placeholder = 'Selecciona una fecha',
minDate,
}: DatePickerProps) {
- const [open, setOpen] = React.useState(false)
+ const [open, setOpen] = React.useState(false);
const isDateDisabled = React.useCallback(
(date: Date) => {
- if (minDate && date < minDate) return true
- if (disabled) return disabled(date)
- return false
+ if (minDate && date < minDate) return true;
+ if (disabled) return disabled(date);
+ return false;
},
[minDate, disabled]
- )
+ );
return (
@@ -48,18 +44,14 @@ export function DatePicker({
- {value ? (
- format(value, "PPP", { locale: es })
- ) : (
- {placeholder}
- )}
+ {value ? format(value, 'PPP', { locale: es }) : {placeholder} }
@@ -67,13 +59,13 @@ export function DatePicker({
mode="single"
selected={value}
onSelect={(date) => {
- onChange?.(date)
- setOpen(false)
+ onChange?.(date);
+ setOpen(false);
}}
disabled={isDateDisabled}
initialFocus
/>
- )
+ );
}
diff --git a/apps/frontend/src/components/ui/dialog.tsx b/apps/frontend/src/components/ui/dialog.tsx
index 527af74..7a74ddd 100644
--- a/apps/frontend/src/components/ui/dialog.tsx
+++ b/apps/frontend/src/components/ui/dialog.tsx
@@ -1,32 +1,24 @@
-import * as React from "react"
-import { Dialog as DialogPrimitive } from "radix-ui"
+import { Dialog as DialogPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { XIcon } from "lucide-react"
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+import { XIcon } from 'lucide-react';
-function Dialog({
- ...props
-}: React.ComponentProps) {
- return
+function Dialog({ ...props }: React.ComponentProps) {
+ return ;
}
-function DialogTrigger({
- ...props
-}: React.ComponentProps) {
- return
+function DialogTrigger({ ...props }: React.ComponentProps) {
+ return ;
}
-function DialogPortal({
- ...props
-}: React.ComponentProps) {
- return
+function DialogPortal({ ...props }: React.ComponentProps) {
+ return ;
}
-function DialogClose({
- ...props
-}: React.ComponentProps) {
- return
+function DialogClose({ ...props }: React.ComponentProps) {
+ return ;
}
function DialogOverlay({
@@ -37,12 +29,12 @@ function DialogOverlay({
- )
+ );
}
function DialogContent({
@@ -51,7 +43,7 @@ function DialogContent({
showCloseButton = true,
...props
}: React.ComponentProps & {
- showCloseButton?: boolean
+ showCloseButton?: boolean;
}) {
return (
@@ -59,7 +51,7 @@ function DialogContent({
-
-
+
+
Close
)}
- )
+ );
}
-function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
-
- )
+
+ );
}
function DialogFooter({
@@ -98,14 +81,14 @@ function DialogFooter({
showCloseButton = false,
children,
...props
-}: React.ComponentProps<"div"> & {
- showCloseButton?: boolean
+}: React.ComponentProps<'div'> & {
+ showCloseButton?: boolean;
}) {
return (
)}
- )
+ );
}
-function DialogTitle({
- className,
- ...props
-}: React.ComponentProps) {
+function DialogTitle({ className, ...props }: React.ComponentProps) {
return (
- )
+ );
}
function DialogDescription({
@@ -144,12 +121,12 @@ function DialogDescription({
- )
+ );
}
export {
@@ -163,4 +140,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
-}
+};
diff --git a/apps/frontend/src/components/ui/dropdown-menu.tsx b/apps/frontend/src/components/ui/dropdown-menu.tsx
index c263ad5..7727236 100644
--- a/apps/frontend/src/components/ui/dropdown-menu.tsx
+++ b/apps/frontend/src/components/ui/dropdown-menu.tsx
@@ -1,39 +1,30 @@
-"use client"
+'use client';
-import * as React from "react"
-import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
+import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
-import { CheckIcon, ChevronRightIcon } from "lucide-react"
+import { cn } from '@/lib/utils';
+import { CheckIcon, ChevronRightIcon } from 'lucide-react';
-function DropdownMenu({
- ...props
-}: React.ComponentProps) {
- return
+function DropdownMenu({ ...props }: React.ComponentProps) {
+ return ;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps) {
- return (
-
- )
+ return ;
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps) {
- return (
-
- )
+ return ;
}
function DropdownMenuContent({
className,
- align = "start",
+ align = 'start',
sideOffset = 4,
...props
}: React.ComponentProps) {
@@ -43,29 +34,28 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
align={align}
- className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
+ className={cn(
+ 'z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
+ className
+ )}
{...props}
/>
- )
+ );
}
-function DropdownMenuGroup({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
+function DropdownMenuGroup({ ...props }: React.ComponentProps) {
+ return ;
}
function DropdownMenuItem({
className,
inset,
- variant = "default",
+ variant = 'default',
...props
}: React.ComponentProps & {
- inset?: boolean
- variant?: "default" | "destructive"
+ inset?: boolean;
+ variant?: 'default' | 'destructive';
}) {
return (
- )
+ );
}
function DropdownMenuCheckboxItem({
@@ -88,7 +78,7 @@ function DropdownMenuCheckboxItem({
inset,
...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
return (
-
+
{children}
- )
+ );
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps) {
- return (
-
- )
+ return ;
}
function DropdownMenuRadioItem({
@@ -132,7 +116,7 @@ function DropdownMenuRadioItem({
inset,
...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
return (
-
+
{children}
- )
+ );
}
function DropdownMenuLabel({
@@ -163,19 +146,19 @@ function DropdownMenuLabel({
inset,
...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
return (
- )
+ );
}
function DropdownMenuSeparator({
@@ -185,32 +168,27 @@ function DropdownMenuSeparator({
return (
- )
+ );
}
-function DropdownMenuShortcut({
- className,
- ...props
-}: React.ComponentProps<"span">) {
+function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
return (
- )
+ );
}
-function DropdownMenuSub({
- ...props
-}: React.ComponentProps) {
- return
+function DropdownMenuSub({ ...props }: React.ComponentProps) {
+ return ;
}
function DropdownMenuSubTrigger({
@@ -219,7 +197,7 @@ function DropdownMenuSubTrigger({
children,
...props
}: React.ComponentProps & {
- inset?: boolean
+ inset?: boolean;
}) {
return (
- )
+ );
}
function DropdownMenuSubContent({
@@ -244,10 +222,13 @@ function DropdownMenuSubContent({
return (
- )
+ );
}
export {
@@ -266,4 +247,4 @@ export {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
-}
+};
diff --git a/apps/frontend/src/components/ui/field.tsx b/apps/frontend/src/components/ui/field.tsx
index 4f1426f..d61a213 100644
--- a/apps/frontend/src/components/ui/field.tsx
+++ b/apps/frontend/src/components/ui/field.tsx
@@ -1,77 +1,74 @@
-import { useMemo } from "react"
-import { cva, type VariantProps } from "class-variance-authority"
+import { type VariantProps, cva } from 'class-variance-authority';
+import { useMemo } from 'react';
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
-import { Separator } from "@/components/ui/separator"
+import { Label } from '@/components/ui/label';
+import { Separator } from '@/components/ui/separator';
+import { cn } from '@/lib/utils';
-function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
+function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
return (
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
+ 'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className
)}
{...props}
/>
- )
+ );
}
function FieldLegend({
className,
- variant = "legend",
+ variant = 'legend',
...props
-}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
+}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
return (
- )
+ );
}
-function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
+function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
-const fieldVariants = cva(
- "group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
- {
- variants: {
- orientation: {
- vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
- horizontal:
- "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
- responsive:
- "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
- },
+const fieldVariants = cva('group/field flex w-full gap-2 data-[invalid=true]:text-destructive', {
+ variants: {
+ orientation: {
+ vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
+ horizontal:
+ 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
+ responsive:
+ 'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
},
- defaultVariants: {
- orientation: "vertical",
- },
- }
-)
+ },
+ defaultVariants: {
+ orientation: 'vertical',
+ },
+});
function Field({
className,
- orientation = "vertical",
+ orientation = 'vertical',
...props
-}: React.ComponentProps<"div"> & VariantProps) {
+}: React.ComponentProps<'div'> & VariantProps) {
return (
- )
+ );
}
-function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
+function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
-function FieldLabel({
- className,
- ...props
-}: React.ComponentProps) {
+function FieldLabel({ className, ...props }: React.ComponentProps) {
return (
[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
- "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
+ 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10',
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
className
)}
{...props}
/>
- )
+ );
}
-function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
+function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
-function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
+function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
+ 'text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
+ 'last:mt-0 nth-last-2:-mt-1',
+ '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
className
)}
{...props}
/>
- )
+ );
}
function FieldSeparator({
children,
className,
...props
-}: React.ComponentProps<"div"> & {
- children?: React.ReactNode
+}: React.ComponentProps<'div'> & {
+ children?: React.ReactNode;
}) {
return (
)}
- )
+ );
}
function FieldError({
@@ -176,50 +167,45 @@ function FieldError({
children,
errors,
...props
-}: React.ComponentProps<"div"> & {
- errors?: Array<{ message?: string } | undefined>
+}: React.ComponentProps<'div'> & {
+ errors?: Array<{ message?: string } | undefined>;
}) {
const content = useMemo(() => {
if (children) {
- return children
+ return children;
}
if (!errors?.length) {
- return null
+ return null;
}
- const uniqueErrors = [
- ...new Map(errors.map((error) => [error?.message, error])).values(),
- ]
+ const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
- if (uniqueErrors?.length == 1) {
- return uniqueErrors[0]?.message
+ if (uniqueErrors?.length === 1) {
+ return uniqueErrors[0]?.message;
}
return (
- {uniqueErrors.map(
- (error, index) =>
- error?.message && {error.message}
- )}
+ {uniqueErrors.map((error, index) => error?.message && {error.message} )}
- )
- }, [children, errors])
+ );
+ }, [children, errors]);
if (!content) {
- return null
+ return null;
}
return (
{content}
- )
+ );
}
export {
@@ -233,4 +219,4 @@ export {
FieldSet,
FieldContent,
FieldTitle,
-}
+};
diff --git a/apps/frontend/src/components/ui/form.tsx b/apps/frontend/src/components/ui/form.tsx
index e984e3f..9d46740 100644
--- a/apps/frontend/src/components/ui/form.tsx
+++ b/apps/frontend/src/components/ui/form.tsx
@@ -1,32 +1,26 @@
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { Slot } from "@radix-ui/react-slot"
-import type {
- ControllerProps,
- FieldPath,
- FieldValues,
-} from "react-hook-form"
-import { Controller, FormProvider, useFormContext } from "react-hook-form"
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { Slot } from '@radix-ui/react-slot';
+import * as React from 'react';
+import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
+import { Controller, FormProvider, useFormContext } from 'react-hook-form';
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
+import { Label } from '@/components/ui/label';
+import { cn } from '@/lib/utils';
-const Form = FormProvider
+const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath
+ TName extends FieldPath = FieldPath,
> = {
- name: TName
-}
+ name: TName;
+};
-const FormFieldContext = React.createContext(
- {} as FormFieldContextValue
-)
+const FormFieldContext = React.createContext({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath
+ TName extends FieldPath = FieldPath,
>({
...props
}: ControllerProps) => {
@@ -34,21 +28,21 @@ const FormField = <
- )
-}
+ );
+};
const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext)
- const itemContext = React.useContext(FormItemContext)
- const { getFieldState, formState } = useFormContext()
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
- const fieldState = getFieldState(fieldContext.name, formState)
+ const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
- throw new Error("useFormField should be used within ")
+ throw new Error('useFormField should be used within ');
}
- const { id } = itemContext
+ const { id } = itemContext;
return {
id,
@@ -57,110 +51,103 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
- }
-}
+ };
+};
type FormItemContextValue = {
- id: string
-}
+ id: string;
+};
-const FormItemContext = React.createContext(
- {} as FormItemContextValue
-)
+const FormItemContext = React.createContext({} as FormItemContextValue);
-const FormItem = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => {
- const id = React.useId()
+const FormItem = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const id = React.useId();
- return (
-
-
-
- )
-})
-FormItem.displayName = "FormItem"
+ return (
+
+
+
+ );
+ }
+);
+FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
- const { error, formItemId } = useFormField()
+ const { error, formItemId } = useFormField();
return (
- )
-})
-FormLabel.displayName = "FormLabel"
+ );
+});
+FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
- )
-})
-FormControl.displayName = "FormControl"
+ );
+});
+FormControl.displayName = 'FormControl';
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const { formDescriptionId } = useFormField()
+ const { formDescriptionId } = useFormField();
return (
- )
-})
-FormDescription.displayName = "FormDescription"
+ );
+});
+FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, children, ...props }, ref) => {
- const { error, formMessageId } = useFormField()
- const body = error ? String(error?.message) : children
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
if (!body) {
- return null
+ return null;
}
return (
{body}
- )
-})
-FormMessage.displayName = "FormMessage"
+ );
+});
+FormMessage.displayName = 'FormMessage';
export {
useFormField,
@@ -171,4 +158,4 @@ export {
FormDescription,
FormField,
FormMessage,
-}
\ No newline at end of file
+};
diff --git a/apps/frontend/src/components/ui/input-otp.tsx b/apps/frontend/src/components/ui/input-otp.tsx
index 27c9ed6..150438e 100644
--- a/apps/frontend/src/components/ui/input-otp.tsx
+++ b/apps/frontend/src/components/ui/input-otp.tsx
@@ -1,59 +1,59 @@
-import * as React from "react"
-import { OTPInput, OTPInputContext } from "input-otp"
+import { OTPInput, OTPInputContext } from 'input-otp';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
-import { MinusIcon } from "lucide-react"
+import { cn } from '@/lib/utils';
+import { MinusIcon } from 'lucide-react';
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps & {
- containerClassName?: string
+ containerClassName?: string;
}) {
return (
- )
+ );
}
-function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
+function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
- )
+ );
}
function InputOTPSlot({
index,
className,
...props
-}: React.ComponentProps<"div"> & {
- index: number
+}: React.ComponentProps<'div'> & {
+ index: number;
}) {
- const inputOTPContext = React.useContext(OTPInputContext)
- const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
+ const inputOTPContext = React.useContext(OTPInputContext);
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
)}
- )
+ );
}
-function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
+function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
return (
) {
role="separator"
{...props}
>
-
+
- )
+ );
}
-export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
diff --git a/apps/frontend/src/components/ui/input.tsx b/apps/frontend/src/components/ui/input.tsx
index d763cd9..c1f9dcb 100644
--- a/apps/frontend/src/components/ui/input.tsx
+++ b/apps/frontend/src/components/ui/input.tsx
@@ -1,19 +1,19 @@
-import * as React from "react"
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
- )
+ );
}
-export { Input }
+export { Input };
diff --git a/apps/frontend/src/components/ui/label.tsx b/apps/frontend/src/components/ui/label.tsx
index f752f82..7e80928 100644
--- a/apps/frontend/src/components/ui/label.tsx
+++ b/apps/frontend/src/components/ui/label.tsx
@@ -1,22 +1,19 @@
-import * as React from "react"
-import { Label as LabelPrimitive } from "radix-ui"
+import { Label as LabelPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
-function Label({
- className,
- ...props
-}: React.ComponentProps) {
+function Label({ className, ...props }: React.ComponentProps) {
return (
- )
+ );
}
-export { Label }
+export { Label };
diff --git a/apps/frontend/src/components/ui/popover.tsx b/apps/frontend/src/components/ui/popover.tsx
index bbba7e0..8551f97 100644
--- a/apps/frontend/src/components/ui/popover.tsx
+++ b/apps/frontend/src/components/ui/popover.tsx
@@ -1,29 +1,29 @@
-import * as React from "react"
-import * as PopoverPrimitive from "@radix-ui/react-popover"
+import * as PopoverPrimitive from '@radix-ui/react-popover';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
-const Popover = PopoverPrimitive.Root
+const Popover = PopoverPrimitive.Root;
-const PopoverTrigger = PopoverPrimitive.Trigger
+const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
-))
-PopoverContent.displayName = PopoverPrimitive.Content.displayName
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
-export { Popover, PopoverTrigger, PopoverContent }
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/apps/frontend/src/components/ui/select.tsx b/apps/frontend/src/components/ui/select.tsx
index 8333850..41f8c37 100644
--- a/apps/frontend/src/components/ui/select.tsx
+++ b/apps/frontend/src/components/ui/select.tsx
@@ -1,41 +1,34 @@
-import * as React from "react"
-import { Select as SelectPrimitive } from "radix-ui"
+import { Select as SelectPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
-import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
+import { cn } from '@/lib/utils';
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
-function Select({
- ...props
-}: React.ComponentProps) {
- return
+function Select({ ...props }: React.ComponentProps) {
+ return ;
}
-function SelectGroup({
- className,
- ...props
-}: React.ComponentProps) {
+function SelectGroup({ className, ...props }: React.ComponentProps) {
return (
- )
+ );
}
-function SelectValue({
- ...props
-}: React.ComponentProps) {
- return
+function SelectValue({ ...props }: React.ComponentProps) {
+ return ;
}
function SelectTrigger({
className,
- size = "default",
+ size = 'default',
children,
...props
}: React.ComponentProps & {
- size?: "sm" | "default"
+ size?: 'sm' | 'default';
}) {
return (
- )
+ );
}
function SelectContent({
className,
children,
- position = "item-aligned",
- align = "center",
+ position = 'item-aligned',
+ align = 'center',
...props
}: React.ComponentProps) {
return (
{children}
@@ -85,20 +83,17 @@ function SelectContent({
- )
+ );
}
-function SelectLabel({
- className,
- ...props
-}: React.ComponentProps) {
+function SelectLabel({ className, ...props }: React.ComponentProps) {
return (
- )
+ );
}
function SelectItem({
@@ -122,7 +117,7 @@ function SelectItem({
{children}
- )
+ );
}
function SelectSeparator({
@@ -132,10 +127,10 @@ function SelectSeparator({
return (
- )
+ );
}
function SelectScrollUpButton({
@@ -151,10 +146,9 @@ function SelectScrollUpButton({
)}
{...props}
>
-
+
- )
+ );
}
function SelectScrollDownButton({
@@ -170,10 +164,9 @@ function SelectScrollDownButton({
)}
{...props}
>
-
+
- )
+ );
}
export {
@@ -187,4 +180,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
-}
+};
diff --git a/apps/frontend/src/components/ui/separator.tsx b/apps/frontend/src/components/ui/separator.tsx
index d457090..a915215 100644
--- a/apps/frontend/src/components/ui/separator.tsx
+++ b/apps/frontend/src/components/ui/separator.tsx
@@ -1,13 +1,13 @@
-"use client"
+'use client';
-import * as React from "react"
-import { Separator as SeparatorPrimitive } from "radix-ui"
+import { Separator as SeparatorPrimitive } from 'radix-ui';
+import * as React from 'react';
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils';
function Separator({
className,
- orientation = "horizontal",
+ orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps) {
@@ -17,12 +17,12 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
- "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
+ 'shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
className
)}
{...props}
/>
- )
+ );
}
-export { Separator }
+export { Separator };
diff --git a/apps/frontend/src/features/about/about-page.tsx b/apps/frontend/src/features/about/about-page.tsx
index 7ae9a51..71f61de 100644
--- a/apps/frontend/src/features/about/about-page.tsx
+++ b/apps/frontend/src/features/about/about-page.tsx
@@ -8,5 +8,5 @@ export function AboutPage() {
- )
+ );
}
diff --git a/apps/frontend/src/features/complex/complex-courts-page.tsx b/apps/frontend/src/features/complex/complex-courts-page.tsx
index 88e61ec..a74cd4d 100644
--- a/apps/frontend/src/features/complex/complex-courts-page.tsx
+++ b/apps/frontend/src/features/complex/complex-courts-page.tsx
@@ -1,35 +1,35 @@
-import { useEffect, useState } from 'react'
-import { useQuery } from '@tanstack/react-query'
-import { type Court, type CreateCourtInput } from '@repo/api-contract'
-import { apiClient } from '@/lib/api-client'
-import { setCurrentComplexSlug } from '@/lib/current-complex'
-import { CourtFormSection } from './components/court-form-section'
-import { CourtListSection } from './components/court-list-section'
+import { apiClient } from '@/lib/api-client';
+import { setCurrentComplexSlug } from '@/lib/current-complex';
+import { type Court, type CreateCourtInput } from '@repo/api-contract';
+import { useQuery } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
+import { CourtFormSection } from './components/court-form-section';
+import { CourtListSection } from './components/court-list-section';
type ComplexCourtsPageProps = {
- complexSlug: string
-}
+ complexSlug: string;
+};
export function ComplexCourtsPage({ complexSlug }: ComplexCourtsPageProps) {
- const [editingCourt, setEditingCourt] = useState(null)
- const [initialDraft, setInitialDraft] = useState(null)
+ const [editingCourt, setEditingCourt] = useState(null);
+ const [initialDraft, setInitialDraft] = useState(null);
useEffect(() => {
- setCurrentComplexSlug(complexSlug)
- }, [complexSlug])
+ setCurrentComplexSlug(complexSlug);
+ }, [complexSlug]);
const complexQuery = useQuery({
queryKey: ['complex-by-slug', complexSlug],
queryFn: () => apiClient.complexes.getBySlug(complexSlug),
- })
+ });
- const complexId = complexQuery.data?.id ?? null
+ const complexId = complexQuery.data?.id ?? null;
const courtsQuery = useQuery({
queryKey: ['courts', complexId],
enabled: Boolean(complexId),
queryFn: () => apiClient.courts.listByComplex(complexId as string),
- })
+ });
return (
@@ -47,7 +47,7 @@ export function ComplexCourtsPage({ complexSlug }: ComplexCourtsPageProps) {
editingCourt={editingCourt}
initialDraft={initialDraft}
onCancelEdit={() => {
- setEditingCourt(null)
+ setEditingCourt(null);
}}
/>
@@ -56,7 +56,7 @@ export function ComplexCourtsPage({ complexSlug }: ComplexCourtsPageProps) {
isLoading={courtsQuery.isLoading}
isError={courtsQuery.isError}
onDuplicateCourt={(court) => {
- setEditingCourt(null)
+ setEditingCourt(null);
setInitialDraft({
name: `${court.name} - Copy`,
sportId: court.sportId,
@@ -67,15 +67,15 @@ export function ComplexCourtsPage({ complexSlug }: ComplexCourtsPageProps) {
startTime: slot.startTime,
endTime: slot.endTime,
})),
- })
- window.scrollTo({ top: 0, behavior: 'smooth' })
+ });
+ window.scrollTo({ top: 0, behavior: 'smooth' });
}}
onEditCourt={(court) => {
- setInitialDraft(null)
- setEditingCourt(court)
- window.scrollTo({ top: 0, behavior: 'smooth' })
+ setInitialDraft(null);
+ setEditingCourt(court);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
}}
/>
- )
+ );
}
diff --git a/apps/frontend/src/features/complex/complex-settings-page.tsx b/apps/frontend/src/features/complex/complex-settings-page.tsx
index d69febd..9640828 100644
--- a/apps/frontend/src/features/complex/complex-settings-page.tsx
+++ b/apps/frontend/src/features/complex/complex-settings-page.tsx
@@ -1,35 +1,35 @@
-import { useEffect, useState } from 'react'
-import { useQuery } from '@tanstack/react-query'
-import { Building2, MapPin, Settings } from 'lucide-react'
-import { apiClient } from '@/lib/api-client'
-import { setCurrentComplexSlug } from '@/lib/current-complex'
-import { ComplexDetailsSection } from './components/complex-details-section'
-import { ComplexCourtsSection } from './components/complex-courts-section'
+import { apiClient } from '@/lib/api-client';
+import { setCurrentComplexSlug } from '@/lib/current-complex';
+import { useQuery } from '@tanstack/react-query';
+import { Building2, MapPin, Settings } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import { ComplexCourtsSection } from './components/complex-courts-section';
+import { ComplexDetailsSection } from './components/complex-details-section';
type ComplexSettingsPageProps = {
- complexSlug: string
-}
+ complexSlug: string;
+};
-type TabOption = 'details' | 'courts'
+type TabOption = 'details' | 'courts';
export function ComplexSettingsPage({ complexSlug }: ComplexSettingsPageProps) {
- const [activeTab, setActiveTab] = useState('details')
+ const [activeTab, setActiveTab] = useState('details');
useEffect(() => {
- setCurrentComplexSlug(complexSlug)
- }, [complexSlug])
+ setCurrentComplexSlug(complexSlug);
+ }, [complexSlug]);
const complexQuery = useQuery({
queryKey: ['complex-by-slug', complexSlug],
queryFn: () => apiClient.complexes.getBySlug(complexSlug),
- })
+ });
- const complexId = complexQuery.data?.id ?? null
+ const complexId = complexQuery.data?.id ?? null;
const tabs = [
{ id: 'details' as const, label: 'Datos del complejo', icon: Building2 },
{ id: 'courts' as const, label: 'Canchas', icon: MapPin },
- ]
+ ];
return (
@@ -46,8 +46,8 @@ export function ComplexSettingsPage({ complexSlug }: ComplexSettingsPageProps) {
{tabs.map((tab) => {
- const Icon = tab.icon
- const isActive = activeTab === tab.id
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
return (
{tab.label}
- )
+ );
})}
@@ -75,13 +75,9 @@ export function ComplexSettingsPage({ complexSlug }: ComplexSettingsPageProps) {
/>
)}
- {activeTab === 'courts' && (
-
- )}
+ {activeTab === 'courts' && }
- )
+ );
}
diff --git a/apps/frontend/src/features/complex/components/complex-courts-section.tsx b/apps/frontend/src/features/complex/components/complex-courts-section.tsx
index 37a4349..d22c26a 100644
--- a/apps/frontend/src/features/complex/components/complex-courts-section.tsx
+++ b/apps/frontend/src/features/complex/components/complex-courts-section.tsx
@@ -1,27 +1,25 @@
-import { useState } from 'react'
-import { useQuery } from '@tanstack/react-query'
-import { type Court, type CreateCourtInput } from '@repo/api-contract'
-import { MapPin } from 'lucide-react'
-import { apiClient } from '@/lib/api-client'
-import { CourtFormSection } from './court-form-section'
-import { CourtListSection } from './court-list-section'
-import { Separator } from '@/components/ui/separator'
+import { Separator } from '@/components/ui/separator';
+import { apiClient } from '@/lib/api-client';
+import { type Court, type CreateCourtInput } from '@repo/api-contract';
+import { useQuery } from '@tanstack/react-query';
+import { MapPin } from 'lucide-react';
+import { useState } from 'react';
+import { CourtFormSection } from './court-form-section';
+import { CourtListSection } from './court-list-section';
type ComplexCourtsSectionProps = {
- complexId: string | null
-}
+ complexId: string | null;
+};
-export function ComplexCourtsSection({
- complexId,
-}: ComplexCourtsSectionProps) {
- const [editingCourt, setEditingCourt] = useState(null)
- const [initialDraft, setInitialDraft] = useState(null)
+export function ComplexCourtsSection({ complexId }: ComplexCourtsSectionProps) {
+ const [editingCourt, setEditingCourt] = useState(null);
+ const [initialDraft, setInitialDraft] = useState(null);
const courtsQuery = useQuery({
queryKey: ['courts', complexId],
enabled: Boolean(complexId),
queryFn: () => apiClient.courts.listByComplex(complexId as string),
- })
+ });
return (
@@ -30,16 +28,14 @@ export function ComplexCourtsSection({
Canchas
-
- Alta y edición de canchas del complejo.
-
+ Alta y edición de canchas del complejo.
{
- setEditingCourt(null)
+ setEditingCourt(null);
}}
/>
@@ -48,7 +44,7 @@ export function ComplexCourtsSection({
isLoading={courtsQuery.isLoading}
isError={courtsQuery.isError}
onDuplicateCourt={(court) => {
- setEditingCourt(null)
+ setEditingCourt(null);
setInitialDraft({
name: `${court.name} - Copy`,
sportId: court.sportId,
@@ -59,15 +55,15 @@ export function ComplexCourtsSection({
startTime: slot.startTime,
endTime: slot.endTime,
})),
- })
- window.scrollTo({ top: 0, behavior: 'smooth' })
+ });
+ window.scrollTo({ top: 0, behavior: 'smooth' });
}}
onEditCourt={(court) => {
- setInitialDraft(null)
- setEditingCourt(court)
- window.scrollTo({ top: 0, behavior: 'smooth' })
+ setInitialDraft(null);
+ setEditingCourt(court);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
}}
/>
- )
+ );
}
diff --git a/apps/frontend/src/features/complex/components/complex-details-section.tsx b/apps/frontend/src/features/complex/components/complex-details-section.tsx
index 91d8068..a7725c8 100644
--- a/apps/frontend/src/features/complex/components/complex-details-section.tsx
+++ b/apps/frontend/src/features/complex/components/complex-details-section.tsx
@@ -1,43 +1,39 @@
-import { useEffect, useState } from 'react'
-import { useMutation, useQueryClient } from '@tanstack/react-query'
-import type { Complex, UpdateComplexInput } from '@repo/api-contract'
-import { updateComplexSchema } from '@repo/api-contract'
-import { Building2, Loader2 } from 'lucide-react'
-import { apiClient } from '@/lib/api-client'
-import { Button } from '@/components/ui/button'
-import { Field, FieldLabel } from '@/components/ui/field'
-import { Input } from '@/components/ui/input'
-import { Separator } from '@/components/ui/separator'
+import { Button } from '@/components/ui/button';
+import { Field, FieldLabel } from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
+import { Separator } from '@/components/ui/separator';
+import { apiClient } from '@/lib/api-client';
+import type { Complex, UpdateComplexInput } from '@repo/api-contract';
+import { updateComplexSchema } from '@repo/api-contract';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Building2, Loader2 } from 'lucide-react';
+import { useEffect, useState } from 'react';
type ComplexDetailsSectionProps = {
- complex: Complex | null
- isLoading: boolean
- isError: boolean
-}
+ complex: Complex | null;
+ isLoading: boolean;
+ isError: boolean;
+};
type FormValues = {
- complexName: string
- physicalAddress: string
- city: string
- state: string
- country: string
-}
+ complexName: string;
+ physicalAddress: string;
+ city: string;
+ state: string;
+ country: string;
+};
-export function ComplexDetailsSection({
- complex,
- isLoading,
- isError,
-}: ComplexDetailsSectionProps) {
- const queryClient = useQueryClient()
+export function ComplexDetailsSection({ complex, isLoading, isError }: ComplexDetailsSectionProps) {
+ const queryClient = useQueryClient();
const [formValues, setFormValues] = useState({
complexName: '',
physicalAddress: '',
city: '',
state: '',
country: '',
- })
- const [errorMessage, setErrorMessage] = useState(null)
- const [successMessage, setSuccessMessage] = useState(null)
+ });
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [successMessage, setSuccessMessage] = useState(null);
useEffect(() => {
if (complex) {
@@ -47,28 +43,27 @@ export function ComplexDetailsSection({
city: complex.city ?? '',
state: complex.state ?? '',
country: complex.country ?? '',
- })
+ });
}
- }, [complex])
+ }, [complex]);
const updateMutation = useMutation({
- mutationFn: (data: UpdateComplexInput) =>
- apiClient.complexes.update(complex!.id, data),
+ mutationFn: (data: UpdateComplexInput) => apiClient.complexes.update(complex!.id, data),
onSuccess: (updatedComplex) => {
- setSuccessMessage('Datos actualizados correctamente.')
- setErrorMessage(null)
- queryClient.setQueryData(['complex-by-slug', complex?.complexSlug], updatedComplex)
+ setSuccessMessage('Datos actualizados correctamente.');
+ setErrorMessage(null);
+ queryClient.setQueryData(['complex-by-slug', complex?.complexSlug], updatedComplex);
},
onError: (error: Error) => {
- setErrorMessage(error.message || 'Error al actualizar los datos.')
- setSuccessMessage(null)
+ setErrorMessage(error.message || 'Error al actualizar los datos.');
+ setSuccessMessage(null);
},
- })
+ });
const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- setErrorMessage(null)
- setSuccessMessage(null)
+ e.preventDefault();
+ setErrorMessage(null);
+ setSuccessMessage(null);
const result = updateComplexSchema.safeParse({
complexName: formValues.complexName,
@@ -76,15 +71,15 @@ export function ComplexDetailsSection({
city: formValues.city || null,
state: formValues.state || null,
country: formValues.country || null,
- })
+ });
if (!result.success) {
- setErrorMessage(result.error.issues[0]?.message || 'Datos invƔlidos.')
- return
+ setErrorMessage(result.error.issues[0]?.message || 'Datos invƔlidos.');
+ return;
}
- updateMutation.mutate(result.data)
- }
+ updateMutation.mutate(result.data);
+ };
if (isLoading) {
return (
@@ -96,7 +91,7 @@ export function ComplexDetailsSection({
Cargando...
- )
+ );
}
if (isError || !complex) {
@@ -107,11 +102,9 @@ export function ComplexDetailsSection({
Datos del complejo
-
- No se pudieron cargar los datos del complejo.
-
+ No se pudieron cargar los datos del complejo.
- )
+ );
}
return (
@@ -129,9 +122,7 @@ export function ComplexDetailsSection({
id="complex-name"
type="text"
value={formValues.complexName}
- onChange={(e) =>
- setFormValues((prev) => ({ ...prev, complexName: e.target.value }))
- }
+ onChange={(e) => setFormValues((prev) => ({ ...prev, complexName: e.target.value }))}
/>
@@ -156,9 +147,7 @@ export function ComplexDetailsSection({
type="text"
placeholder="Ej: Salta"
value={formValues.city}
- onChange={(e) =>
- setFormValues((prev) => ({ ...prev, city: e.target.value }))
- }
+ onChange={(e) => setFormValues((prev) => ({ ...prev, city: e.target.value }))}
/>
@@ -169,9 +158,7 @@ export function ComplexDetailsSection({
type="text"
placeholder="Ej: Salta"
value={formValues.state}
- onChange={(e) =>
- setFormValues((prev) => ({ ...prev, state: e.target.value }))
- }
+ onChange={(e) => setFormValues((prev) => ({ ...prev, state: e.target.value }))}
/>
@@ -182,32 +169,23 @@ export function ComplexDetailsSection({
type="text"
placeholder="Ej: Argentina"
value={formValues.country}
- onChange={(e) =>
- setFormValues((prev) => ({ ...prev, country: e.target.value }))
- }
+ onChange={(e) => setFormValues((prev) => ({ ...prev, country: e.target.value }))}
/>
{errorMessage && {errorMessage}
}
{successMessage && (
-
- {successMessage}
-
+ {successMessage}
)}
-
- {updateMutation.isPending && (
-
- )}
+
+ {updateMutation.isPending && }
Guardar cambios
- )
+ );
}
diff --git a/apps/frontend/src/features/complex/components/court-form-section.tsx b/apps/frontend/src/features/complex/components/court-form-section.tsx
index 2640be0..2a11981 100644
--- a/apps/frontend/src/features/complex/components/court-form-section.tsx
+++ b/apps/frontend/src/features/complex/components/court-form-section.tsx
@@ -1,9 +1,4 @@
-import { useEffect, useMemo, useState } from 'react'
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { createCourtSchema, type Court, type CreateCourtInput } from '@repo/api-contract'
-import { Controller, useFieldArray, useForm } from 'react-hook-form'
-import { Button } from '@/components/ui/button'
+import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
@@ -11,26 +6,31 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from '@/components/ui/dialog'
-import { Field, FieldError, FieldLabel } from '@/components/ui/field'
-import { Input } from '@/components/ui/input'
+} from '@/components/ui/dialog';
+import { Field, FieldError, FieldLabel } from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from '@/components/ui/select'
-import { ApiClientError, apiClient } from '@/lib/api-client'
-import { DAY_OPTIONS } from '../lib/court.constants'
-import { createDefaultCourtValues } from '../lib/court.utils'
+} from '@/components/ui/select';
+import { ApiClientError, apiClient } from '@/lib/api-client';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { type Court, type CreateCourtInput, createCourtSchema } from '@repo/api-contract';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useEffect, useMemo, useState } from 'react';
+import { Controller, useFieldArray, useForm } from 'react-hook-form';
+import { DAY_OPTIONS } from '../lib/court.constants';
+import { createDefaultCourtValues } from '../lib/court.utils';
type CourtFormSectionProps = {
- complexId: string | null
- editingCourt: Court | null
- initialDraft: CreateCourtInput | null
- onCancelEdit: () => void
-}
+ complexId: string | null;
+ editingCourt: Court | null;
+ initialDraft: CreateCourtInput | null;
+ onCancelEdit: () => void;
+};
export function CourtFormSection({
complexId,
@@ -38,67 +38,67 @@ export function CourtFormSection({
initialDraft,
onCancelEdit,
}: CourtFormSectionProps) {
- const queryClient = useQueryClient()
- const [formError, setFormError] = useState(null)
- const [isWeekModalOpen, setIsWeekModalOpen] = useState(false)
- const [weekDefaultStartTime, setWeekDefaultStartTime] = useState('08:00')
- const [weekDefaultEndTime, setWeekDefaultEndTime] = useState('22:00')
- const [weekModalError, setWeekModalError] = useState(null)
+ const queryClient = useQueryClient();
+ const [formError, setFormError] = useState(null);
+ const [isWeekModalOpen, setIsWeekModalOpen] = useState(false);
+ const [weekDefaultStartTime, setWeekDefaultStartTime] = useState('08:00');
+ const [weekDefaultEndTime, setWeekDefaultEndTime] = useState('22:00');
+ const [weekModalError, setWeekModalError] = useState(null);
const sportsQuery = useQuery({
queryKey: ['sports'],
queryFn: () => apiClient.sports.list(),
- })
+ });
const form = useForm({
resolver: zodResolver(createCourtSchema),
mode: 'onBlur',
defaultValues: createDefaultCourtValues(),
- })
+ });
const availabilityFieldArray = useFieldArray({
control: form.control,
name: 'availability',
- })
+ });
const saveMutation = useMutation({
mutationFn: async (payload: CreateCourtInput) => {
if (editingCourt) {
- return apiClient.courts.update(editingCourt.id, payload)
+ return apiClient.courts.update(editingCourt.id, payload);
}
if (!complexId) {
- throw new ApiClientError('No se pudo resolver el complejo para guardar la cancha.')
+ throw new ApiClientError('No se pudo resolver el complejo para guardar la cancha.');
}
- return apiClient.courts.create(complexId, payload)
+ return apiClient.courts.create(complexId, payload);
},
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ['courts', complexId],
- })
- setFormError(null)
+ });
+ setFormError(null);
if (editingCourt) {
- onCancelEdit()
+ onCancelEdit();
}
- form.reset(createDefaultCourtValues())
+ form.reset(createDefaultCourtValues());
},
onError: (error) => {
const message =
error instanceof ApiClientError
? error.message
- : 'No se pudo guardar la cancha. Intenta nuevamente.'
- setFormError(message)
+ : 'No se pudo guardar la cancha. Intenta nuevamente.';
+ setFormError(message);
},
- })
+ });
const activeSports = useMemo(
() => (sportsQuery.data ?? []).filter((sport) => sport.isActive),
- [sportsQuery.data],
- )
+ [sportsQuery.data]
+ );
const onSubmit = async (values: CreateCourtInput) => {
- setFormError(null)
- await saveMutation.mutateAsync(values)
- }
+ setFormError(null);
+ await saveMutation.mutateAsync(values);
+ };
useEffect(() => {
if (editingCourt) {
@@ -112,33 +112,33 @@ export function CourtFormSection({
startTime: slot.startTime,
endTime: slot.endTime,
})),
- })
- return
+ });
+ return;
}
if (initialDraft) {
- form.reset(initialDraft)
- return
+ form.reset(initialDraft);
+ return;
}
- form.reset(createDefaultCourtValues())
- }, [editingCourt, form, initialDraft])
+ form.reset(createDefaultCourtValues());
+ }, [editingCourt, form, initialDraft]);
- const title = editingCourt ? 'Editar cancha' : 'Nueva cancha'
- const buttonText = editingCourt ? 'Guardar cambios' : 'Agregar cancha'
+ const title = editingCourt ? 'Editar cancha' : 'Nueva cancha';
+ const buttonText = editingCourt ? 'Guardar cambios' : 'Agregar cancha';
return (
{title}
- Configura nombre, deporte, duración, precio base y horarios. El backend ya
- contempla precios por franja horaria para la siguiente etapa.
+ Configura nombre, deporte, duración, precio base y horarios. El backend ya contempla precios
+ por franja horaria para la siguiente etapa.
)}
@@ -583,5 +591,5 @@ export function HomePage() {
- )
+ );
}
diff --git a/apps/frontend/src/features/layout/root-layout.tsx b/apps/frontend/src/features/layout/root-layout.tsx
index 9fb6665..f0b88a4 100644
--- a/apps/frontend/src/features/layout/root-layout.tsx
+++ b/apps/frontend/src/features/layout/root-layout.tsx
@@ -1,9 +1,5 @@
-import { Link, Outlet, useNavigate } from '@tanstack/react-router'
-import { useEffect, useMemo, useState } from 'react'
-import { useQuery } from '@tanstack/react-query'
-import { LogOut, Menu, UserRound } from 'lucide-react'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Button } from '@/components/ui/button'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -11,54 +7,56 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu'
-import { useAuth } from '@/lib/auth'
+} from '@/components/ui/dropdown-menu';
+import { apiClient } from '@/lib/api-client';
+import { useAuth } from '@/lib/auth';
import {
getCurrentComplexSlug,
setCurrentComplexSlug as persistCurrentComplexSlug,
-} from '@/lib/current-complex'
-import { apiClient } from '@/lib/api-client'
-import { ThemeSwitcher } from './theme-switcher'
+} from '@/lib/current-complex';
+import { useQuery } from '@tanstack/react-query';
+import { Link, Outlet, useNavigate } from '@tanstack/react-router';
+import { LogOut, Menu, UserRound } from 'lucide-react';
+import { useEffect, useMemo, useState } from 'react';
+import { ThemeSwitcher } from './theme-switcher';
export function RootLayout() {
- const navigate = useNavigate()
- const { isAuthenticated, displayName, initials, avatarUrl, signOut } = useAuth()
- const [currentComplexSlug, setCurrentComplexSlug] = useState(null)
+ const navigate = useNavigate();
+ const { isAuthenticated, displayName, initials, avatarUrl, signOut } = useAuth();
+ const [currentComplexSlug, setCurrentComplexSlug] = useState(null);
const myComplexesQuery = useQuery({
queryKey: ['my-complexes'],
enabled: isAuthenticated,
queryFn: () => apiClient.complexes.listMine(),
- })
+ });
useEffect(() => {
- setCurrentComplexSlug(getCurrentComplexSlug())
- }, [])
+ setCurrentComplexSlug(getCurrentComplexSlug());
+ }, []);
useEffect(() => {
- if (currentComplexSlug) return
+ if (currentComplexSlug) return;
- const fallbackSlug = myComplexesQuery.data?.[0]?.complexSlug
+ const fallbackSlug = myComplexesQuery.data?.[0]?.complexSlug;
if (fallbackSlug) {
- setCurrentComplexSlug(fallbackSlug)
- persistCurrentComplexSlug(fallbackSlug)
+ setCurrentComplexSlug(fallbackSlug);
+ persistCurrentComplexSlug(fallbackSlug);
}
- }, [currentComplexSlug, myComplexesQuery.data])
+ }, [currentComplexSlug, myComplexesQuery.data]);
const currentComplexName = useMemo(() => {
- const complexes = myComplexesQuery.data ?? []
- if (complexes.length === 0) return 'Mi complejo'
+ const complexes = myComplexesQuery.data ?? [];
+ if (complexes.length === 0) return 'Mi complejo';
- const selected = complexes.find(
- (complex) => complex.complexSlug === currentComplexSlug,
- )
+ const selected = complexes.find((complex) => complex.complexSlug === currentComplexSlug);
- return selected?.complexName ?? complexes[0]?.complexName ?? 'Mi complejo'
- }, [currentComplexSlug, myComplexesQuery.data])
+ return selected?.complexName ?? complexes[0]?.complexName ?? 'Mi complejo';
+ }, [currentComplexSlug, myComplexesQuery.data]);
const handleSignOut = async () => {
- await signOut()
- await navigate({ to: '/login' })
- }
+ await signOut();
+ await navigate({ to: '/login' });
+ };
return (
@@ -106,9 +104,7 @@ export function RootLayout() {
{initials}
-
- {displayName}
-
+
{displayName}
@@ -116,7 +112,7 @@ export function RootLayout() {
{
- void navigate({ to: '/profile' })
+ void navigate({ to: '/profile' });
}}
>
@@ -125,7 +121,7 @@ export function RootLayout() {
{
- void handleSignOut()
+ void handleSignOut();
}}
>
@@ -152,14 +148,14 @@ export function RootLayout() {
{
- void navigate({ to: '/' })
+ void navigate({ to: '/' });
}}
>
Home
{
- void navigate({ to: '/about' })
+ void navigate({ to: '/about' });
}}
>
About
@@ -170,7 +166,7 @@ export function RootLayout() {
void navigate({
to: '/complex/$slug/edit',
params: { slug: currentComplexSlug },
- })
+ });
}}
>
Configuración
@@ -182,7 +178,7 @@ export function RootLayout() {
<>
{
- void navigate({ to: '/profile' })
+ void navigate({ to: '/profile' });
}}
>
@@ -191,7 +187,7 @@ export function RootLayout() {
{
- void handleSignOut()
+ void handleSignOut();
}}
>
@@ -201,7 +197,7 @@ export function RootLayout() {
) : (
{
- void navigate({ to: '/login' })
+ void navigate({ to: '/login' });
}}
>
Login
@@ -215,5 +211,5 @@ export function RootLayout() {
- )
+ );
}
diff --git a/apps/frontend/src/features/layout/theme-switcher.tsx b/apps/frontend/src/features/layout/theme-switcher.tsx
index 4b4ebda..031385e 100644
--- a/apps/frontend/src/features/layout/theme-switcher.tsx
+++ b/apps/frontend/src/features/layout/theme-switcher.tsx
@@ -1,5 +1,4 @@
-import { Laptop, Moon, Sun } from 'lucide-react'
-import { Button } from '@/components/ui/button'
+import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -8,21 +7,18 @@ import {
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu'
-import { useTheme } from '@/lib/theme'
+} from '@/components/ui/dropdown-menu';
+import { useTheme } from '@/lib/theme';
+import { Laptop, Moon, Sun } from 'lucide-react';
export function ThemeSwitcher() {
- const { theme, resolvedTheme, setTheme } = useTheme()
+ const { theme, resolvedTheme, setTheme } = useTheme();
return (
- {resolvedTheme === 'dark' ? (
-
- ) : (
-
- )}
+ {resolvedTheme === 'dark' ? : }
@@ -47,5 +43,5 @@ export function ThemeSwitcher() {
- )
+ );
}
diff --git a/apps/frontend/src/features/login/login-page.tsx b/apps/frontend/src/features/login/login-page.tsx
index f35bd5d..f0a3af2 100644
--- a/apps/frontend/src/features/login/login-page.tsx
+++ b/apps/frontend/src/features/login/login-page.tsx
@@ -1,28 +1,28 @@
-import { useState } from 'react'
-import { Link, useNavigate } from '@tanstack/react-router'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { useForm } from 'react-hook-form'
-import { z } from 'zod'
-import { Button } from '@/components/ui/button'
-import { Field, FieldError, FieldLabel } from '@/components/ui/field'
-import { Input } from '@/components/ui/input'
-import { useAuth } from '@/lib/auth'
+import { Button } from '@/components/ui/button';
+import { Field, FieldError, FieldLabel } from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
+import { useAuth } from '@/lib/auth';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Link, useNavigate } from '@tanstack/react-router';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email('IngresƔ un email vƔlido.'),
password: z.string().min(6, 'La contraseƱa debe tener al menos 6 caracteres.'),
-})
+});
-type LoginForm = z.infer
+type LoginForm = z.infer;
type LoginPageProps = {
- redirectTo?: string
-}
+ redirectTo?: string;
+};
export function LoginPage({ redirectTo = '/' }: LoginPageProps) {
- const navigate = useNavigate()
- const { signInWithPassword } = useAuth()
- const [submitError, setSubmitError] = useState(null)
+ const navigate = useNavigate();
+ const { signInWithPassword } = useAuth();
+ const [submitError, setSubmitError] = useState(null);
const {
register,
@@ -35,18 +35,18 @@ export function LoginPage({ redirectTo = '/' }: LoginPageProps) {
email: '',
password: '',
},
- })
+ });
const onSubmit = async (values: LoginForm) => {
- setSubmitError(null)
+ setSubmitError(null);
try {
- await signInWithPassword(values)
- await navigate({ to: redirectTo })
+ await signInWithPassword(values);
+ await navigate({ to: redirectTo });
} catch {
- setSubmitError('No pudimos iniciar sesión. VerificÔ tus credenciales.')
+ setSubmitError('No pudimos iniciar sesión. VerificÔ tus credenciales.');
}
- }
+ };
return (
@@ -96,5 +96,5 @@ export function LoginPage({ redirectTo = '/' }: LoginPageProps) {
- )
+ );
}
diff --git a/apps/frontend/src/features/not-found/not-found-page.tsx b/apps/frontend/src/features/not-found/not-found-page.tsx
index 8f9489b..952b514 100644
--- a/apps/frontend/src/features/not-found/not-found-page.tsx
+++ b/apps/frontend/src/features/not-found/not-found-page.tsx
@@ -1,6 +1,6 @@
-import { Link } from '@tanstack/react-router'
-import { Compass, Home, Sparkles } from 'lucide-react'
-import { Button } from '@/components/ui/button'
+import { Button } from '@/components/ui/button';
+import { Link } from '@tanstack/react-router';
+import { Compass, Home, Sparkles } from 'lucide-react';
export function NotFoundPage() {
return (
@@ -16,7 +16,8 @@ export function NotFoundPage() {
404
PƔgina no encontrada
- La ruta que intentaste abrir no existe o fue movida. VolvƩ al inicio o andƔ al onboarding para continuar.
+ La ruta que intentaste abrir no existe o fue movida. VolvƩ al inicio o andƔ al onboarding
+ para continuar.
@@ -36,5 +37,5 @@ export function NotFoundPage() {
- )
+ );
}
diff --git a/apps/frontend/src/features/onboard/components/onboarding-check-email-step.tsx b/apps/frontend/src/features/onboard/components/onboarding-check-email-step.tsx
index 3814ba9..4ada26b 100644
--- a/apps/frontend/src/features/onboard/components/onboarding-check-email-step.tsx
+++ b/apps/frontend/src/features/onboard/components/onboarding-check-email-step.tsx
@@ -3,5 +3,5 @@ export function OnboardingCheckEmailStep() {
Revisa tu correo y abre el link de verificacion para continuar.
- )
+ );
}
diff --git a/apps/frontend/src/features/onboard/components/onboarding-complete-step.tsx b/apps/frontend/src/features/onboard/components/onboarding-complete-step.tsx
index 49c40ae..f9bd31a 100644
--- a/apps/frontend/src/features/onboard/components/onboarding-complete-step.tsx
+++ b/apps/frontend/src/features/onboard/components/onboarding-complete-step.tsx
@@ -1,19 +1,19 @@
-import type { PlanSummary } from '@repo/api-contract'
-import { Controller, type UseFormReturn } from 'react-hook-form'
-import { Button } from '@/components/ui/button'
-import { Field, FieldError, FieldLabel } from '@/components/ui/field'
-import { Input } from '@/components/ui/input'
-import type { CompleteValues } from '@/features/onboard/onboarding.types'
+import { Button } from '@/components/ui/button';
+import { Field, FieldError, FieldLabel } from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
+import type { CompleteValues } from '@/features/onboard/onboarding.types';
+import type { PlanSummary } from '@repo/api-contract';
+import { Controller, type UseFormReturn } from 'react-hook-form';
type OnboardingCompleteStepProps = {
- form: UseFormReturn
- plans: PlanSummary[]
- verifiedEmail: string | null
- errorMessage: string | null
- infoMessage: string | null
- planPlaceholder: string
- onSubmit: (values: CompleteValues) => Promise
-}
+ form: UseFormReturn;
+ plans: PlanSummary[];
+ verifiedEmail: string | null;
+ errorMessage: string | null;
+ infoMessage: string | null;
+ planPlaceholder: string;
+ onSubmit: (values: CompleteValues) => Promise;
+};
export function OnboardingCompleteStep({
form,
@@ -81,7 +81,7 @@ export function OnboardingCompleteStep({
aria-invalid={Boolean(form.formState.errors.planCode)}
value={field.value ?? ''}
onChange={(event) => {
- field.onChange(event.target.value)
+ field.onChange(event.target.value);
}}
disabled={plans.length === 0}
>
@@ -115,5 +115,5 @@ export function OnboardingCompleteStep({
{form.formState.isSubmitting ? 'Finalizando...' : 'Completar onboarding'}
- )
+ );
}
diff --git a/apps/frontend/src/features/onboard/components/onboarding-layout.tsx b/apps/frontend/src/features/onboard/components/onboarding-layout.tsx
index eafa7b6..9a7801f 100644
--- a/apps/frontend/src/features/onboard/components/onboarding-layout.tsx
+++ b/apps/frontend/src/features/onboard/components/onboarding-layout.tsx
@@ -1,15 +1,11 @@
-import type { PropsWithChildren } from 'react'
+import type { PropsWithChildren } from 'react';
type OnboardingLayoutProps = PropsWithChildren<{
- title: string
- description: string
-}>
+ title: string;
+ description: string;
+}>;
-export function OnboardingLayout({
- title,
- description,
- children,
-}: OnboardingLayoutProps) {
+export function OnboardingLayout({ title, description, children }: OnboardingLayoutProps) {
return (
@@ -18,5 +14,5 @@ export function OnboardingLayout({
{children}
- )
+ );
}
diff --git a/apps/frontend/src/features/onboard/components/onboarding-start-step.tsx b/apps/frontend/src/features/onboard/components/onboarding-start-step.tsx
index 5d270c6..f8f980c 100644
--- a/apps/frontend/src/features/onboard/components/onboarding-start-step.tsx
+++ b/apps/frontend/src/features/onboard/components/onboarding-start-step.tsx
@@ -1,15 +1,15 @@
-import type { UseFormReturn } from 'react-hook-form'
-import { Button } from '@/components/ui/button'
-import { Field, FieldError, FieldLabel } from '@/components/ui/field'
-import { Input } from '@/components/ui/input'
-import type { StartValues } from '@/features/onboard/onboarding.types'
+import { Button } from '@/components/ui/button';
+import { Field, FieldError, FieldLabel } from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
+import type { StartValues } from '@/features/onboard/onboarding.types';
+import type { UseFormReturn } from 'react-hook-form';
type OnboardingStartStepProps = {
- form: UseFormReturn
- errorMessage: string | null
- infoMessage: string | null
- onSubmit: (values: StartValues) => Promise
-}
+ form: UseFormReturn;
+ errorMessage: string | null;
+ infoMessage: string | null;
+ onSubmit: (values: StartValues) => Promise;
+};
export function OnboardingStartStep({
form,
@@ -54,5 +54,5 @@ export function OnboardingStartStep({
{form.formState.isSubmitting ? 'Enviando...' : 'Enviar codigo OTP'}
- )
+ );
}
diff --git a/apps/frontend/src/features/onboard/components/onboarding-verify-otp-step.tsx b/apps/frontend/src/features/onboard/components/onboarding-verify-otp-step.tsx
index 2faf219..05a3a2f 100644
--- a/apps/frontend/src/features/onboard/components/onboarding-verify-otp-step.tsx
+++ b/apps/frontend/src/features/onboard/components/onboarding-verify-otp-step.tsx
@@ -1,26 +1,26 @@
-import { RefreshCw } from 'lucide-react'
-import type { UseFormReturn } from 'react-hook-form'
-import { Button } from '@/components/ui/button'
-import { Field, FieldError, FieldLabel } from '@/components/ui/field'
+import { Button } from '@/components/ui/button';
+import { Field, FieldError, FieldLabel } from '@/components/ui/field';
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
-} from '@/components/ui/input-otp'
-import type { VerifyOtpValues } from '@/features/onboard/onboarding.types'
+} from '@/components/ui/input-otp';
+import type { VerifyOtpValues } from '@/features/onboard/onboarding.types';
+import { RefreshCw } from 'lucide-react';
+import type { UseFormReturn } from 'react-hook-form';
type OnboardingVerifyOtpStepProps = {
- form: UseFormReturn
- email: string | null
- errorMessage: string | null
- infoMessage: string | null
- remainingAttempts: number
- resendCooldownSeconds: number
- isResending: boolean
- onSubmit: (values: VerifyOtpValues) => Promise
- onResend: () => Promise
-}
+ form: UseFormReturn;
+ email: string | null;
+ errorMessage: string | null;
+ infoMessage: string | null;
+ remainingAttempts: number;
+ resendCooldownSeconds: number;
+ isResending: boolean;
+ onSubmit: (values: VerifyOtpValues) => Promise;
+ onResend: () => Promise;
+};
export function OnboardingVerifyOtpStep({
form,
@@ -33,7 +33,7 @@ export function OnboardingVerifyOtpStep({
onSubmit,
onResend,
}: OnboardingVerifyOtpStepProps) {
- const resendDisabled = resendCooldownSeconds > 0 || isResending
+ const resendDisabled = resendCooldownSeconds > 0 || isResending;
return (
@@ -51,7 +51,7 @@ export function OnboardingVerifyOtpStep({
size="sm"
disabled={resendDisabled}
onClick={() => {
- void onResend()
+ void onResend();
}}
>
@@ -70,7 +70,7 @@ export function OnboardingVerifyOtpStep({
pattern="\d*"
value={form.watch('otp')}
onChange={(value) => {
- form.setValue('otp', value, { shouldValidate: true })
+ form.setValue('otp', value, { shouldValidate: true });
}}
>
@@ -104,5 +104,5 @@ export function OnboardingVerifyOtpStep({
{form.formState.isSubmitting ? 'Verificando...' : 'Verificar codigo'}
- )
+ );
}
diff --git a/apps/frontend/src/features/onboard/components/onboarding-verifying-step.tsx b/apps/frontend/src/features/onboard/components/onboarding-verifying-step.tsx
index 08d4dd4..c792468 100644
--- a/apps/frontend/src/features/onboard/components/onboarding-verifying-step.tsx
+++ b/apps/frontend/src/features/onboard/components/onboarding-verifying-step.tsx
@@ -1,7 +1,5 @@
export function OnboardingVerifyingStep() {
return (
-
- Verificando tu email, espera un momento...
-
- )
+ Verificando tu email, espera un momento...
+ );
}
diff --git a/apps/frontend/src/features/onboard/onboard-page.tsx b/apps/frontend/src/features/onboard/onboard-page.tsx
index 94c35ed..d092e22 100644
--- a/apps/frontend/src/features/onboard/onboard-page.tsx
+++ b/apps/frontend/src/features/onboard/onboard-page.tsx
@@ -1,43 +1,43 @@
-import { useEffect, useMemo, useState } from 'react'
-import { useNavigate } from '@tanstack/react-router'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { useForm } from 'react-hook-form'
-import {
- onboardingCompleteSchema,
- onboardingStartSchema,
- onboardingVerifyOtpSchema,
- type PlanSummary,
-} from '@repo/api-contract'
-import { OnboardingCompleteStep } from '@/features/onboard/components/onboarding-complete-step'
-import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout'
-import { OnboardingStartStep } from '@/features/onboard/components/onboarding-start-step'
-import { OnboardingVerifyOtpStep } from '@/features/onboard/components/onboarding-verify-otp-step'
+import { OnboardingCompleteStep } from '@/features/onboard/components/onboarding-complete-step';
+import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
+import { OnboardingStartStep } from '@/features/onboard/components/onboarding-start-step';
+import { OnboardingVerifyOtpStep } from '@/features/onboard/components/onboarding-verify-otp-step';
import type {
CompleteValues,
StartValues,
VerifyOtpValues,
-} from '@/features/onboard/onboarding.types'
-import { apiClient } from '@/lib/api-client'
-import { useAuth } from '@/lib/auth'
-import { setCurrentComplexSlug } from '@/lib/current-complex'
+} from '@/features/onboard/onboarding.types';
+import { apiClient } from '@/lib/api-client';
+import { useAuth } from '@/lib/auth';
+import { setCurrentComplexSlug } from '@/lib/current-complex';
+import { zodResolver } from '@hookform/resolvers/zod';
+import {
+ type PlanSummary,
+ onboardingCompleteSchema,
+ onboardingStartSchema,
+ onboardingVerifyOtpSchema,
+} from '@repo/api-contract';
+import { useNavigate } from '@tanstack/react-router';
+import { useEffect, useMemo, useState } from 'react';
+import { useForm } from 'react-hook-form';
-const OTP_MAX_ATTEMPTS = 5
-type OnboardStep = 'start' | 'verify-otp' | 'complete'
+const OTP_MAX_ATTEMPTS = 5;
+type OnboardStep = 'start' | 'verify-otp' | 'complete';
export function OnboardPage() {
- const navigate = useNavigate()
- const { signInWithPassword } = useAuth()
+ const navigate = useNavigate();
+ const { signInWithPassword } = useAuth();
- const [step, setStep] = useState('start')
- const [requestId, setRequestId] = useState(null)
- const [onboardingEmail, setOnboardingEmail] = useState(null)
- const [verifiedEmail, setVerifiedEmail] = useState(null)
- const [plans, setPlans] = useState([])
- const [remainingAttempts, setRemainingAttempts] = useState(OTP_MAX_ATTEMPTS)
- const [resendCooldownSeconds, setResendCooldownSeconds] = useState(0)
- const [isResending, setIsResending] = useState(false)
- const [errorMessage, setErrorMessage] = useState(null)
- const [infoMessage, setInfoMessage] = useState(null)
+ const [step, setStep] = useState('start');
+ const [requestId, setRequestId] = useState(null);
+ const [onboardingEmail, setOnboardingEmail] = useState(null);
+ const [verifiedEmail, setVerifiedEmail] = useState(null);
+ const [plans, setPlans] = useState([]);
+ const [remainingAttempts, setRemainingAttempts] = useState(OTP_MAX_ATTEMPTS);
+ const [resendCooldownSeconds, setResendCooldownSeconds] = useState(0);
+ const [isResending, setIsResending] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [infoMessage, setInfoMessage] = useState(null);
const startForm = useForm({
resolver: zodResolver(onboardingStartSchema),
@@ -46,12 +46,10 @@ export function OnboardPage() {
fullName: '',
email: '',
},
- })
+ });
const completeForm = useForm({
- resolver: zodResolver(
- onboardingCompleteSchema.omit({ onboardingRequestId: true }),
- ),
+ resolver: zodResolver(onboardingCompleteSchema.omit({ onboardingRequestId: true })),
mode: 'onChange',
defaultValues: {
password: '',
@@ -59,7 +57,7 @@ export function OnboardPage() {
physicalAddress: '',
planCode: '',
},
- })
+ });
const verifyOtpForm = useForm({
resolver: zodResolver(onboardingVerifyOtpSchema.omit({ requestId: true })),
@@ -67,114 +65,114 @@ export function OnboardPage() {
defaultValues: {
otp: '',
},
- })
+ });
useEffect(() => {
- if (step !== 'complete') return
+ if (step !== 'complete') return;
void (async () => {
try {
- const result = await apiClient.plans.list()
- setPlans(result)
+ const result = await apiClient.plans.list();
+ setPlans(result);
} catch {
- setPlans([])
+ setPlans([]);
}
- })()
- }, [step])
+ })();
+ }, [step]);
const planPlaceholder = useMemo(() => {
- if (plans.length === 0) return 'Codigo del plan (por ejemplo: BASIC)'
- return 'Selecciona un plan'
- }, [plans.length])
+ if (plans.length === 0) return 'Codigo del plan (por ejemplo: BASIC)';
+ return 'Selecciona un plan';
+ }, [plans.length]);
useEffect(() => {
- if (resendCooldownSeconds <= 0) return
+ if (resendCooldownSeconds <= 0) return;
const timeout = window.setTimeout(() => {
- setResendCooldownSeconds((current) => Math.max(0, current - 1))
- }, 1000)
+ setResendCooldownSeconds((current) => Math.max(0, current - 1));
+ }, 1000);
return () => {
- window.clearTimeout(timeout)
- }
- }, [resendCooldownSeconds])
+ window.clearTimeout(timeout);
+ };
+ }, [resendCooldownSeconds]);
const onSubmitStart = async (values: StartValues) => {
- setErrorMessage(null)
- setInfoMessage(null)
+ setErrorMessage(null);
+ setInfoMessage(null);
try {
- const result = await apiClient.onboarding.start(values)
- setRequestId(result.requestId)
- setOnboardingEmail(result.email)
- setRemainingAttempts(OTP_MAX_ATTEMPTS)
- setResendCooldownSeconds(result.cooldownSeconds)
- verifyOtpForm.reset({ otp: '' })
- setStep('verify-otp')
- setInfoMessage(result.message)
+ const result = await apiClient.onboarding.start(values);
+ setRequestId(result.requestId);
+ setOnboardingEmail(result.email);
+ setRemainingAttempts(OTP_MAX_ATTEMPTS);
+ setResendCooldownSeconds(result.cooldownSeconds);
+ verifyOtpForm.reset({ otp: '' });
+ setStep('verify-otp');
+ setInfoMessage(result.message);
} catch {
- setErrorMessage('No pudimos iniciar el onboarding. Intenta nuevamente.')
+ setErrorMessage('No pudimos iniciar el onboarding. Intenta nuevamente.');
}
- }
+ };
const onSubmitVerifyOtp = async (values: VerifyOtpValues) => {
if (!requestId) {
- setErrorMessage('No encontramos una solicitud de onboarding activa.')
- return
+ setErrorMessage('No encontramos una solicitud de onboarding activa.');
+ return;
}
- setErrorMessage(null)
+ setErrorMessage(null);
try {
const result = await apiClient.onboarding.verifyOtp({
requestId,
otp: values.otp,
- })
+ });
- setRemainingAttempts(result.remainingAttempts)
- setResendCooldownSeconds(result.cooldownSeconds)
- setInfoMessage(result.message)
+ setRemainingAttempts(result.remainingAttempts);
+ setResendCooldownSeconds(result.cooldownSeconds);
+ setInfoMessage(result.message);
if (result.verified) {
- setVerifiedEmail(result.email ?? onboardingEmail)
- setStep('complete')
- return
+ setVerifiedEmail(result.email ?? onboardingEmail);
+ setStep('complete');
+ return;
}
} catch {
- setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.')
+ setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.');
}
- }
+ };
const onResendOtp = async () => {
if (!requestId) {
- setErrorMessage('No encontramos una solicitud de onboarding activa.')
- return
+ setErrorMessage('No encontramos una solicitud de onboarding activa.');
+ return;
}
- setIsResending(true)
- setErrorMessage(null)
+ setIsResending(true);
+ setErrorMessage(null);
try {
- const result = await apiClient.onboarding.resendOtp({ requestId })
- setOnboardingEmail(result.email)
- setRemainingAttempts(result.remainingAttempts)
- setResendCooldownSeconds(result.cooldownSeconds)
- verifyOtpForm.reset({ otp: '' })
- setInfoMessage(result.message)
+ const result = await apiClient.onboarding.resendOtp({ requestId });
+ setOnboardingEmail(result.email);
+ setRemainingAttempts(result.remainingAttempts);
+ setResendCooldownSeconds(result.cooldownSeconds);
+ verifyOtpForm.reset({ otp: '' });
+ setInfoMessage(result.message);
} catch {
- setErrorMessage('No pudimos reenviar el OTP. Intenta nuevamente.')
+ setErrorMessage('No pudimos reenviar el OTP. Intenta nuevamente.');
} finally {
- setIsResending(false)
+ setIsResending(false);
}
- }
+ };
const onSubmitComplete = async (values: CompleteValues) => {
if (!requestId) {
- setErrorMessage('No encontramos una solicitud de onboarding activa.')
- return
+ setErrorMessage('No encontramos una solicitud de onboarding activa.');
+ return;
}
- setErrorMessage(null)
+ setErrorMessage(null);
try {
const result = await apiClient.onboarding.complete({
@@ -183,19 +181,19 @@ export function OnboardPage() {
complexName: values.complexName,
physicalAddress: values.physicalAddress,
planCode: values.planCode,
- })
- setCurrentComplexSlug(result.complexSlug)
+ });
+ setCurrentComplexSlug(result.complexSlug);
- const emailForSignIn = verifiedEmail ?? onboardingEmail
+ const emailForSignIn = verifiedEmail ?? onboardingEmail;
if (emailForSignIn) {
- await signInWithPassword({ email: emailForSignIn, password: values.password })
+ await signInWithPassword({ email: emailForSignIn, password: values.password });
}
- await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } })
+ await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } });
} catch {
- setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.')
+ setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.');
}
- }
+ };
return (
)}
- )
+ );
}
diff --git a/apps/frontend/src/features/onboard/onboarding.types.ts b/apps/frontend/src/features/onboard/onboarding.types.ts
index aecb69c..6810078 100644
--- a/apps/frontend/src/features/onboard/onboarding.types.ts
+++ b/apps/frontend/src/features/onboard/onboarding.types.ts
@@ -2,15 +2,9 @@ import {
onboardingCompleteSchema,
onboardingStartSchema,
onboardingVerifyOtpSchema,
-} from '@repo/api-contract'
-import { z } from 'zod'
+} from '@repo/api-contract';
+import { z } from 'zod';
-export type StartValues = z.infer
-export type VerifyOtpValues = Omit<
- z.infer,
- 'requestId'
->
-export type CompleteValues = Omit<
- z.infer,
- 'onboardingRequestId'
->
+export type StartValues = z.infer;
+export type VerifyOtpValues = Omit, 'requestId'>;
+export type CompleteValues = Omit, 'onboardingRequestId'>;
diff --git a/apps/frontend/src/features/profile/profile-page.tsx b/apps/frontend/src/features/profile/profile-page.tsx
index c5f3972..a5ef6d4 100644
--- a/apps/frontend/src/features/profile/profile-page.tsx
+++ b/apps/frontend/src/features/profile/profile-page.tsx
@@ -1,11 +1,5 @@
-import { Link } from '@tanstack/react-router'
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import { useForm } from 'react-hook-form'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { z } from 'zod'
-import type { UserProfileResponse } from '@repo/api-contract'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Button } from '@/components/ui/button'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
@@ -13,42 +7,51 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from '@/components/ui/form'
-import { Input } from '@/components/ui/input'
-import { apiClient } from '@/lib/api-client'
-import { useAuth } from '@/lib/auth'
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { apiClient } from '@/lib/api-client';
+import { useAuth } from '@/lib/auth';
+import { zodResolver } from '@hookform/resolvers/zod';
+import type { UserProfileResponse } from '@repo/api-contract';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { Link } from '@tanstack/react-router';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
const updateProfileSchema = z.object({
fullName: z.string().min(1, 'El nombre es requerido'),
-})
+});
-const updatePasswordSchema = z.object({
- currentPassword: z.string().min(1, 'La contraseƱa actual es requerida'),
- newPassword: z.string().min(6, 'La nueva contraseƱa debe tener al menos 6 caracteres'),
- confirmPassword: z.string().min(1, 'La confirmación de contraseña es requerida'),
-}).refine((data) => data.newPassword === data.confirmPassword, {
- message: 'Las contraseƱas no coinciden',
- path: ['confirmPassword'],
-})
+const updatePasswordSchema = z
+ .object({
+ currentPassword: z.string().min(1, 'La contraseƱa actual es requerida'),
+ newPassword: z.string().min(6, 'La nueva contraseƱa debe tener al menos 6 caracteres'),
+ confirmPassword: z.string().min(1, 'La confirmación de contraseña es requerida'),
+ })
+ .refine((data) => data.newPassword === data.confirmPassword, {
+ message: 'Las contraseƱas no coinciden',
+ path: ['confirmPassword'],
+ });
-type UpdateProfileForm = z.infer
-type UpdatePasswordForm = z.infer
+type UpdateProfileForm = z.infer;
+type UpdatePasswordForm = z.infer;
export function ProfilePage() {
- const { user, displayName, initials, avatarUrl, isAuthenticated, updateProfile, updatePassword } = useAuth()
- const queryClient = useQueryClient()
+ const { user, displayName, initials, avatarUrl, isAuthenticated, updateProfile, updatePassword } =
+ useAuth();
+ const queryClient = useQueryClient();
const profileQuery = useQuery({
queryKey: ['user-profile'],
enabled: isAuthenticated,
queryFn: (): Promise => apiClient.user.getProfile(),
- })
+ });
const updateProfileForm = useForm({
resolver: zodResolver(updateProfileSchema),
defaultValues: {
fullName: displayName,
},
- })
+ });
const updatePasswordForm = useForm({
resolver: zodResolver(updatePasswordSchema),
@@ -57,34 +60,34 @@ export function ProfilePage() {
newPassword: '',
confirmPassword: '',
},
- })
+ });
const updateProfileMutation = useMutation({
mutationFn: updateProfile,
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['user-profile'] })
- updateProfileForm.reset()
+ queryClient.invalidateQueries({ queryKey: ['user-profile'] });
+ updateProfileForm.reset();
},
- })
+ });
const updatePasswordMutation = useMutation({
mutationFn: async (data: UpdatePasswordForm) => {
// Note: Supabase doesn't require current password for password update
// The user needs to be recently authenticated
- await updatePassword({ password: data.newPassword })
+ await updatePassword({ password: data.newPassword });
},
onSuccess: () => {
- updatePasswordForm.reset()
+ updatePasswordForm.reset();
},
- })
+ });
const onUpdateProfile = (data: UpdateProfileForm) => {
- updateProfileMutation.mutate(data)
- }
+ updateProfileMutation.mutate(data);
+ };
const onUpdatePassword = (data: UpdatePasswordForm) => {
- updatePasswordMutation.mutate(data)
- }
+ updatePasswordMutation.mutate(data);
+ };
if (!isAuthenticated) {
return (
@@ -99,7 +102,7 @@ export function ProfilePage() {
- )
+ );
}
return (
@@ -122,9 +125,7 @@ export function ProfilePage() {
{profileQuery.data?.email ?? user?.email}
{profileQuery.data?.role && (
-
- Rol: {profileQuery.data.role}
-
+ Rol: {profileQuery.data.role}
)}
@@ -157,10 +158,7 @@ export function ProfilePage() {
)}
/>
-
+
{updateProfileMutation.isPending ? 'Guardando...' : 'Guardar cambios'}
@@ -171,16 +169,17 @@ export function ProfilePage() {
)}
{updateProfileMutation.isSuccess && (
-
- Perfil actualizado exitosamente.
-
+ Perfil actualizado exitosamente.
)}
Cambiar ContraseƱa
-
+
)}
/>
-
+
{updatePasswordMutation.isPending ? 'Cambiando...' : 'Cambiar contraseƱa'}
@@ -234,12 +230,10 @@ export function ProfilePage() {
)}
{updatePasswordMutation.isSuccess && (
-
- ContraseƱa cambiada exitosamente.
-
+ ContraseƱa cambiada exitosamente.
)}
- )
+ );
}
diff --git a/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx b/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx
index 2100583..d0a9261 100644
--- a/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx
+++ b/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx
@@ -1,25 +1,25 @@
-import type { PublicBookingConfirmation } from '@repo/api-contract'
-import { siWhatsapp } from 'simple-icons'
-import { Button } from '@/components/ui/button'
+import { Button } from '@/components/ui/button';
+import type { PublicBookingConfirmation } from '@repo/api-contract';
+import { siWhatsapp } from 'simple-icons';
type ShareWhatsappButtonProps = {
- confirmation: PublicBookingConfirmation
-}
+ confirmation: PublicBookingConfirmation;
+};
function formatDateLabel(isoDate: string) {
- const [year, month, day] = isoDate.split('-').map(Number)
- const date = new Date(year, (month ?? 1) - 1, day ?? 1)
+ const [year, month, day] = isoDate.split('-').map(Number);
+ const date = new Date(year, (month ?? 1) - 1, day ?? 1);
return new Intl.DateTimeFormat('es-AR', {
weekday: 'long',
day: '2-digit',
month: 'long',
year: 'numeric',
- }).format(date)
+ }).format(date);
}
function createWhatsappMessage(confirmation: PublicBookingConfirmation) {
- const dateLabel = formatDateLabel(confirmation.date)
+ const dateLabel = formatDateLabel(confirmation.date);
return [
'Ya reservamos cancha para jugar.',
`Complejo: ${confirmation.complexName}`,
@@ -27,13 +27,13 @@ function createWhatsappMessage(confirmation: PublicBookingConfirmation) {
`Fecha: ${dateLabel}`,
`Horario: ${confirmation.startTime} - ${confirmation.endTime}`,
`Codigo de reserva: ${confirmation.bookingCode}`,
- ].join('\n')
+ ].join('\n');
}
export function ShareWhatsappButton({ confirmation }: ShareWhatsappButtonProps) {
const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(
- createWhatsappMessage(confirmation),
- )}`
+ createWhatsappMessage(confirmation)
+ )}`;
return (
@@ -49,5 +49,5 @@ export function ShareWhatsappButton({ confirmation }: ShareWhatsappButtonProps)
Compartir por WhatsApp
- )
+ );
}
diff --git a/apps/frontend/src/features/public-booking/public-booking-confirmation-page.tsx b/apps/frontend/src/features/public-booking/public-booking-confirmation-page.tsx
index 5dd63ef..28406f2 100644
--- a/apps/frontend/src/features/public-booking/public-booking-confirmation-page.tsx
+++ b/apps/frontend/src/features/public-booking/public-booking-confirmation-page.tsx
@@ -1,43 +1,43 @@
-import { useQuery } from '@tanstack/react-query'
-import { useNavigate } from '@tanstack/react-router'
-import { Button } from '@/components/ui/button'
-import { ApiClientError, apiClient } from '@/lib/api-client'
-import { ShareWhatsappButton } from './components/share-whatsapp-button'
+import { Button } from '@/components/ui/button';
+import { ApiClientError, apiClient } from '@/lib/api-client';
+import { useQuery } from '@tanstack/react-query';
+import { useNavigate } from '@tanstack/react-router';
+import { ShareWhatsappButton } from './components/share-whatsapp-button';
type PublicBookingConfirmationPageProps = {
- complexSlug: string
- bookingCode: string
-}
+ complexSlug: string;
+ bookingCode: string;
+};
function extractMessage(error: unknown, fallback: string) {
if (error instanceof ApiClientError) {
- return error.message || fallback
+ return error.message || fallback;
}
- return fallback
+ return fallback;
}
function formatDateLabel(isoDate: string) {
- const [year, month, day] = isoDate.split('-').map(Number)
- const date = new Date(year, (month ?? 1) - 1, day ?? 1)
+ const [year, month, day] = isoDate.split('-').map(Number);
+ const date = new Date(year, (month ?? 1) - 1, day ?? 1);
return new Intl.DateTimeFormat('es-AR', {
weekday: 'long',
day: '2-digit',
month: 'long',
year: 'numeric',
- }).format(date)
+ }).format(date);
}
export function PublicBookingConfirmationPage({
complexSlug,
bookingCode,
}: PublicBookingConfirmationPageProps) {
- const navigate = useNavigate()
+ const navigate = useNavigate();
const confirmationQuery = useQuery({
queryKey: ['public-booking-confirmation', complexSlug, bookingCode],
queryFn: () => apiClient.publicBookings.getConfirmation(complexSlug, bookingCode),
- })
+ });
return (
@@ -51,7 +51,7 @@ export function PublicBookingConfirmationPage({
{extractMessage(
confirmationQuery.error,
- 'No pudimos cargar la confirmacion de la reserva.',
+ 'No pudimos cargar la confirmacion de la reserva.'
)}
)}
@@ -100,7 +100,7 @@ export function PublicBookingConfirmationPage({
void navigate({
to: '/$complexSlug/booking',
params: { complexSlug },
- })
+ });
}}
>
Hacer otra reserva
@@ -111,5 +111,5 @@ export function PublicBookingConfirmationPage({
- )
+ );
}
diff --git a/apps/frontend/src/features/public-booking/public-booking-page.tsx b/apps/frontend/src/features/public-booking/public-booking-page.tsx
index efb3513..52f9b8e 100644
--- a/apps/frontend/src/features/public-booking/public-booking-page.tsx
+++ b/apps/frontend/src/features/public-booking/public-booking-page.tsx
@@ -1,22 +1,22 @@
-import { zodResolver } from '@hookform/resolvers/zod'
-import { useMutation, useQuery } from '@tanstack/react-query'
-import { useNavigate } from '@tanstack/react-router'
-import { useEffect, useMemo, useState } from 'react'
-import { useForm } from 'react-hook-form'
-import { z } from 'zod'
-import { Button } from '@/components/ui/button'
-import { Field, FieldError, FieldLabel } from '@/components/ui/field'
-import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button';
+import { Field, FieldError, FieldLabel } from '@/components/ui/field';
+import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from '@/components/ui/select'
-import { ApiClientError, apiClient } from '@/lib/api-client'
+} from '@/components/ui/select';
+import { ApiClientError, apiClient } from '@/lib/api-client';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { useNavigate } from '@tanstack/react-router';
+import { useEffect, useMemo, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
-const DAYS_STEP = 5
+const DAYS_STEP = 5;
const bookingFormSchema = z.object({
customerName: z
@@ -29,93 +29,93 @@ const bookingFormSchema = z.object({
.trim()
.min(6, 'Ingresa un telefono valido.')
.max(30, 'El telefono no puede superar los 30 caracteres.'),
-})
+});
-type BookingFormValues = z.infer
+type BookingFormValues = z.infer;
type PublicBookingPageProps = {
- complexSlug: string
-}
+ complexSlug: string;
+};
type SelectedSlot = {
- courtId: string
- courtName: string
- sportId: string
- sportName: string
- startTime: string
- endTime: string
-}
+ courtId: string;
+ courtName: string;
+ sportId: string;
+ sportName: string;
+ startTime: string;
+ endTime: string;
+};
function addDays(baseDate: Date, amount: number) {
- const next = new Date(baseDate)
- next.setDate(next.getDate() + amount)
- return next
+ const next = new Date(baseDate);
+ next.setDate(next.getDate() + amount);
+ return next;
}
function toIsoDateLocal(date: Date) {
- const year = date.getFullYear()
- const month = String(date.getMonth() + 1).padStart(2, '0')
- const day = String(date.getDate()).padStart(2, '0')
- return `${year}-${month}-${day}`
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
}
function toMinutes(value: string): number {
- const [hours, minutes] = value.split(':').map((part) => Number(part))
- return hours * 60 + minutes
+ const [hours, minutes] = value.split(':').map((part) => Number(part));
+ return hours * 60 + minutes;
}
function formatDayLabel(date: Date) {
- const weekday = new Intl.DateTimeFormat('es-AR', { weekday: 'short' }).format(date)
+ const weekday = new Intl.DateTimeFormat('es-AR', { weekday: 'short' }).format(date);
const dayMonth = new Intl.DateTimeFormat('es-AR', {
day: '2-digit',
month: '2-digit',
- }).format(date)
+ }).format(date);
return {
weekday: weekday.replace('.', ''),
dayMonth,
- }
+ };
}
function extractMessage(error: unknown, fallback: string) {
if (error instanceof ApiClientError) {
- return error.message || fallback
+ return error.message || fallback;
}
- return fallback
+ return fallback;
}
export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
- const navigate = useNavigate()
- const today = useMemo(() => new Date(), [])
- const todayIsoDate = useMemo(() => toIsoDateLocal(today), [today])
- const [windowStartOffset, setWindowStartOffset] = useState(0)
- const [selectedSportId, setSelectedSportId] = useState()
- const [selectedSlot, setSelectedSlot] = useState(null)
- const [navigationError, setNavigationError] = useState(null)
- const [hasAutoAdjustedInitialDate, setHasAutoAdjustedInitialDate] = useState(false)
- const [autoAdjustedDateNotice, setAutoAdjustedDateNotice] = useState(null)
+ const navigate = useNavigate();
+ const today = useMemo(() => new Date(), []);
+ const todayIsoDate = useMemo(() => toIsoDateLocal(today), [today]);
+ const [windowStartOffset, setWindowStartOffset] = useState(0);
+ const [selectedSportId, setSelectedSportId] = useState();
+ const [selectedSlot, setSelectedSlot] = useState(null);
+ const [navigationError, setNavigationError] = useState(null);
+ const [hasAutoAdjustedInitialDate, setHasAutoAdjustedInitialDate] = useState(false);
+ const [autoAdjustedDateNotice, setAutoAdjustedDateNotice] = useState(null);
const dayOptions = useMemo(() => {
return Array.from({ length: DAYS_STEP }).map((_, index) => {
- const current = addDays(today, windowStartOffset + index)
+ const current = addDays(today, windowStartOffset + index);
return {
value: toIsoDateLocal(current),
date: current,
...formatDayLabel(current),
- }
- })
- }, [today, windowStartOffset])
+ };
+ });
+ }, [today, windowStartOffset]);
- const [selectedDate, setSelectedDate] = useState(dayOptions[0]?.value ?? '')
+ const [selectedDate, setSelectedDate] = useState(dayOptions[0]?.value ?? '');
useEffect(() => {
if (dayOptions.some((option) => option.value === selectedDate)) {
- return
+ return;
}
- setSelectedDate(dayOptions[0]?.value ?? '')
- }, [dayOptions, selectedDate])
+ setSelectedDate(dayOptions[0]?.value ?? '');
+ }, [dayOptions, selectedDate]);
const availabilityQuery = useQuery({
queryKey: ['public-booking-availability', complexSlug, selectedDate, selectedSportId],
@@ -125,26 +125,23 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
date: selectedDate,
...(selectedSportId ? { sportId: selectedSportId } : {}),
}),
- })
+ });
useEffect(() => {
- const availability = availabilityQuery.data
- if (!availability) return
+ const availability = availabilityQuery.data;
+ if (!availability) return;
if (!availability.sportSelectionRequired) {
if (!selectedSportId && availability.sports[0]) {
- setSelectedSportId(availability.sports[0].id)
+ setSelectedSportId(availability.sports[0].id);
}
- return
+ return;
}
- if (
- selectedSportId &&
- !availability.sports.some((sport) => sport.id === selectedSportId)
- ) {
- setSelectedSportId(undefined)
+ if (selectedSportId && !availability.sports.some((sport) => sport.id === selectedSportId)) {
+ setSelectedSportId(undefined);
}
- }, [availabilityQuery.data, selectedSportId])
+ }, [availabilityQuery.data, selectedSportId]);
const {
register,
@@ -158,12 +155,12 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
customerName: '',
customerPhone: '',
},
- })
+ });
const createBookingMutation = useMutation({
mutationFn: (payload: BookingFormValues) => {
if (!selectedSlot) {
- throw new Error('Debes seleccionar un horario.')
+ throw new Error('Debes seleccionar un horario.');
}
return apiClient.publicBookings.create(complexSlug, {
@@ -173,123 +170,125 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
startTime: selectedSlot.startTime,
customerName: payload.customerName,
customerPhone: payload.customerPhone,
- })
+ });
},
- })
+ });
const onSubmit = async (values: BookingFormValues) => {
- setNavigationError(null)
- const booking = await createBookingMutation.mutateAsync(values)
+ setNavigationError(null);
+ const booking = await createBookingMutation.mutateAsync(values);
if (!booking.bookingCode) {
- setNavigationError('No se pudo obtener el codigo de confirmacion de la reserva.')
- return
+ setNavigationError('No se pudo obtener el codigo de confirmacion de la reserva.');
+ return;
}
- reset()
- setSelectedSlot(null)
- await availabilityQuery.refetch()
+ reset();
+ setSelectedSlot(null);
+ await availabilityQuery.refetch();
await navigate({
to: '/$complexSlug/booking/confirmed/$bookingCode',
params: {
complexSlug,
bookingCode: booking.bookingCode,
},
- })
- }
+ });
+ };
const visibleCourts = useMemo(() => {
- const courts = availabilityQuery.data?.courts ?? []
+ const courts = availabilityQuery.data?.courts ?? [];
if (selectedDate !== todayIsoDate) {
- return courts
+ return courts;
}
- const now = new Date()
- const nowMinutes = now.getHours() * 60 + now.getMinutes()
+ const now = new Date();
+ const nowMinutes = now.getHours() * 60 + now.getMinutes();
return courts
.map((court) => ({
...court,
availableSlots: court.availableSlots.filter(
- (slot) => toMinutes(slot.startTime) >= nowMinutes,
+ (slot) => toMinutes(slot.startTime) >= nowMinutes
),
}))
- .filter((court) => court.availableSlots.length > 0)
- }, [availabilityQuery.data?.courts, selectedDate, todayIsoDate])
+ .filter((court) => court.availableSlots.length > 0);
+ }, [availabilityQuery.data?.courts, selectedDate, todayIsoDate]);
useEffect(() => {
- if (!selectedSlot) return
+ if (!selectedSlot) return;
const stillAvailable = visibleCourts.some(
(court) =>
court.courtId === selectedSlot.courtId &&
- court.availableSlots.some((slot) => slot.startTime === selectedSlot.startTime),
- )
+ court.availableSlots.some((slot) => slot.startTime === selectedSlot.startTime)
+ );
if (!stillAvailable) {
- setSelectedSlot(null)
+ setSelectedSlot(null);
}
- }, [selectedSlot, visibleCourts])
+ }, [selectedSlot, visibleCourts]);
useEffect(() => {
- if (hasAutoAdjustedInitialDate) return
+ if (hasAutoAdjustedInitialDate) return;
if (selectedDate !== todayIsoDate) {
- setHasAutoAdjustedInitialDate(true)
- return
+ setHasAutoAdjustedInitialDate(true);
+ return;
}
if (availabilityQuery.isLoading || availabilityQuery.isError || !availabilityQuery.data) {
- return
+ return;
}
if (availabilityQuery.data.sportSelectionRequired && !selectedSportId) {
- return
+ return;
}
if (visibleCourts.length > 0) {
- setHasAutoAdjustedInitialDate(true)
- return
+ setHasAutoAdjustedInitialDate(true);
+ return;
}
- let cancelled = false
+ let cancelled = false;
const findNextAvailableDate = async () => {
for (let dayOffset = 1; dayOffset <= 30; dayOffset += 1) {
- const candidateDate = toIsoDateLocal(addDays(today, dayOffset))
+ const candidateDate = toIsoDateLocal(addDays(today, dayOffset));
try {
const availability = await apiClient.publicBookings.getAvailability(complexSlug, {
date: candidateDate,
...(selectedSportId ? { sportId: selectedSportId } : {}),
- })
+ });
if (availability.courts.length === 0) {
- continue
+ continue;
}
- if (cancelled) return
+ if (cancelled) return;
- setWindowStartOffset(dayOffset)
- setSelectedDate(candidateDate)
- setSelectedSlot(null)
- setAutoAdjustedDateNotice('No habia turnos disponibles para hoy. Te mostramos el proximo dia con turnos.')
- setHasAutoAdjustedInitialDate(true)
- return
+ setWindowStartOffset(dayOffset);
+ setSelectedDate(candidateDate);
+ setSelectedSlot(null);
+ setAutoAdjustedDateNotice(
+ 'No habia turnos disponibles para hoy. Te mostramos el proximo dia con turnos.'
+ );
+ setHasAutoAdjustedInitialDate(true);
+ return;
} catch {
- break
+ break;
}
}
if (!cancelled) {
- setHasAutoAdjustedInitialDate(true)
+ setHasAutoAdjustedInitialDate(true);
}
- }
+ };
- void findNextAvailableDate()
+ void findNextAvailableDate();
return () => {
- cancelled = true
- }
+ cancelled = true;
+ };
}, [
availabilityQuery.data,
availabilityQuery.isError,
@@ -301,33 +300,33 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
today,
todayIsoDate,
visibleCourts.length,
- ])
+ ]);
- const canGoBack = windowStartOffset > 0
+ const canGoBack = windowStartOffset > 0;
const goToPreviousFiveDays = () => {
- if (!canGoBack) return
+ if (!canGoBack) return;
setWindowStartOffset((previous) => {
- const next = Math.max(0, previous - DAYS_STEP)
- const nextStartDate = toIsoDateLocal(addDays(today, next))
- setSelectedDate(nextStartDate)
- setSelectedSlot(null)
- setAutoAdjustedDateNotice(null)
- return next
- })
- }
+ const next = Math.max(0, previous - DAYS_STEP);
+ const nextStartDate = toIsoDateLocal(addDays(today, next));
+ setSelectedDate(nextStartDate);
+ setSelectedSlot(null);
+ setAutoAdjustedDateNotice(null);
+ return next;
+ });
+ };
const goToNextFiveDays = () => {
setWindowStartOffset((previous) => {
- const next = previous + DAYS_STEP
- const nextStartDate = toIsoDateLocal(addDays(today, next))
- setSelectedDate(nextStartDate)
- setSelectedSlot(null)
- setAutoAdjustedDateNotice(null)
- return next
- })
- }
+ const next = previous + DAYS_STEP;
+ const nextStartDate = toIsoDateLocal(addDays(today, next));
+ setSelectedDate(nextStartDate);
+ setSelectedSlot(null);
+ setAutoAdjustedDateNotice(null);
+ return next;
+ });
+ };
return (
@@ -372,9 +371,9 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
key={option.value}
type="button"
onClick={() => {
- setSelectedDate(option.value)
- setSelectedSlot(null)
- setAutoAdjustedDateNotice(null)
+ setSelectedDate(option.value);
+ setSelectedSlot(null);
+ setAutoAdjustedDateNotice(null);
}}
className={`rounded-xl border px-1 py-2 text-center transition-colors sm:px-2 ${
selectedDate === option.value
@@ -416,9 +415,9 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
{
- setSelectedSportId(value)
- setSelectedSlot(null)
- setAutoAdjustedDateNotice(null)
+ setSelectedSportId(value);
+ setSelectedSlot(null);
+ setAutoAdjustedDateNotice(null);
}}
>
@@ -479,7 +478,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
{court.availableSlots.map((slot) => {
const isSelected =
selectedSlot?.courtId === court.courtId &&
- selectedSlot.startTime === slot.startTime
+ selectedSlot.startTime === slot.startTime;
return (
{slot.startTime}
- )
+ );
})}
@@ -548,9 +547,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
{extractMessage(createBookingMutation.error, 'No pudimos confirmar el turno.')}
)}
- {navigationError && (
- {navigationError}
- )}
+ {navigationError && {navigationError}
}
{createBookingMutation.isPending ? 'Confirmando...' : 'Confirmar turno'}
@@ -560,5 +557,5 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
)}
- )
+ );
}
diff --git a/apps/frontend/src/features/server-error/server-error-page.tsx b/apps/frontend/src/features/server-error/server-error-page.tsx
index 5d4032d..6441b22 100644
--- a/apps/frontend/src/features/server-error/server-error-page.tsx
+++ b/apps/frontend/src/features/server-error/server-error-page.tsx
@@ -1,6 +1,6 @@
-import { Link, type ErrorComponentProps } from '@tanstack/react-router'
-import { AlertTriangle, Home, RefreshCw, Route } from 'lucide-react'
-import { Button } from '@/components/ui/button'
+import { Button } from '@/components/ui/button';
+import { type ErrorComponentProps, Link } from '@tanstack/react-router';
+import { AlertTriangle, Home, RefreshCw, Route } from 'lucide-react';
export function ServerErrorPage({ error, reset }: ErrorComponentProps) {
return (
@@ -47,5 +47,5 @@ export function ServerErrorPage({ error, reset }: ErrorComponentProps) {
- )
+ );
}
diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css
index fb3c7e9..e49f238 100644
--- a/apps/frontend/src/index.css
+++ b/apps/frontend/src/index.css
@@ -6,125 +6,125 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
- --font-heading: var(--font-sans);
- --font-sans: 'Geist Variable', sans-serif;
- --color-sidebar-ring: var(--sidebar-ring);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar: var(--sidebar);
- --color-chart-5: var(--chart-5);
- --color-chart-4: var(--chart-4);
- --color-chart-3: var(--chart-3);
- --color-chart-2: var(--chart-2);
- --color-chart-1: var(--chart-1);
- --color-ring: var(--ring);
- --color-input: var(--input);
- --color-border: var(--border);
- --color-destructive: var(--destructive);
- --color-accent-foreground: var(--accent-foreground);
- --color-accent: var(--accent);
- --color-muted-foreground: var(--muted-foreground);
- --color-muted: var(--muted);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-secondary: var(--secondary);
- --color-primary-foreground: var(--primary-foreground);
- --color-primary: var(--primary);
- --color-popover-foreground: var(--popover-foreground);
- --color-popover: var(--popover);
- --color-card-foreground: var(--card-foreground);
- --color-card: var(--card);
- --color-foreground: var(--foreground);
- --color-background: var(--background);
- --radius-sm: calc(var(--radius) * 0.6);
- --radius-md: calc(var(--radius) * 0.8);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) * 1.4);
- --radius-2xl: calc(var(--radius) * 1.8);
- --radius-3xl: calc(var(--radius) * 2.2);
- --radius-4xl: calc(var(--radius) * 2.6);
+ --font-heading: var(--font-sans);
+ --font-sans: "Geist Variable", sans-serif;
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --color-foreground: var(--foreground);
+ --color-background: var(--background);
+ --radius-sm: calc(var(--radius) * 0.6);
+ --radius-md: calc(var(--radius) * 0.8);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) * 1.4);
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
}
:root {
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.87 0 0);
- --chart-2: oklch(0.556 0 0);
- --chart-3: oklch(0.439 0 0);
- --chart-4: oklch(0.371 0 0);
- --chart-5: oklch(0.269 0 0);
- --radius: 0.625rem;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
}
.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.87 0 0);
- --chart-2: oklch(0.556 0 0);
- --chart-3: oklch(0.439 0 0);
- --chart-4: oklch(0.371 0 0);
- --chart-5: oklch(0.269 0 0);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
- }
+ }
body {
@apply bg-background text-foreground;
- }
+ }
html {
@apply font-sans;
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/apps/frontend/src/lib/api-client.ts b/apps/frontend/src/lib/api-client.ts
index e2d71d2..8764afd 100644
--- a/apps/frontend/src/lib/api-client.ts
+++ b/apps/frontend/src/lib/api-client.ts
@@ -1,13 +1,12 @@
-import axios, { AxiosError } from 'axios'
import type {
AdminBooking,
- CreateAdminBookingInput,
- Court,
- CreateCourtInput,
- CreatePublicBookingInput,
Complex,
+ Court,
+ CreateAdminBookingInput,
CreateComplexPayload,
CreateComplexResponse,
+ CreateCourtInput,
+ CreatePublicBookingInput,
CreateSportInput,
OnboardingCompleteInput,
OnboardingCompleteResponse,
@@ -23,48 +22,49 @@ import type {
PublicBookingConfirmation,
Sport,
UpdateAdminBookingStatusInput,
- UpdateCourtInput,
UpdateComplexPayload,
UpdateComplexResponse,
+ UpdateCourtInput,
UpdateSportInput,
UserProfileResponse,
-} from '@repo/api-contract'
+} from '@repo/api-contract';
+import axios, { AxiosError } from 'axios';
const apiBaseUrl =
import.meta.env.VITE_API_BASE_URL?.trim() ||
- (import.meta.env.DEV ? 'http://localhost:3000' : window.location.origin)
-let accessToken: string | null = null
+ (import.meta.env.DEV ? 'http://localhost:3000' : window.location.origin);
+let accessToken: string | null = null;
export class ApiClientError extends Error {
- status?: number
- details?: unknown
- causeError?: unknown
+ status?: number;
+ details?: unknown;
+ causeError?: unknown;
constructor(
message: string,
- options?: { status?: number; details?: unknown; causeError?: unknown },
+ options?: { status?: number; details?: unknown; causeError?: unknown }
) {
- super(message)
- this.name = 'ApiClientError'
- this.status = options?.status
- this.details = options?.details
- this.causeError = options?.causeError
+ super(message);
+ this.name = 'ApiClientError';
+ this.status = options?.status;
+ this.details = options?.details;
+ this.causeError = options?.causeError;
}
}
type ErrorHandlers = {
- onForbidden?: (error: ApiClientError) => void | Promise
- onError?: (error: ApiClientError) => void | Promise
-}
+ onForbidden?: (error: ApiClientError) => void | Promise;
+ onError?: (error: ApiClientError) => void | Promise;
+};
-const handlers: ErrorHandlers = {}
+const handlers: ErrorHandlers = {};
const http = axios.create({
baseURL: apiBaseUrl,
headers: {
'Content-Type': 'application/json',
},
-})
+});
function extractMessage(details: unknown, fallback: string) {
if (
@@ -73,239 +73,221 @@ function extractMessage(details: unknown, fallback: string) {
'message' in details &&
typeof details.message === 'string'
) {
- return details.message
+ return details.message;
}
- return fallback
+ return fallback;
}
function normalizeError(error: unknown): ApiClientError {
- if (error instanceof ApiClientError) return error
+ if (error instanceof ApiClientError) return error;
if (axios.isAxiosError(error)) {
- const status = error.response?.status
- const details = error.response?.data
+ const status = error.response?.status;
+ const details = error.response?.data;
- return new ApiClientError(
- extractMessage(details, error.message || 'Request failed'),
- {
- status,
- details,
- causeError: error,
- },
- )
+ return new ApiClientError(extractMessage(details, error.message || 'Request failed'), {
+ status,
+ details,
+ causeError: error,
+ });
}
if (error instanceof Error) {
return new ApiClientError(error.message, {
causeError: error,
- })
+ });
}
return new ApiClientError('Error inesperado.', {
causeError: error,
- })
+ });
}
http.interceptors.request.use((config) => {
- if (!accessToken) return config
+ if (!accessToken) return config;
- config.headers = config.headers ?? {}
- config.headers.Authorization = `Bearer ${accessToken}`
- return config
-})
+ config.headers = config.headers ?? {};
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ return config;
+});
http.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
- const normalized = normalizeError(error)
+ const normalized = normalizeError(error);
if (normalized.status === 403 && handlers.onForbidden) {
- await handlers.onForbidden(normalized)
+ await handlers.onForbidden(normalized);
}
if (handlers.onError) {
- await handlers.onError(normalized)
+ await handlers.onError(normalized);
}
- return Promise.reject(normalized)
- },
-)
+ return Promise.reject(normalized);
+ }
+);
export const apiClient = {
onboarding: {
start: async (payload: OnboardingStartInput) => {
- const response = await http.post(
- '/api/onboarding/start',
- payload,
- )
- return response.data
+ const response = await http.post('/api/onboarding/start', payload);
+ return response.data;
},
verifyOtp: async (payload: OnboardingVerifyOtpInput) => {
const response = await http.post(
'/api/onboarding/verify-otp',
- payload,
- )
- return response.data
+ payload
+ );
+ return response.data;
},
resendOtp: async (payload: OnboardingResendOtpInput) => {
const response = await http.post(
'/api/onboarding/resend-otp',
- payload,
- )
- return response.data
+ payload
+ );
+ return response.data;
},
complete: async (payload: OnboardingCompleteInput) => {
const response = await http.post(
'/api/onboarding/complete',
- payload,
- )
- return response.data
+ payload
+ );
+ return response.data;
},
},
plans: {
list: async () => {
- const response = await http.get('/api/plans')
- return response.data
+ const response = await http.get('/api/plans');
+ return response.data;
},
},
user: {
getProfile: async () => {
- const response = await http.get('/api/user/profile')
- return response.data
+ const response = await http.get('/api/user/profile');
+ return response.data;
},
},
complexes: {
create: async (payload: CreateComplexPayload) => {
- const response = await http.post(
- '/api/complexes',
- payload,
- )
- return response.data
+ const response = await http.post('/api/complexes', payload);
+ return response.data;
},
getById: async (id: string) => {
- const response = await http.get(`/api/complexes/${id}`)
- return response.data
+ const response = await http.get(`/api/complexes/${id}`);
+ return response.data;
},
getBySlug: async (slug: string) => {
- const response = await http.get(`/api/complexes/slug/${slug}`)
- return response.data
+ const response = await http.get(`/api/complexes/slug/${slug}`);
+ return response.data;
},
update: async (id: string, payload: UpdateComplexPayload) => {
- const response = await http.patch(
- `/api/complexes/${id}`,
- payload,
- )
- return response.data
+ const response = await http.patch(`/api/complexes/${id}`, payload);
+ return response.data;
},
listMine: async () => {
- const response = await http.get('/api/complexes/mine')
- return response.data
+ const response = await http.get('/api/complexes/mine');
+ return response.data;
},
},
sports: {
list: async () => {
- const response = await http.get('/api/sports')
- return response.data
+ const response = await http.get('/api/sports');
+ return response.data;
},
create: async (payload: CreateSportInput) => {
- const response = await http.post('/api/sports', payload)
- return response.data
+ const response = await http.post('/api/sports', payload);
+ return response.data;
},
update: async (id: string, payload: UpdateSportInput) => {
- const response = await http.patch(`/api/sports/${id}`, payload)
- return response.data
+ const response = await http.patch(`/api/sports/${id}`, payload);
+ return response.data;
},
},
courts: {
listByComplex: async (complexId: string) => {
- const response = await http.get(`/api/courts/complex/${complexId}`)
- return response.data
+ const response = await http.get(`/api/courts/complex/${complexId}`);
+ return response.data;
},
create: async (complexId: string, payload: CreateCourtInput) => {
- const response = await http.post(
- `/api/courts/complex/${complexId}`,
- payload,
- )
- return response.data
+ const response = await http.post(`/api/courts/complex/${complexId}`, payload);
+ return response.data;
},
update: async (id: string, payload: UpdateCourtInput) => {
- const response = await http.patch(`/api/courts/${id}`, payload)
- return response.data
+ const response = await http.patch(`/api/courts/${id}`, payload);
+ return response.data;
},
},
publicBookings: {
getAvailability: async (
complexSlug: string,
params: {
- date: string
- sportId?: string
- },
+ date: string;
+ sportId?: string;
+ }
) => {
const response = await http.get(
`/api/public-bookings/complex/${complexSlug}/availability`,
{
params,
- },
- )
- return response.data
+ }
+ );
+ return response.data;
},
create: async (complexSlug: string, payload: CreatePublicBookingInput) => {
const response = await http.post(
`/api/public-bookings/complex/${complexSlug}`,
- payload,
- )
- return response.data
+ payload
+ );
+ return response.data;
},
getConfirmation: async (complexSlug: string, bookingCode: string) => {
const response = await http.get(
- `/api/public-bookings/complex/${complexSlug}/confirmation/${bookingCode}`,
- )
- return response.data
+ `/api/public-bookings/complex/${complexSlug}/confirmation/${bookingCode}`
+ );
+ return response.data;
},
},
adminBookings: {
listByComplex: async (
complexId: string,
params: {
- fromDate: string
- },
+ fromDate: string;
+ }
) => {
const response = await http.get<{ bookings: AdminBooking[] }>(
`/api/admin-bookings/complex/${complexId}`,
{
params,
- },
- )
- return response.data
+ }
+ );
+ return response.data;
},
create: async (complexId: string, payload: CreateAdminBookingInput) => {
const response = await http.post(
`/api/admin-bookings/complex/${complexId}`,
- payload,
- )
- return response.data
+ payload
+ );
+ return response.data;
},
- updateStatus: async (
- bookingId: string,
- payload: UpdateAdminBookingStatusInput,
- ) => {
+ updateStatus: async (bookingId: string, payload: UpdateAdminBookingStatusInput) => {
const response = await http.patch(
`/api/admin-bookings/${bookingId}/status`,
- payload,
- )
- return response.data
+ payload
+ );
+ return response.data;
},
},
-}
+};
-export type ApiClient = typeof apiClient
+export type ApiClient = typeof apiClient;
export function configureApiClient(nextHandlers: ErrorHandlers) {
- handlers.onForbidden = nextHandlers.onForbidden
- handlers.onError = nextHandlers.onError
+ handlers.onForbidden = nextHandlers.onForbidden;
+ handlers.onError = nextHandlers.onError;
}
export function setApiAccessToken(token: string | null) {
- accessToken = token
+ accessToken = token;
}
diff --git a/apps/frontend/src/lib/auth.tsx b/apps/frontend/src/lib/auth.tsx
index 4f7fd15..e4a20c5 100644
--- a/apps/frontend/src/lib/auth.tsx
+++ b/apps/frontend/src/lib/auth.tsx
@@ -1,119 +1,117 @@
+import { setApiAccessToken } from '@/lib/api-client';
+import { supabase } from '@/lib/supabase';
+import type { Session, User } from '@supabase/supabase-js';
import {
+ type PropsWithChildren,
createContext,
useContext,
useEffect,
useMemo,
useState,
- type PropsWithChildren,
-} from 'react'
-import type { Session, User } from '@supabase/supabase-js'
-import { setApiAccessToken } from '@/lib/api-client'
-import { supabase } from '@/lib/supabase'
+} from 'react';
type SignInParams = {
- email: string
- password: string
-}
+ email: string;
+ password: string;
+};
type SignUpParams = {
- email: string
- password: string
- fullName: string
-}
+ email: string;
+ password: string;
+ fullName: string;
+};
type UpdateProfileParams = {
- fullName: string
-}
+ fullName: string;
+};
type UpdatePasswordParams = {
- password: string
-}
+ password: string;
+};
export type AuthContextValue = {
- user: User | null
- session: Session | null
- loading: boolean
- isAuthenticated: boolean
- displayName: string
- initials: string
- avatarUrl: string | null
- signInWithPassword: (params: SignInParams) => Promise
- signUp: (params: SignUpParams) => Promise<{ requiresEmailConfirmation: boolean }>
- updateProfile: (params: UpdateProfileParams) => Promise
- updatePassword: (params: UpdatePasswordParams) => Promise
- signOut: () => Promise
-}
+ user: User | null;
+ session: Session | null;
+ loading: boolean;
+ isAuthenticated: boolean;
+ displayName: string;
+ initials: string;
+ avatarUrl: string | null;
+ signInWithPassword: (params: SignInParams) => Promise;
+ signUp: (params: SignUpParams) => Promise<{ requiresEmailConfirmation: boolean }>;
+ updateProfile: (params: UpdateProfileParams) => Promise;
+ updatePassword: (params: UpdatePasswordParams) => Promise;
+ signOut: () => Promise;
+};
-const AuthContext = createContext(null)
+const AuthContext = createContext(null);
function getDisplayName(user: User | null): string {
- if (!user) return 'Usuario'
+ if (!user) return 'Usuario';
const fromMetadata =
- user.user_metadata?.name ??
- user.user_metadata?.full_name ??
- user.user_metadata?.user_name
+ user.user_metadata?.name ?? user.user_metadata?.full_name ?? user.user_metadata?.user_name;
if (typeof fromMetadata === 'string' && fromMetadata.trim().length > 0) {
- return fromMetadata.trim()
+ return fromMetadata.trim();
}
- return user.email ?? 'Usuario'
+ return user.email ?? 'Usuario';
}
function getInitials(name: string): string {
- const words = name.trim().split(/\s+/).slice(0, 2)
- const initials = words.map((word) => word[0]?.toUpperCase() ?? '').join('')
- return initials || 'U'
+ const words = name.trim().split(/\s+/).slice(0, 2);
+ const initials = words.map((word) => word[0]?.toUpperCase() ?? '').join('');
+ return initials || 'U';
}
function getAvatarUrl(user: User | null): string | null {
- if (!user) return null
+ if (!user) return null;
- const value = user.user_metadata?.avatar_url ?? user.user_metadata?.picture
- return typeof value === 'string' && value.length > 0 ? value : null
+ const value = user.user_metadata?.avatar_url ?? user.user_metadata?.picture;
+ return typeof value === 'string' && value.length > 0 ? value : null;
}
export function AuthProvider({ children }: PropsWithChildren) {
- const [session, setSession] = useState(null)
- const [user, setUser] = useState(null)
- const [loading, setLoading] = useState(true)
+ const [session, setSession] = useState(null);
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
useEffect(() => {
- let mounted = true
+ let mounted = true;
const init = async () => {
- const { data } = await supabase.auth.getSession()
+ const { data } = await supabase.auth.getSession();
- if (!mounted) return
+ if (!mounted) return;
- setSession(data.session)
- setUser(data.session?.user ?? null)
- setApiAccessToken(data.session?.access_token ?? null)
- setLoading(false)
- }
+ setSession(data.session);
+ setUser(data.session?.user ?? null);
+ setApiAccessToken(data.session?.access_token ?? null);
+ setLoading(false);
+ };
- void init()
+ void init();
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, nextSession) => {
- setSession(nextSession)
- setUser(nextSession?.user ?? null)
- setApiAccessToken(nextSession?.access_token ?? null)
- setLoading(false)
- })
+ setSession(nextSession);
+ setUser(nextSession?.user ?? null);
+ setApiAccessToken(nextSession?.access_token ?? null);
+ setLoading(false);
+ });
return () => {
- mounted = false
- subscription.unsubscribe()
- }
- }, [])
+ mounted = false;
+ subscription.unsubscribe();
+ };
+ }, []);
const value = useMemo(() => {
- const displayName = getDisplayName(user)
- const initials = getInitials(displayName)
- const avatarUrl = getAvatarUrl(user)
+ const displayName = getDisplayName(user);
+ const initials = getInitials(displayName);
+ const avatarUrl = getAvatarUrl(user);
return {
user,
@@ -127,10 +125,10 @@ export function AuthProvider({ children }: PropsWithChildren) {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
- })
+ });
if (error) {
- throw error
+ throw error;
}
},
signUp: async ({ email, password, fullName }) => {
@@ -143,15 +141,15 @@ export function AuthProvider({ children }: PropsWithChildren) {
name: fullName,
},
},
- })
+ });
if (error) {
- throw error
+ throw error;
}
return {
requiresEmailConfirmation: !data.session,
- }
+ };
},
updateProfile: async ({ fullName }) => {
const { error } = await supabase.auth.updateUser({
@@ -159,39 +157,39 @@ export function AuthProvider({ children }: PropsWithChildren) {
full_name: fullName,
name: fullName,
},
- })
+ });
if (error) {
- throw error
+ throw error;
}
},
updatePassword: async ({ password }) => {
const { error } = await supabase.auth.updateUser({
password,
- })
+ });
if (error) {
- throw error
+ throw error;
}
},
signOut: async () => {
- const { error } = await supabase.auth.signOut()
+ const { error } = await supabase.auth.signOut();
if (error) {
- throw error
+ throw error;
}
},
- }
- }, [loading, session, user])
+ };
+ }, [loading, session, user]);
- return {children}
+ return {children} ;
}
export function useAuth() {
- const context = useContext(AuthContext)
+ const context = useContext(AuthContext);
if (!context) {
- throw new Error('useAuth must be used within AuthProvider')
+ throw new Error('useAuth must be used within AuthProvider');
}
- return context
+ return context;
}
diff --git a/apps/frontend/src/lib/current-complex.ts b/apps/frontend/src/lib/current-complex.ts
index 498cc57..44af08a 100644
--- a/apps/frontend/src/lib/current-complex.ts
+++ b/apps/frontend/src/lib/current-complex.ts
@@ -1,11 +1,11 @@
-const CURRENT_COMPLEX_SLUG_KEY = 'current-complex-slug'
+const CURRENT_COMPLEX_SLUG_KEY = 'current-complex-slug';
export function getCurrentComplexSlug(): string | null {
- if (typeof window === 'undefined') return null
- return window.localStorage.getItem(CURRENT_COMPLEX_SLUG_KEY)
+ if (typeof window === 'undefined') return null;
+ return window.localStorage.getItem(CURRENT_COMPLEX_SLUG_KEY);
}
export function setCurrentComplexSlug(complexSlug: string) {
- if (typeof window === 'undefined') return
- window.localStorage.setItem(CURRENT_COMPLEX_SLUG_KEY, complexSlug)
+ if (typeof window === 'undefined') return;
+ window.localStorage.setItem(CURRENT_COMPLEX_SLUG_KEY, complexSlug);
}
diff --git a/apps/frontend/src/lib/query-client.ts b/apps/frontend/src/lib/query-client.ts
index 62e9e4f..a4087d3 100644
--- a/apps/frontend/src/lib/query-client.ts
+++ b/apps/frontend/src/lib/query-client.ts
@@ -1,4 +1,4 @@
-import { QueryClient } from '@tanstack/react-query'
+import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
@@ -8,4 +8,4 @@ export const queryClient = new QueryClient({
retry: 1,
},
},
-})
+});
diff --git a/apps/frontend/src/lib/supabase.ts b/apps/frontend/src/lib/supabase.ts
index e4284c2..2e30267 100644
--- a/apps/frontend/src/lib/supabase.ts
+++ b/apps/frontend/src/lib/supabase.ts
@@ -1,12 +1,12 @@
-import { createClient } from '@supabase/supabase-js'
+import { createClient } from '@supabase/supabase-js';
-const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
-const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
- 'Missing Supabase environment variables. Define VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.',
- )
+ 'Missing Supabase environment variables. Define VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.'
+ );
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
@@ -15,4 +15,4 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
persistSession: true,
detectSessionInUrl: true,
},
-})
+});
diff --git a/apps/frontend/src/lib/theme.tsx b/apps/frontend/src/lib/theme.tsx
index bd875a9..8b39396 100644
--- a/apps/frontend/src/lib/theme.tsx
+++ b/apps/frontend/src/lib/theme.tsx
@@ -1,68 +1,63 @@
import {
+ type PropsWithChildren,
createContext,
useContext,
useEffect,
useMemo,
useState,
- type PropsWithChildren,
-} from 'react'
+} from 'react';
-type Theme = 'light' | 'dark' | 'system'
-type ResolvedTheme = 'light' | 'dark'
+type Theme = 'light' | 'dark' | 'system';
+type ResolvedTheme = 'light' | 'dark';
type ThemeContextValue = {
- theme: Theme
- resolvedTheme: ResolvedTheme
- setTheme: (theme: Theme) => void
-}
+ theme: Theme;
+ resolvedTheme: ResolvedTheme;
+ setTheme: (theme: Theme) => void;
+};
-const THEME_STORAGE_KEY = 'theme'
-const ThemeContext = createContext(null)
+const THEME_STORAGE_KEY = 'theme';
+const ThemeContext = createContext(null);
function getSystemTheme(): ResolvedTheme {
- return window.matchMedia('(prefers-color-scheme: dark)').matches
- ? 'dark'
- : 'light'
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function applyResolvedTheme(theme: ResolvedTheme) {
- document.documentElement.classList.toggle('dark', theme === 'dark')
+ document.documentElement.classList.toggle('dark', theme === 'dark');
}
export function ThemeProvider({ children }: PropsWithChildren) {
- const [theme, setThemeState] = useState('system')
- const [resolvedTheme, setResolvedTheme] = useState('light')
+ const [theme, setThemeState] = useState('system');
+ const [resolvedTheme, setResolvedTheme] = useState('light');
useEffect(() => {
- const saved = localStorage.getItem(THEME_STORAGE_KEY)
+ const saved = localStorage.getItem(THEME_STORAGE_KEY);
const initialTheme: Theme =
- saved === 'light' || saved === 'dark' || saved === 'system'
- ? saved
- : 'system'
+ saved === 'light' || saved === 'dark' || saved === 'system' ? saved : 'system';
- setThemeState(initialTheme)
- }, [])
+ setThemeState(initialTheme);
+ }, []);
useEffect(() => {
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateTheme = () => {
- const nextResolvedTheme =
- theme === 'system' ? getSystemTheme() : theme
- setResolvedTheme(nextResolvedTheme)
- applyResolvedTheme(nextResolvedTheme)
- }
+ const nextResolvedTheme = theme === 'system' ? getSystemTheme() : theme;
+ setResolvedTheme(nextResolvedTheme);
+ applyResolvedTheme(nextResolvedTheme);
+ };
- updateTheme()
+ updateTheme();
- mediaQuery.addEventListener('change', updateTheme)
- return () => mediaQuery.removeEventListener('change', updateTheme)
- }, [theme])
+ mediaQuery.addEventListener('change', updateTheme);
+ return () => mediaQuery.removeEventListener('change', updateTheme);
+ }, [theme]);
const setTheme = (nextTheme: Theme) => {
- setThemeState(nextTheme)
- localStorage.setItem(THEME_STORAGE_KEY, nextTheme)
- }
+ setThemeState(nextTheme);
+ localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
+ };
const value = useMemo(
() => ({
@@ -70,20 +65,20 @@ export function ThemeProvider({ children }: PropsWithChildren) {
resolvedTheme,
setTheme,
}),
- [theme, resolvedTheme],
- )
+ [theme, resolvedTheme]
+ );
- return {children}
+ return {children} ;
}
export function useTheme() {
- const context = useContext(ThemeContext)
+ const context = useContext(ThemeContext);
if (!context) {
- throw new Error('useTheme must be used within ThemeProvider')
+ throw new Error('useTheme must be used within ThemeProvider');
}
- return context
+ return context;
}
-export type { Theme }
+export type { Theme };
diff --git a/apps/frontend/src/lib/utils.ts b/apps/frontend/src/lib/utils.ts
index bd0c391..9ad0df4 100644
--- a/apps/frontend/src/lib/utils.ts
+++ b/apps/frontend/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}
diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx
index d8b5912..3fe9581 100644
--- a/apps/frontend/src/main.tsx
+++ b/apps/frontend/src/main.tsx
@@ -1,45 +1,45 @@
-import { StrictMode, useEffect } from 'react'
-import { createRoot } from 'react-dom/client'
-import { QueryClientProvider } from '@tanstack/react-query'
-import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
-import { RouterProvider } from '@tanstack/react-router'
-import { TanStackRouterDevtools } from '@tanstack/router-devtools'
-import { AuthProvider, useAuth } from '@/lib/auth'
-import { apiClient, configureApiClient } from '@/lib/api-client'
-import { queryClient } from '@/lib/query-client'
-import { ThemeProvider } from '@/lib/theme'
-import { router } from './router'
-import './index.css'
+import { apiClient, configureApiClient } from '@/lib/api-client';
+import { AuthProvider, useAuth } from '@/lib/auth';
+import { queryClient } from '@/lib/query-client';
+import { ThemeProvider } from '@/lib/theme';
+import { QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { RouterProvider } from '@tanstack/react-router';
+import { TanStackRouterDevtools } from '@tanstack/router-devtools';
+import { StrictMode, useEffect } from 'react';
+import { createRoot } from 'react-dom/client';
+import { router } from './router';
+import './index.css';
function AppRouter() {
- const auth = useAuth()
+ const auth = useAuth();
useEffect(() => {
- router.invalidate()
- }, [auth.isAuthenticated, auth.loading, auth.user])
+ router.invalidate();
+ }, [auth.isAuthenticated, auth.loading, auth.user]);
useEffect(() => {
configureApiClient({
onForbidden: async () => {
if (!auth.isAuthenticated) {
- return
+ return;
}
- await auth.signOut()
- await router.navigate({ to: '/login' })
+ await auth.signOut();
+ await router.navigate({ to: '/login' });
},
onError: (error) => {
- console.error('[API]', error)
+ console.error('[API]', error);
},
- })
- }, [auth])
+ });
+ }, [auth]);
if (auth.loading) {
return (
Validando sesión...
- )
+ );
}
return (
@@ -47,7 +47,7 @@ function AppRouter() {
{import.meta.env.DEV && }
>
- )
+ );
}
createRoot(document.getElementById('root')!).render(
@@ -60,5 +60,5 @@ createRoot(document.getElementById('root')!).render(
- ,
-)
+
+);
diff --git a/apps/frontend/src/router.tsx b/apps/frontend/src/router.tsx
index 0a66179..a1f65fb 100644
--- a/apps/frontend/src/router.tsx
+++ b/apps/frontend/src/router.tsx
@@ -1,6 +1,6 @@
-import { createRouter } from '@tanstack/react-router'
-import { apiClient } from '@/lib/api-client'
-import { routeTree } from './routeTree.gen'
+import { apiClient } from '@/lib/api-client';
+import { createRouter } from '@tanstack/react-router';
+import { routeTree } from './routeTree.gen';
export const router = createRouter({
routeTree,
@@ -8,10 +8,10 @@ export const router = createRouter({
auth: undefined!,
api: apiClient,
},
-})
+});
declare module '@tanstack/react-router' {
interface Register {
- router: typeof router
+ router: typeof router;
}
}
diff --git a/apps/frontend/src/routes/$complexSlug/booking.tsx b/apps/frontend/src/routes/$complexSlug/booking.tsx
index 6aa05b7..89ac185 100644
--- a/apps/frontend/src/routes/$complexSlug/booking.tsx
+++ b/apps/frontend/src/routes/$complexSlug/booking.tsx
@@ -1,5 +1,5 @@
-import { Outlet, createFileRoute } from '@tanstack/react-router'
+import { Outlet, createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/$complexSlug/booking')({
component: Outlet,
-})
+});
diff --git a/apps/frontend/src/routes/$complexSlug/booking/confirmed/$bookingCode.tsx b/apps/frontend/src/routes/$complexSlug/booking/confirmed/$bookingCode.tsx
index 0ccaee7..9371023 100644
--- a/apps/frontend/src/routes/$complexSlug/booking/confirmed/$bookingCode.tsx
+++ b/apps/frontend/src/routes/$complexSlug/booking/confirmed/$bookingCode.tsx
@@ -1,17 +1,12 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { PublicBookingConfirmationPage } from '@/features/public-booking/public-booking-confirmation-page'
+import { PublicBookingConfirmationPage } from '@/features/public-booking/public-booking-confirmation-page';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/$complexSlug/booking/confirmed/$bookingCode')({
component: PublicBookingConfirmationRouteComponent,
-})
+});
function PublicBookingConfirmationRouteComponent() {
- const { complexSlug, bookingCode } = Route.useParams()
+ const { complexSlug, bookingCode } = Route.useParams();
- return (
-
- )
+ return ;
}
diff --git a/apps/frontend/src/routes/$complexSlug/booking/index.tsx b/apps/frontend/src/routes/$complexSlug/booking/index.tsx
index 02aa597..9d080a1 100644
--- a/apps/frontend/src/routes/$complexSlug/booking/index.tsx
+++ b/apps/frontend/src/routes/$complexSlug/booking/index.tsx
@@ -1,12 +1,12 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { PublicBookingPage } from '@/features/public-booking/public-booking-page'
+import { PublicBookingPage } from '@/features/public-booking/public-booking-page';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/$complexSlug/booking/')({
component: PublicBookingIndexRouteComponent,
-})
+});
function PublicBookingIndexRouteComponent() {
- const { complexSlug } = Route.useParams()
+ const { complexSlug } = Route.useParams();
- return
+ return ;
}
diff --git a/apps/frontend/src/routes/__root.tsx b/apps/frontend/src/routes/__root.tsx
index c18de02..986e608 100644
--- a/apps/frontend/src/routes/__root.tsx
+++ b/apps/frontend/src/routes/__root.tsx
@@ -1,16 +1,16 @@
-import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
-import type { AuthContextValue } from '@/lib/auth'
-import type { ApiClient } from '@/lib/api-client'
-import { NotFoundPage } from '@/features/not-found/not-found-page'
-import { ServerErrorPage } from '@/features/server-error/server-error-page'
+import { NotFoundPage } from '@/features/not-found/not-found-page';
+import { ServerErrorPage } from '@/features/server-error/server-error-page';
+import type { ApiClient } from '@/lib/api-client';
+import type { AuthContextValue } from '@/lib/auth';
+import { Outlet, createRootRouteWithContext } from '@tanstack/react-router';
type RouterContext = {
- auth: AuthContextValue
- api: ApiClient
-}
+ auth: AuthContextValue;
+ api: ApiClient;
+};
export const Route = createRootRouteWithContext()({
component: Outlet,
errorComponent: ServerErrorPage,
notFoundComponent: NotFoundPage,
-})
+});
diff --git a/apps/frontend/src/routes/_app/_authenticated/complex/$slug/edit.tsx b/apps/frontend/src/routes/_app/_authenticated/complex/$slug/edit.tsx
index a8b9379..b0705dd 100644
--- a/apps/frontend/src/routes/_app/_authenticated/complex/$slug/edit.tsx
+++ b/apps/frontend/src/routes/_app/_authenticated/complex/$slug/edit.tsx
@@ -1,11 +1,11 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { ComplexSettingsPage } from '@/features/complex/complex-settings-page'
+import { ComplexSettingsPage } from '@/features/complex/complex-settings-page';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated/complex/$slug/edit')({
component: RouteComponent,
-})
+});
function RouteComponent() {
- const { slug } = Route.useParams()
- return
+ const { slug } = Route.useParams();
+ return ;
}
diff --git a/apps/frontend/src/routes/_app/_authenticated/index.tsx b/apps/frontend/src/routes/_app/_authenticated/index.tsx
index 66fe397..54ffea6 100644
--- a/apps/frontend/src/routes/_app/_authenticated/index.tsx
+++ b/apps/frontend/src/routes/_app/_authenticated/index.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { HomePage } from '@/features/home/home-page'
+import { HomePage } from '@/features/home/home-page';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated/')({
component: HomePage,
-})
+});
diff --git a/apps/frontend/src/routes/_app/_authenticated/route.tsx b/apps/frontend/src/routes/_app/_authenticated/route.tsx
index e1856f5..4c781d2 100644
--- a/apps/frontend/src/routes/_app/_authenticated/route.tsx
+++ b/apps/frontend/src/routes/_app/_authenticated/route.tsx
@@ -1,4 +1,4 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
+import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/_authenticated')({
beforeLoad: ({ context, location }) => {
@@ -6,7 +6,7 @@ export const Route = createFileRoute('/_app/_authenticated')({
throw redirect({
to: '/login',
search: { redirect: location.href },
- })
+ });
}
},
-})
+});
diff --git a/apps/frontend/src/routes/_app/about.tsx b/apps/frontend/src/routes/_app/about.tsx
index 5bd878b..01fa3cf 100644
--- a/apps/frontend/src/routes/_app/about.tsx
+++ b/apps/frontend/src/routes/_app/about.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { AboutPage } from '@/features/about/about-page'
+import { AboutPage } from '@/features/about/about-page';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/about')({
component: AboutPage,
-})
+});
diff --git a/apps/frontend/src/routes/_app/profile.tsx b/apps/frontend/src/routes/_app/profile.tsx
index 629cf93..b6ad326 100644
--- a/apps/frontend/src/routes/_app/profile.tsx
+++ b/apps/frontend/src/routes/_app/profile.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { ProfilePage } from '@/features/profile/profile-page'
+import { ProfilePage } from '@/features/profile/profile-page';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app/profile')({
component: ProfilePage,
-})
+});
diff --git a/apps/frontend/src/routes/_app/route.tsx b/apps/frontend/src/routes/_app/route.tsx
index 157546e..ed8b452 100644
--- a/apps/frontend/src/routes/_app/route.tsx
+++ b/apps/frontend/src/routes/_app/route.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { RootLayout } from '@/features/layout/root-layout'
+import { RootLayout } from '@/features/layout/root-layout';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_app')({
component: RootLayout,
-})
+});
diff --git a/apps/frontend/src/routes/login.tsx b/apps/frontend/src/routes/login.tsx
index ee656f7..45ee78d 100644
--- a/apps/frontend/src/routes/login.tsx
+++ b/apps/frontend/src/routes/login.tsx
@@ -1,22 +1,22 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { z } from 'zod'
-import { LoginPage } from '@/features/login/login-page'
+import { LoginPage } from '@/features/login/login-page';
+import { createFileRoute, redirect } from '@tanstack/react-router';
+import { z } from 'zod';
const loginSearchSchema = z.object({
redirect: z.string().optional(),
-})
+});
export const Route = createFileRoute('/login')({
validateSearch: loginSearchSchema,
beforeLoad: ({ context }) => {
if (context.auth.isAuthenticated) {
- throw redirect({ to: '/' })
+ throw redirect({ to: '/' });
}
},
component: LoginRouteComponent,
-})
+});
function LoginRouteComponent() {
- const search = Route.useSearch()
- return
+ const search = Route.useSearch();
+ return ;
}
diff --git a/apps/frontend/src/routes/onboard.tsx b/apps/frontend/src/routes/onboard.tsx
index 1c46bcd..5897863 100644
--- a/apps/frontend/src/routes/onboard.tsx
+++ b/apps/frontend/src/routes/onboard.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute } from '@tanstack/react-router'
-import { OnboardPage } from '@/features/onboard/onboard-page'
+import { OnboardPage } from '@/features/onboard/onboard-page';
+import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/onboard')({
component: OnboardPage,
-})
+});
diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json
index aa3c04f..7036885 100644
--- a/apps/frontend/tsconfig.json
+++ b/apps/frontend/tsconfig.json
@@ -6,8 +6,5 @@
}
},
"files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}
diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts
index 6489823..8a2cda7 100644
--- a/apps/frontend/vite.config.ts
+++ b/apps/frontend/vite.config.ts
@@ -1,8 +1,8 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
-import tailwindcss from '@tailwindcss/vite'
-import path from 'node:path'
+import path from 'node:path';
+import tailwindcss from '@tailwindcss/vite';
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
// https://vite.dev/config/
export default defineConfig({
@@ -17,29 +17,26 @@ export default defineConfig({
output: {
manualChunks(id) {
if (id.includes('node_modules/react') || id.includes('node_modules/react-dom')) {
- return 'react-vendor'
+ return 'react-vendor';
}
if (id.includes('node_modules/@tanstack/react-router')) {
- return 'router-vendor'
+ return 'router-vendor';
}
if (id.includes('node_modules/@supabase/supabase-js')) {
- return 'supabase-vendor'
+ return 'supabase-vendor';
}
- if (
- id.includes('node_modules/radix-ui') ||
- id.includes('node_modules/lucide-react')
- ) {
- return 'ui-vendor'
+ if (id.includes('node_modules/radix-ui') || id.includes('node_modules/lucide-react')) {
+ return 'ui-vendor';
}
if (
id.includes('node_modules/react-hook-form') ||
id.includes('node_modules/@hookform/resolvers') ||
id.includes('node_modules/zod')
) {
- return 'form-vendor'
+ return 'form-vendor';
}
},
},
},
},
-})
+});
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..4e15c40
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,66 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "correctness": {
+ "noUnusedVariables": "warn",
+ "useExhaustiveDependencies": "off"
+ },
+ "suspicious": {
+ "noExplicitAny": "off",
+ "noArrayIndexKey": "off",
+ "noMisleadingCharacterClass": "off"
+ },
+ "style": {
+ "useImportType": "off",
+ "noNonNullAssertion": "off"
+ },
+ "a11y": {
+ "useFocusableInteractive": "off",
+ "useSemanticElements": "off"
+ }
+ }
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 100
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "semicolons": "always",
+ "trailingCommas": "es5"
+ }
+ },
+ "json": {
+ "formatter": {
+ "indentStyle": "space",
+ "indentWidth": 2
+ }
+ },
+ "files": {
+ "ignore": [
+ "node_modules",
+ "dist",
+ "build",
+ "generated",
+ ".next",
+ "coverage",
+ "**/tsconfig*.json",
+ "**/tsconfig/**/*.json",
+ "routeTree.gen.ts"
+ ]
+ }
+}
diff --git a/package.json b/package.json
index 7ea589f..d9e361b 100644
--- a/package.json
+++ b/package.json
@@ -9,12 +9,20 @@
"zod": "^4.3.6"
},
"scripts": {
+ "dev": "concurrently -n backend,frontend -c blue,green \"bun run dev:backend\" \"bun run dev:frontend\"",
"dev:frontend": "bun --filter frontend dev",
"dev:backend": "bun --filter backend dev",
"build:frontend": "bun --filter frontend build",
- "lint:frontend": "bun --filter frontend lint"
+ "lint": "bun run lint:frontend && bun run lint:backend",
+ "lint:fix": "bun run lint:frontend:fix && bun run lint:backend:fix",
+ "lint:frontend": "bun --filter frontend lint",
+ "lint:frontend:fix": "bun --filter frontend lint:fix",
+ "lint:backend": "bun --filter backend lint",
+ "lint:backend:fix": "bun --filter backend lint:fix"
},
"devDependencies": {
+ "@biomejs/biome": "^1.9.4",
+ "concurrently": "^9.1.2",
"typescript": "^6.0.2"
}
}