491 lines
14 KiB
TypeScript
491 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
|
|
// Use relative URLs for same-domain images (Next.js can optimize via rewrites)
|
|
// Use absolute URLs only for external images
|
|
getServiceImageUrl: (service: Service): string => {
|
|
// If service has an uploaded image
|
|
if (service.image && typeof service.image === 'string' && service.image.startsWith('/media/')) {
|
|
// Use relative URL - Next.js rewrite will handle fetching from backend during optimization
|
|
return service.image;
|
|
}
|
|
|
|
// If service has an image_url
|
|
if (service.image_url) {
|
|
if (service.image_url.startsWith('http')) {
|
|
// External URL - keep as absolute
|
|
return service.image_url;
|
|
}
|
|
if (service.image_url.startsWith('/media/')) {
|
|
// Same domain media - use relative URL
|
|
return service.image_url;
|
|
}
|
|
// Other relative URLs
|
|
return service.image_url;
|
|
}
|
|
|
|
// Fallback to default image (relative is fine for public images)
|
|
return '/images/service/default.png';
|
|
},
|
|
|
|
// 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;
|
|
});
|
|
},
|
|
};
|