feat: basic onboarding flow\
This commit is contained in:
65
apps/web/src/lib/api.ts
Normal file
65
apps/web/src/lib/api.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
ConnectPayment,
|
||||
ConnectPaymentResult,
|
||||
CreateFirstGroup,
|
||||
CreateFirstGroupResult,
|
||||
GroupList,
|
||||
OnboardingStatusDto,
|
||||
ProblemDetails,
|
||||
} from '@gruperly/shared'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4000'
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly problem: ProblemDetails | null,
|
||||
) {
|
||||
super(problem?.title ?? `Error ${status}`)
|
||||
this.name = 'ApiError'
|
||||
}
|
||||
}
|
||||
|
||||
type JsonBody = Record<string, unknown> | unknown[]
|
||||
|
||||
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = new Headers(init?.headers)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
|
||||
const res = await fetch(`${API_URL}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
let problem: ProblemDetails | null = null
|
||||
try {
|
||||
const body = (await res.json()) as Partial<ProblemDetails>
|
||||
if (body && typeof body === 'object' && typeof body.title === 'string') {
|
||||
problem = body as ProblemDetails
|
||||
}
|
||||
} catch {
|
||||
// Sin cuerpo JSON: usamos el error genérico.
|
||||
}
|
||||
throw new ApiError(res.status, problem)
|
||||
}
|
||||
|
||||
return (await res.json()) as T
|
||||
}
|
||||
|
||||
export const getOnboardingStatus = () => apiFetch<OnboardingStatusDto>('/api/v1/onboarding/status')
|
||||
|
||||
export const connectPayment = (payload: ConnectPayment) =>
|
||||
apiFetch<ConnectPaymentResult>('/api/v1/onboarding/payment-setup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
export const createFirstGroup = (payload: CreateFirstGroup) =>
|
||||
apiFetch<CreateFirstGroupResult>('/api/v1/onboarding/first-group', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
export const getGroups = () => apiFetch<GroupList>('/api/v1/groups')
|
||||
26
apps/web/src/lib/format.ts
Normal file
26
apps/web/src/lib/format.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { BillingType, WeekDay } from '@gruperly/shared'
|
||||
|
||||
export const WEEK_DAY_FULL_LABELS: Record<WeekDay, string> = {
|
||||
MONDAY: 'Lunes',
|
||||
TUESDAY: 'Martes',
|
||||
WEDNESDAY: 'Miércoles',
|
||||
THURSDAY: 'Jueves',
|
||||
FRIDAY: 'Viernes',
|
||||
SATURDAY: 'Sábado',
|
||||
SUNDAY: 'Domingo',
|
||||
}
|
||||
|
||||
export const BILLING_LABELS: Record<BillingType, string> = {
|
||||
MONTHLY: 'Mensual',
|
||||
PER_CLASS: 'Por clase',
|
||||
}
|
||||
|
||||
export function formatSchedule(days: readonly WeekDay[], time: string | null): string {
|
||||
const dayNames = days.map((day) => WEEK_DAY_FULL_LABELS[day])
|
||||
return time ? `${dayNames.join(', ')} · ${time}` : dayNames.join(', ')
|
||||
}
|
||||
|
||||
export function formatPrice(price: number | null): string {
|
||||
if (price == null) return '—'
|
||||
return price.toLocaleString('es-MX', { style: 'currency', currency: 'MXN' })
|
||||
}
|
||||
Reference in New Issue
Block a user