Initial commit

This commit is contained in:
Jose Selesan
2026-04-08 22:53:11 -03:00
commit 9ae270609d
179 changed files with 28096 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
import axios, { AxiosError } from 'axios'
import type {
Court,
CreateCourtInput,
CreatePublicBookingInput,
Complex,
CreateComplexPayload,
CreateComplexResponse,
CreateSportInput,
OnboardingCompleteInput,
OnboardingCompleteResponse,
OnboardingResendOtpInput,
OnboardingResendOtpResponse,
OnboardingStartInput,
OnboardingStartResponse,
OnboardingVerifyOtpInput,
OnboardingVerifyOtpResponse,
PlanSummary,
PublicAvailabilityResponse,
PublicBooking,
PublicBookingConfirmation,
Sport,
UpdateCourtInput,
UpdateComplexPayload,
UpdateComplexResponse,
UpdateSportInput,
UserProfileResponse,
} from '@repo/api-contract'
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'
let accessToken: string | null = null
export class ApiClientError extends Error {
status?: number
details?: unknown
causeError?: unknown
constructor(
message: string,
options?: { status?: number; details?: unknown; causeError?: unknown },
) {
super(message)
this.name = 'ApiClientError'
this.status = options?.status
this.details = options?.details
this.causeError = options?.causeError
}
}
type ErrorHandlers = {
onForbidden?: (error: ApiClientError) => void | Promise<void>
onError?: (error: ApiClientError) => void | Promise<void>
}
const handlers: ErrorHandlers = {}
const http = axios.create({
baseURL: apiBaseUrl,
headers: {
'Content-Type': 'application/json',
},
})
function extractMessage(details: unknown, fallback: string) {
if (
typeof details === 'object' &&
details &&
'message' in details &&
typeof details.message === 'string'
) {
return details.message
}
return fallback
}
function normalizeError(error: unknown): ApiClientError {
if (error instanceof ApiClientError) return error
if (axios.isAxiosError(error)) {
const status = error.response?.status
const details = error.response?.data
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
config.headers = config.headers ?? {}
config.headers.Authorization = `Bearer ${accessToken}`
return config
})
http.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const normalized = normalizeError(error)
if (normalized.status === 403 && handlers.onForbidden) {
await handlers.onForbidden(normalized)
}
if (handlers.onError) {
await handlers.onError(normalized)
}
return Promise.reject(normalized)
},
)
export const apiClient = {
onboarding: {
start: async (payload: OnboardingStartInput) => {
const response = await http.post<OnboardingStartResponse>(
'/api/onboarding/start',
payload,
)
return response.data
},
verifyOtp: async (payload: OnboardingVerifyOtpInput) => {
const response = await http.post<OnboardingVerifyOtpResponse>(
'/api/onboarding/verify-otp',
payload,
)
return response.data
},
resendOtp: async (payload: OnboardingResendOtpInput) => {
const response = await http.post<OnboardingResendOtpResponse>(
'/api/onboarding/resend-otp',
payload,
)
return response.data
},
complete: async (payload: OnboardingCompleteInput) => {
const response = await http.post<OnboardingCompleteResponse>(
'/api/onboarding/complete',
payload,
)
return response.data
},
},
plans: {
list: async () => {
const response = await http.get<PlanSummary[]>('/api/plans')
return response.data
},
},
user: {
getProfile: async () => {
const response = await http.get<UserProfileResponse>('/api/user/profile')
return response.data
},
},
complexes: {
create: async (payload: CreateComplexPayload) => {
const response = await http.post<CreateComplexResponse>(
'/api/complexes',
payload,
)
return response.data
},
getById: async (id: string) => {
const response = await http.get<Complex>(`/api/complexes/${id}`)
return response.data
},
getBySlug: async (slug: string) => {
const response = await http.get<Complex>(`/api/complexes/slug/${slug}`)
return response.data
},
update: async (id: string, payload: UpdateComplexPayload) => {
const response = await http.patch<UpdateComplexResponse>(
`/api/complexes/${id}`,
payload,
)
return response.data
},
listMine: async () => {
const response = await http.get<Complex[]>('/api/complexes/mine')
return response.data
},
},
sports: {
list: async () => {
const response = await http.get<Sport[]>('/api/sports')
return response.data
},
create: async (payload: CreateSportInput) => {
const response = await http.post<Sport>('/api/sports', payload)
return response.data
},
update: async (id: string, payload: UpdateSportInput) => {
const response = await http.patch<Sport>(`/api/sports/${id}`, payload)
return response.data
},
},
courts: {
listByComplex: async (complexId: string) => {
const response = await http.get<Court[]>(`/api/courts/complex/${complexId}`)
return response.data
},
create: async (complexId: string, payload: CreateCourtInput) => {
const response = await http.post<Court>(
`/api/courts/complex/${complexId}`,
payload,
)
return response.data
},
update: async (id: string, payload: UpdateCourtInput) => {
const response = await http.patch<Court>(`/api/courts/${id}`, payload)
return response.data
},
},
publicBookings: {
getAvailability: async (
complexSlug: string,
params: {
date: string
sportId?: string
},
) => {
const response = await http.get<PublicAvailabilityResponse>(
`/api/public-bookings/complex/${complexSlug}/availability`,
{
params,
},
)
return response.data
},
create: async (complexSlug: string, payload: CreatePublicBookingInput) => {
const response = await http.post<PublicBooking>(
`/api/public-bookings/complex/${complexSlug}`,
payload,
)
return response.data
},
getConfirmation: async (complexSlug: string, bookingCode: string) => {
const response = await http.get<PublicBookingConfirmation>(
`/api/public-bookings/complex/${complexSlug}/confirmation/${bookingCode}`,
)
return response.data
},
},
}
export type ApiClient = typeof apiClient
export function configureApiClient(nextHandlers: ErrorHandlers) {
handlers.onForbidden = nextHandlers.onForbidden
handlers.onError = nextHandlers.onError
}
export function setApiAccessToken(token: string | null) {
accessToken = token
}