Files
GNX-WEB/frontEnd/lib/api/serviceService.ts
Iliyan Angelov 4c8b71fe0d updates
2025-11-25 09:21:00 +02:00

507 lines
14 KiB
TypeScript

import { API_CONFIG, getApiHeaders } from '../config/api';
// Types for Service API
export interface ServiceFeature {
id: number;
title: string;
description: string;
icon: string;
display_order: number;
}
export interface ServiceExpertise {
id: number;
title: string;
description: string;
icon: string;
display_order: number;
}
export interface ServiceCategory {
id: number;
name: string;
slug: string;
description: string;
display_order: number;
}
export interface Service {
id: number;
title: string;
description: string;
short_description?: string;
slug: string;
icon: string;
image?: File | string;
image_url?: string;
price: string;
formatted_price: string;
category?: ServiceCategory;
duration?: string;
deliverables?: string;
technologies?: string;
process_steps?: string;
features_description?: string;
deliverables_description?: string;
process_description?: string;
why_choose_description?: string;
expertise_description?: string;
featured: boolean;
display_order: number;
is_active: boolean;
created_at: string;
updated_at: string;
features?: ServiceFeature[];
expertise_items?: ServiceExpertise[];
}
export interface ServiceListResponse {
count: number;
next: string | null;
previous: string | null;
results: Service[];
}
export interface ServiceStats {
total_services: number;
featured_services: number;
categories: number;
average_price: number;
}
export interface ServiceSearchResponse {
query: string;
count: number;
results: Service[];
}
// Helper function to build query string
const buildQueryString = (params: Record<string, any>): string => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
return searchParams.toString();
};
// Service API functions
export const serviceService = {
// Get all services with optional filtering
getServices: async (params?: {
featured?: boolean;
category?: string;
min_price?: number;
max_price?: number;
search?: string;
ordering?: string;
page?: number;
}): Promise<ServiceListResponse> => {
try {
const queryString = params ? buildQueryString(params) : '';
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch services: ${error.message}`);
}
throw new Error('Failed to fetch services: Unknown error');
}
},
// Get a single service by slug
getServiceBySlug: async (slug: string): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/${slug}/`;
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch service: ${error.message}`);
}
throw new Error('Failed to fetch service: Unknown error');
}
},
// Get featured services
getFeaturedServices: async (): Promise<ServiceListResponse> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_FEATURED}/`;
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch featured services: ${error.message}`);
}
throw new Error('Failed to fetch featured services: Unknown error');
}
},
// Search services
searchServices: async (query: string): Promise<ServiceSearchResponse> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_SEARCH}/?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to search services: ${error.message}`);
}
throw new Error('Failed to search services: Unknown error');
}
},
// Get service statistics
getServiceStats: async (): Promise<ServiceStats> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_STATS}/`;
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch service stats: ${error.message}`);
}
throw new Error('Failed to fetch service stats: Unknown error');
}
},
// Get all service categories
getCategories: async (): Promise<ServiceCategory[]> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_CATEGORIES}/`;
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch categories: ${error.message}`);
}
throw new Error('Failed to fetch categories: Unknown error');
}
},
// Get a single category by slug
getCategoryBySlug: async (slug: string): Promise<ServiceCategory> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_CATEGORIES}/${slug}/`;
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch category: ${error.message}`);
}
throw new Error('Failed to fetch category: Unknown error');
}
},
// Admin functions (require authentication)
createService: async (serviceData: Partial<Service>): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/create/`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(serviceData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to create service: ${error.message}`);
}
throw new Error('Failed to create service: Unknown error');
}
},
updateService: async (slug: string, serviceData: Partial<Service>): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/${slug}/update/`;
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(serviceData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to update service: ${error.message}`);
}
throw new Error('Failed to update service: Unknown error');
}
},
deleteService: async (slug: string): Promise<void> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/${slug}/delete/`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to delete service: ${error.message}`);
}
throw new Error('Failed to delete service: Unknown error');
}
},
// Upload image for a service
uploadServiceImage: async (slug: string, imageFile: File): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/${slug}/upload-image/`;
const formData = new FormData();
formData.append('image', imageFile);
const response = await fetch(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
const result = await response.json();
return result.service;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to upload service image: ${error.message}`);
}
throw new Error('Failed to upload service image: Unknown error');
}
},
};
// Utility functions
export const serviceUtils = {
// Format price for display
formatPrice: (price: string | number): string => {
const numPrice = typeof price === 'string' ? parseFloat(price) : price;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numPrice);
},
// Get service image URL with cache-busting
// Use relative URLs for same-domain images (Next.js can optimize via rewrites)
// Use absolute URLs only for external images
// Adds updated_at timestamp as query parameter for cache-busting when images change
getServiceImageUrl: (service: Service): string => {
let imageUrl: string = '';
// If service has an uploaded image
if (service.image && typeof service.image === 'string' && service.image.startsWith('/media/')) {
imageUrl = service.image;
}
// If service has an image_url
else if (service.image_url) {
if (service.image_url.startsWith('http')) {
// External URL - keep as absolute
imageUrl = service.image_url;
} else if (service.image_url.startsWith('/media/')) {
// Same domain media - use relative URL
imageUrl = service.image_url;
} else {
// Other relative URLs
imageUrl = service.image_url;
}
} else {
// Fallback to default image (relative is fine for public images)
imageUrl = '/images/service/default.png';
}
// Add cache-busting query parameter using updated_at timestamp
// This ensures images refresh when service is updated
if (service.updated_at && imageUrl && !imageUrl.includes('?')) {
try {
const timestamp = new Date(service.updated_at).getTime();
const separator = imageUrl.includes('?') ? '&' : '?';
imageUrl = `${imageUrl}${separator}v=${timestamp}`;
} catch (error) {
// If date parsing fails, just return the URL without cache-busting
console.warn('Failed to parse updated_at for cache-busting:', error);
}
}
return imageUrl;
},
// Generate service slug from title
generateSlug: (title: string): string => {
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
},
// Check if service is featured
isFeatured: (service: Service): boolean => {
return service.featured;
},
// Sort services by display order
sortByDisplayOrder: (services: Service[]): Service[] => {
return [...services].sort((a, b) => a.display_order - b.display_order);
},
// Filter services by category
filterByCategory: (services: Service[], categorySlug: string): Service[] => {
return services.filter(service => service.category?.slug === categorySlug);
},
// Get services within price range
filterByPriceRange: (services: Service[], minPrice: number, maxPrice: number): Service[] => {
return services.filter(service => {
const price = parseFloat(service.price);
return price >= minPrice && price <= maxPrice;
});
},
};