refactor: modularize API client by splitting resources into individual files and standardizing HTTP request handling.
This commit is contained in:
@@ -1,41 +1,7 @@
|
||||
import type {
|
||||
AdminBooking,
|
||||
Complex,
|
||||
ComplexWithRole,
|
||||
Court,
|
||||
CreateAdminBookingInput,
|
||||
CreateComplexPayload,
|
||||
CreateComplexResponse,
|
||||
CreateCourtInput,
|
||||
CreatePublicBookingInput,
|
||||
CreateSportInput,
|
||||
OnboardingCompleteInput,
|
||||
OnboardingCompleteResponse,
|
||||
OnboardingResendOtpInput,
|
||||
OnboardingResendOtpResponse,
|
||||
OnboardingStartInput,
|
||||
OnboardingStartResponse,
|
||||
OnboardingVerifyOtpInput,
|
||||
OnboardingVerifyOtpResponse,
|
||||
PlanSummary,
|
||||
PublicAvailabilityResponse,
|
||||
PublicBooking,
|
||||
PublicBookingConfirmation,
|
||||
SelectComplexInput,
|
||||
Sport,
|
||||
UpdateAdminBookingStatusInput,
|
||||
UpdateComplexPayload,
|
||||
UpdateComplexResponse,
|
||||
UpdateCourtInput,
|
||||
UpdateSportInput,
|
||||
UserProfileResponse,
|
||||
} from '@repo/api-contract';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
import * as api from './api';
|
||||
|
||||
const apiBaseUrl =
|
||||
import.meta.env.VITE_API_BASE_URL?.trim() ||
|
||||
(import.meta.env.DEV ? 'http://localhost:3000' : window.location.origin);
|
||||
const apiBaseUrl = api.apiBaseUrl;
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: apiBaseUrl,
|
||||
@@ -61,245 +27,30 @@ export class ApiClientError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
type ErrorHandlers = {
|
||||
onForbidden?: (error: ApiClientError) => void | Promise<void>;
|
||||
onError?: (error: ApiClientError) => void | Promise<void>;
|
||||
};
|
||||
|
||||
const handlers: ErrorHandlers = {};
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: apiBaseUrl,
|
||||
withCredentials: true,
|
||||
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.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<ComplexWithRole[]>('/api/complexes/mine');
|
||||
return response.data;
|
||||
},
|
||||
getCurrent: async () => {
|
||||
const response = await http.get<ComplexWithRole | null>('/api/complexes/me');
|
||||
return response.data;
|
||||
},
|
||||
select: async (payload: SelectComplexInput) => {
|
||||
const response = await http.post<ComplexWithRole>('/api/complexes/select', JSON.stringify(payload), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
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;
|
||||
},
|
||||
},
|
||||
onboarding: api.onboarding,
|
||||
plans: api.plans,
|
||||
user: api.user,
|
||||
complexes: api.complexes,
|
||||
sports: api.sports,
|
||||
courts: api.courts,
|
||||
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;
|
||||
},
|
||||
getAvailability: api.getAvailability,
|
||||
create: api.createPublic,
|
||||
getConfirmation: api.getConfirmation,
|
||||
},
|
||||
adminBookings: {
|
||||
listByComplex: async (
|
||||
complexId: string,
|
||||
params: {
|
||||
fromDate: string;
|
||||
}
|
||||
) => {
|
||||
const response = await http.get<{ bookings: AdminBooking[] }>(
|
||||
`/api/admin-bookings/complex/${complexId}`,
|
||||
{
|
||||
params,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
create: async (complexId: string, payload: CreateAdminBookingInput) => {
|
||||
const response = await http.post<AdminBooking>(
|
||||
`/api/admin-bookings/complex/${complexId}`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
updateStatus: async (bookingId: string, payload: UpdateAdminBookingStatusInput) => {
|
||||
const response = await http.patch<AdminBooking>(
|
||||
`/api/admin-bookings/${bookingId}/status`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
listByComplex: api.listByComplex,
|
||||
create: api.createAdmin,
|
||||
updateStatus: api.updateStatus,
|
||||
},
|
||||
};
|
||||
|
||||
export type ApiClient = typeof apiClient;
|
||||
|
||||
export function configureApiClient(nextHandlers: ErrorHandlers) {
|
||||
handlers.onForbidden = nextHandlers.onForbidden;
|
||||
handlers.onError = nextHandlers.onError;
|
||||
}
|
||||
export type { ErrorHandlers } from './api';
|
||||
export { configureApiClient } from './api';
|
||||
|
||||
export function setApiAccessToken(_token: string | null) {
|
||||
// accessToken = token;
|
||||
}
|
||||
|
||||
export type ApiClient = typeof apiClient;
|
||||
34
apps/frontend/src/lib/api/base.ts
Normal file
34
apps/frontend/src/lib/api/base.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export type ErrorHandlers = {
|
||||
onForbidden?: (error: ApiClientError) => void | Promise<void>;
|
||||
onError?: (error: ApiClientError) => void | Promise<void>;
|
||||
};
|
||||
|
||||
const handlers: ErrorHandlers = {};
|
||||
|
||||
export const apiBaseUrl =
|
||||
import.meta.env.VITE_API_BASE_URL?.trim() ||
|
||||
(import.meta.env.DEV ? 'http://localhost:3000' : window.location.origin);
|
||||
|
||||
export function configureApiClient(nextHandlers: ErrorHandlers) {
|
||||
handlers.onForbidden = nextHandlers.onForbidden;
|
||||
handlers.onError = nextHandlers.onError;
|
||||
}
|
||||
|
||||
export { handlers };
|
||||
60
apps/frontend/src/lib/api/http.ts
Normal file
60
apps/frontend/src/lib/api/http.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import axios, { AxiosError, type AxiosInstance } from 'axios';
|
||||
import { ApiClientError, apiBaseUrl, handlers } from './base';
|
||||
|
||||
export const http: AxiosInstance = axios.create({
|
||||
baseURL: apiBaseUrl,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
function extractMessage(details: unknown, fallback: string): 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.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);
|
||||
}
|
||||
);
|
||||
18
apps/frontend/src/lib/api/index.ts
Normal file
18
apps/frontend/src/lib/api/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export { ApiClientError, configureApiClient, apiBaseUrl, type ErrorHandlers } from './base';
|
||||
export { http } from './http';
|
||||
|
||||
export * as complexes from './resources/complexes';
|
||||
export * as courts from './resources/courts';
|
||||
export * as user from './resources/user';
|
||||
export * as sports from './resources/sports';
|
||||
export * as plans from './resources/plans';
|
||||
export * as onboarding from './resources/onboarding';
|
||||
|
||||
export {
|
||||
getAvailability,
|
||||
createPublic,
|
||||
getConfirmation,
|
||||
listByComplex,
|
||||
createAdmin,
|
||||
updateStatus,
|
||||
} from './resources/bookings';
|
||||
60
apps/frontend/src/lib/api/resources/bookings.ts
Normal file
60
apps/frontend/src/lib/api/resources/bookings.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
AdminBooking,
|
||||
CreateAdminBookingInput,
|
||||
CreatePublicBookingInput,
|
||||
PublicAvailabilityResponse,
|
||||
PublicBooking,
|
||||
PublicBookingConfirmation,
|
||||
UpdateAdminBookingStatusInput,
|
||||
} from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function getAvailability(
|
||||
complexSlug: string,
|
||||
params: { date: string; sportId?: string }
|
||||
) {
|
||||
const response = await http.get<PublicAvailabilityResponse>(
|
||||
`/api/public-bookings/complex/${complexSlug}/availability`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createPublic(complexSlug: string, payload: CreatePublicBookingInput) {
|
||||
const response = await http.post<PublicBooking>(
|
||||
`/api/public-bookings/complex/${complexSlug}`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getConfirmation(complexSlug: string, bookingCode: string) {
|
||||
const response = await http.get<PublicBookingConfirmation>(
|
||||
`/api/public-bookings/complex/${complexSlug}/confirmation/${bookingCode}`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function listByComplex(complexId: string, params: { fromDate: string }) {
|
||||
const response = await http.get<{ bookings: AdminBooking[] }>(
|
||||
`/api/admin-bookings/complex/${complexId}`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createAdmin(complexId: string, payload: CreateAdminBookingInput) {
|
||||
const response = await http.post<AdminBooking>(
|
||||
`/api/admin-bookings/complex/${complexId}`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateStatus(bookingId: string, payload: UpdateAdminBookingStatusInput) {
|
||||
const response = await http.patch<AdminBooking>(
|
||||
`/api/admin-bookings/${bookingId}/status`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
49
apps/frontend/src/lib/api/resources/complexes.ts
Normal file
49
apps/frontend/src/lib/api/resources/complexes.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type {
|
||||
Complex,
|
||||
ComplexWithRole,
|
||||
CreateComplexPayload,
|
||||
CreateComplexResponse,
|
||||
SelectComplexInput,
|
||||
UpdateComplexPayload,
|
||||
UpdateComplexResponse,
|
||||
} from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function create(payload: CreateComplexPayload) {
|
||||
const response = await http.post<CreateComplexResponse>('/api/complexes', payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getById(id: string) {
|
||||
const response = await http.get<Complex>(`/api/complexes/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getBySlug(slug: string) {
|
||||
const response = await http.get<Complex>(`/api/complexes/slug/${slug}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function update(id: string, payload: UpdateComplexPayload) {
|
||||
const response = await http.patch<UpdateComplexResponse>(`/api/complexes/${id}`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function listMine() {
|
||||
const response = await http.get<ComplexWithRole[]>('/api/complexes/mine');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getCurrent() {
|
||||
const response = await http.get<ComplexWithRole | null>('/api/complexes/me');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function select(payload: SelectComplexInput) {
|
||||
const response = await http.post<ComplexWithRole>(
|
||||
'/api/complexes/select',
|
||||
JSON.stringify(payload),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
17
apps/frontend/src/lib/api/resources/courts.ts
Normal file
17
apps/frontend/src/lib/api/resources/courts.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Court, CreateCourtInput, UpdateCourtInput } from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function listByComplex(complexId: string) {
|
||||
const response = await http.get<Court[]>(`/api/courts/complex/${complexId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function create(complexId: string, payload: CreateCourtInput) {
|
||||
const response = await http.post<Court>(`/api/courts/complex/${complexId}`, payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function update(id: string, payload: UpdateCourtInput) {
|
||||
const response = await http.patch<Court>(`/api/courts/${id}`, payload);
|
||||
return response.data;
|
||||
}
|
||||
40
apps/frontend/src/lib/api/resources/onboarding.ts
Normal file
40
apps/frontend/src/lib/api/resources/onboarding.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type {
|
||||
OnboardingCompleteInput,
|
||||
OnboardingCompleteResponse,
|
||||
OnboardingResendOtpInput,
|
||||
OnboardingResendOtpResponse,
|
||||
OnboardingStartInput,
|
||||
OnboardingStartResponse,
|
||||
OnboardingVerifyOtpInput,
|
||||
OnboardingVerifyOtpResponse,
|
||||
} from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function start(payload: OnboardingStartInput) {
|
||||
const response = await http.post<OnboardingStartResponse>('/api/onboarding/start', payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function verifyOtp(payload: OnboardingVerifyOtpInput) {
|
||||
const response = await http.post<OnboardingVerifyOtpResponse>(
|
||||
'/api/onboarding/verify-otp',
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function resendOtp(payload: OnboardingResendOtpInput) {
|
||||
const response = await http.post<OnboardingResendOtpResponse>(
|
||||
'/api/onboarding/resend-otp',
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function complete(payload: OnboardingCompleteInput) {
|
||||
const response = await http.post<OnboardingCompleteResponse>(
|
||||
'/api/onboarding/complete',
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
7
apps/frontend/src/lib/api/resources/plans.ts
Normal file
7
apps/frontend/src/lib/api/resources/plans.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { PlanSummary } from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function list() {
|
||||
const response = await http.get<PlanSummary[]>('/api/plans');
|
||||
return response.data;
|
||||
}
|
||||
17
apps/frontend/src/lib/api/resources/sports.ts
Normal file
17
apps/frontend/src/lib/api/resources/sports.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { CreateSportInput, Sport, UpdateSportInput } from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function list() {
|
||||
const response = await http.get<Sport[]>('/api/sports');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function create(payload: CreateSportInput) {
|
||||
const response = await http.post<Sport>('/api/sports', payload);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function update(id: string, payload: UpdateSportInput) {
|
||||
const response = await http.patch<Sport>(`/api/sports/${id}`, payload);
|
||||
return response.data;
|
||||
}
|
||||
7
apps/frontend/src/lib/api/resources/user.ts
Normal file
7
apps/frontend/src/lib/api/resources/user.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { UserProfileResponse } from '@repo/api-contract';
|
||||
import { http } from '../http';
|
||||
|
||||
export async function getProfile() {
|
||||
const response = await http.get<UserProfileResponse>('/api/user/profile');
|
||||
return response.data;
|
||||
}
|
||||
Reference in New Issue
Block a user