gohorsejobs/frontend/src/lib/api.ts

192 lines
5.8 KiB
TypeScript

import { getToken } from "./auth";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8521";
export interface ApiUser {
id: string;
name: string;
email: string;
identifier: string;
phone?: string;
role: string;
status: string;
created_at: string;
}
export interface ApiCompany {
id: string;
name: string;
slug: string;
email?: string;
phone?: string;
website?: string;
address?: string;
active: boolean;
verified: boolean;
created_at: string;
}
async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = getToken();
// Sanitize API_URL: remove trailing slash
let baseUrl = API_URL.replace(/\/+$/, "");
// Sanitize endpoint: ensure leading slash
let cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
// Detect and fix double prefixing of /api/v1
// Case 1: BaseURL ends with /api/v1 AND endpoint starts with /api/v1
if (baseUrl.endsWith("/api/v1") && cleanEndpoint.startsWith("/api/v1")) {
cleanEndpoint = cleanEndpoint.replace("/api/v1", "");
}
// Case 2: Double /api/v1 inside endpoint itself (if passed incorrectly)
if (cleanEndpoint.includes("/api/v1/api/v1")) {
cleanEndpoint = cleanEndpoint.replace("/api/v1/api/v1", "/api/v1");
}
const url = `${baseUrl}${cleanEndpoint}`;
console.log(`[API Request] ${url}`); // Debug log
const res = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (!res.ok) {
const error = await res.text();
throw new Error(error || `API Error: ${res.status}`);
}
return res.json();
}
// Users API
export const usersApi = {
list: () => apiRequest<ApiUser[]>("/api/v1/users"),
create: (data: { name: string; email: string; password: string; role: string }) =>
apiRequest<ApiUser>("/api/v1/users", {
method: "POST",
body: JSON.stringify(data),
}),
delete: (id: string) =>
apiRequest<void>(`/api/v1/users/${id}`, {
method: "DELETE",
}),
};
// Companies API
export const companiesApi = {
list: () => apiRequest<ApiCompany[]>("/api/v1/companies"),
create: (data: { name: string; slug: string; email?: string }) =>
apiRequest<ApiCompany>("/api/v1/companies", {
method: "POST",
body: JSON.stringify(data),
}),
};
// Jobs API (public)
export interface ApiJob {
id: number;
companyId: number;
createdBy: number;
title: string;
description: string;
salaryMin?: number;
salaryMax?: number;
salaryType?: string;
employmentType?: string;
workMode?: string;
workingHours?: string;
location?: string;
regionId?: number;
cityId?: number;
requirements?: Record<string, unknown> | string[];
benefits?: Record<string, unknown> | string[];
visaSupport: boolean;
languageLevel?: string;
status: string;
createdAt: string;
updatedAt: string;
companyName?: string;
companyLogoUrl?: string;
regionName?: string;
cityName?: string;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
};
}
export const jobsApi = {
list: (params?: { page?: number; limit?: number; companyId?: number }) => {
const query = new URLSearchParams();
if (params?.page) query.set('page', String(params.page));
if (params?.limit) query.set('limit', String(params.limit));
if (params?.companyId) query.set('companyId', String(params.companyId));
const queryStr = query.toString();
return apiRequest<PaginatedResponse<ApiJob>>(`/jobs${queryStr ? `?${queryStr}` : ''}`);
},
getById: (id: number) => apiRequest<ApiJob>(`/jobs/${id}`),
};
// Transform API job to frontend Job format
export function transformApiJobToFrontend(apiJob: ApiJob): import('./types').Job {
// Format salary
let salary: string | undefined;
if (apiJob.salaryMin && apiJob.salaryMax) {
salary = `R$ ${apiJob.salaryMin.toLocaleString('pt-BR')} - R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`;
} else if (apiJob.salaryMin) {
salary = `A partir de R$ ${apiJob.salaryMin.toLocaleString('pt-BR')}`;
} else if (apiJob.salaryMax) {
salary = `Até R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`;
}
// Determine type
type JobType = 'full-time' | 'part-time' | 'contract' | 'Remoto' | 'Tempo Integral';
let type: JobType = 'Tempo Integral';
if (apiJob.employmentType === 'full-time') type = 'Tempo Integral';
else if (apiJob.employmentType === 'part-time') type = 'part-time';
else if (apiJob.employmentType === 'contract') type = 'contract';
else if (apiJob.location?.toLowerCase().includes('remoto')) type = 'Remoto';
// Extract requirements
const requirements: string[] = [];
if (apiJob.requirements) {
if (Array.isArray(apiJob.requirements)) {
requirements.push(...apiJob.requirements.map(String));
} else if (typeof apiJob.requirements === 'object') {
Object.values(apiJob.requirements).forEach(v => requirements.push(String(v)));
}
}
return {
id: String(apiJob.id),
title: apiJob.title,
company: apiJob.companyName || 'Empresa',
location: apiJob.location || apiJob.cityName || 'Localização não informada',
type,
workMode: apiJob.workMode as any,
salary,
description: apiJob.description,
requirements: requirements.length > 0 ? requirements : ['Ver detalhes'],
postedAt: apiJob.createdAt?.split('T')[0] || new Date().toISOString().split('T')[0],
};
}