Add new Frontend API folder

This commit is contained in:
Urtzi Alfaro
2025-08-03 17:48:34 +02:00
parent 935f45a283
commit 03e9dc6469
50 changed files with 4737 additions and 3321 deletions

View File

@@ -0,0 +1,50 @@
// frontend/src/api/utils/error.ts
/**
* Error Handling Utilities
*/
import type { ApiError } from '../types';
export class ApiErrorHandler {
static formatError(error: any): string {
if (error?.response?.data) {
const errorData = error.response.data as ApiError;
return errorData.detail || errorData.message || 'An error occurred';
}
if (error?.message) {
return error.message;
}
return 'An unexpected error occurred';
}
static getErrorCode(error: any): string | undefined {
return error?.response?.data?.error_code;
}
static isNetworkError(error: any): boolean {
return !error?.response && error?.message?.includes('Network');
}
static isAuthError(error: any): boolean {
const status = error?.response?.status;
return status === 401 || status === 403;
}
static isValidationError(error: any): boolean {
return error?.response?.status === 422;
}
static isServerError(error: any): boolean {
const status = error?.response?.status;
return status >= 500;
}
static shouldRetry(error: any): boolean {
if (this.isNetworkError(error)) return true;
if (this.isServerError(error)) return true;
const status = error?.response?.status;
return status === 408 || status === 429; // Timeout or Rate limited
}
}

View File

@@ -0,0 +1,9 @@
// frontend/src/api/utils/index.ts
/**
* Main Utils Export
*/
export { ApiErrorHandler } from './error';
export { ResponseProcessor } from './response';
export { RequestValidator } from './validation';
export { DataTransformer } from './transform';

View File

@@ -0,0 +1,34 @@
// frontend/src/api/utils/response.ts
/**
* Response Processing Utilities
*/
import type { ApiResponse, PaginatedResponse } from '../types';
export class ResponseProcessor {
static extractData<T>(response: ApiResponse<T>): T {
return response.data;
}
static extractPaginatedData<T>(response: PaginatedResponse<T>): {
data: T[];
pagination: PaginatedResponse<T>['pagination'];
} {
return {
data: response.data,
pagination: response.pagination,
};
}
static isSuccessResponse(response: ApiResponse): boolean {
return response.status === 'success' || response.status === 'ok';
}
static extractMessage(response: ApiResponse): string | undefined {
return response.message;
}
static extractMeta(response: ApiResponse): any {
return response.meta;
}
}

View File

@@ -0,0 +1,70 @@
// frontend/src/api/utils/transform.ts
/**
* Data Transformation Utilities
*/
export class DataTransformer {
static formatDate(date: string | Date): string {
const d = new Date(date);
return d.toLocaleDateString();
}
static formatDateTime(date: string | Date): string {
const d = new Date(date);
return d.toLocaleString();
}
static formatCurrency(amount: number, currency = 'EUR'): string {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency,
}).format(amount);
}
static formatPercentage(value: number, decimals = 1): string {
return `${(value * 100).toFixed(decimals)}%`;
}
static formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${Math.round(bytes / Math.pow(1024, i) * 100) / 100} ${sizes[i]}`;
}
static slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w ]+/g, '')
.replace(/ +/g, '-');
}
static truncate(text: string, length: number): string {
if (text.length <= length) return text;
return `${text.substring(0, length)}...`;
}
static camelToKebab(str: string): string {
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
}
static kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
}
static deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
static removeEmpty(obj: Record<string, any>): Record<string, any> {
const cleaned: Record<string, any> = {};
Object.keys(obj).forEach(key => {
if (obj[key] !== null && obj[key] !== undefined && obj[key] !== '') {
cleaned[key] = obj[key];
}
});
return cleaned;
}
}

View File

@@ -0,0 +1,72 @@
// frontend/src/api/utils/validation.ts
/**
* Request Validation Utilities
*/
export class RequestValidator {
static validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static validatePassword(password: string): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
if (!/(?=.*[a-z])/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/(?=.*[A-Z])/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/(?=.*\d)/.test(password)) {
errors.push('Password must contain at least one number');
}
return {
valid: errors.length === 0,
errors,
};
}
static validateFile(file: File, allowedTypes: string[], maxSize: number): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (!allowedTypes.includes(file.type)) {
errors.push(`File type ${file.type} is not allowed`);
}
if (file.size > maxSize) {
errors.push(`File size exceeds maximum of ${maxSize} bytes`);
}
return {
valid: errors.length === 0,
errors,
};
}
static validatePhoneNumber(phone: string): boolean {
// Basic international phone number validation
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
return phoneRegex.test(phone.replace(/\s/g, ''));
}
static validateRequired(value: any, fieldName: string): string | null {
if (value === null || value === undefined || value === '') {
return `${fieldName} is required`;
}
return null;
}
}