GNXSOFT.COM
This commit is contained in:
334
gnx-react/lib/api/aboutService.ts
Normal file
334
gnx-react/lib/api/aboutService.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
|
||||
// Types for About Us data
|
||||
export interface AboutStat {
|
||||
number: string;
|
||||
label: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface AboutSocialLink {
|
||||
platform: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
aria_label: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface AboutBanner {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
badge_text: string;
|
||||
badge_icon: string;
|
||||
cta_text: string;
|
||||
cta_link: string;
|
||||
cta_icon: string;
|
||||
image_url: string | null;
|
||||
is_active: boolean;
|
||||
stats: AboutStat[];
|
||||
social_links: AboutSocialLink[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AboutFeature {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface AboutService {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
badge_text: string;
|
||||
badge_icon: string;
|
||||
image_url: string | null;
|
||||
cta_text: string;
|
||||
cta_link: string;
|
||||
is_active: boolean;
|
||||
features: AboutFeature[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AboutProcessStep {
|
||||
step_number: string;
|
||||
title: string;
|
||||
description: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface AboutProcess {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
badge_text: string;
|
||||
badge_icon: string;
|
||||
image_url: string | null;
|
||||
cta_text: string;
|
||||
cta_link: string;
|
||||
is_active: boolean;
|
||||
steps: AboutProcessStep[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AboutMilestone {
|
||||
year: string;
|
||||
title: string;
|
||||
description: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface AboutJourney {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
badge_text: string;
|
||||
badge_icon: string;
|
||||
image_url: string | null;
|
||||
cta_text: string;
|
||||
cta_link: string;
|
||||
is_active: boolean;
|
||||
milestones: AboutMilestone[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AboutPageData {
|
||||
banner: AboutBanner;
|
||||
service: AboutService;
|
||||
process: AboutProcess;
|
||||
journey: AboutJourney;
|
||||
}
|
||||
|
||||
class AboutService {
|
||||
private baseUrl = `${API_BASE_URL}/api/about`;
|
||||
|
||||
/**
|
||||
* Get all about page data in one request
|
||||
*/
|
||||
async getAboutPageData(): Promise<AboutPageData> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/page/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching about page data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all about banners
|
||||
*/
|
||||
async getBanners(): Promise<AboutBanner[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/banner/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching about banners:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific about banner by ID
|
||||
*/
|
||||
async getBanner(id: number): Promise<AboutBanner> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/banner/${id}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching about banner ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all about services
|
||||
*/
|
||||
async getServices(): Promise<AboutService[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/service/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching about services:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific about service by ID
|
||||
*/
|
||||
async getService(id: number): Promise<AboutService> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/service/${id}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching about service ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all about processes
|
||||
*/
|
||||
async getProcesses(): Promise<AboutProcess[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/process/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching about processes:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific about process by ID
|
||||
*/
|
||||
async getProcess(id: number): Promise<AboutProcess> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/process/${id}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching about process ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all about journeys
|
||||
*/
|
||||
async getJourneys(): Promise<AboutJourney[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/journey/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching about journeys:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific about journey by ID
|
||||
*/
|
||||
async getJourney(id: number): Promise<AboutJourney> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/journey/${id}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching about journey ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const aboutService = new AboutService();
|
||||
export default aboutService;
|
||||
131
gnx-react/lib/api/contactService.ts
Normal file
131
gnx-react/lib/api/contactService.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Contact API Service
|
||||
* Handles communication with the Django REST API for contact form submissions
|
||||
*/
|
||||
|
||||
import { API_CONFIG } from '@/lib/config/api';
|
||||
|
||||
export interface ContactFormData {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
company: string;
|
||||
job_title: string;
|
||||
industry?: string;
|
||||
company_size?: string;
|
||||
project_type?: string;
|
||||
timeline?: string;
|
||||
budget?: string;
|
||||
message: string;
|
||||
newsletter_subscription: boolean;
|
||||
privacy_consent: boolean;
|
||||
}
|
||||
|
||||
export interface ContactSubmissionResponse {
|
||||
message: string;
|
||||
submission_id: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
errors?: Record<string, string[]>;
|
||||
status: number;
|
||||
}
|
||||
|
||||
class ContactApiService {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTACT}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a contact form to the Django API
|
||||
*/
|
||||
async submitContactForm(data: ContactFormData): Promise<ContactSubmissionResponse> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/submissions/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
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;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to submit contact form: ${error.message}`);
|
||||
}
|
||||
throw new Error('Failed to submit contact form: Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contact submission statistics (admin only)
|
||||
*/
|
||||
async getContactStats(): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/submissions/stats/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to fetch contact stats: ${error.message}`);
|
||||
}
|
||||
throw new Error('Failed to fetch contact stats: Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent contact submissions (admin only)
|
||||
*/
|
||||
async getRecentSubmissions(): Promise<any[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/submissions/recent/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to fetch recent submissions: ${error.message}`);
|
||||
}
|
||||
throw new Error('Failed to fetch recent submissions: Unknown error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export a singleton instance
|
||||
export const contactApiService = new ContactApiService();
|
||||
|
||||
// Export the class for testing purposes
|
||||
export default ContactApiService;
|
||||
495
gnx-react/lib/api/serviceService.ts
Normal file
495
gnx-react/lib/api/serviceService.ts
Normal file
@@ -0,0 +1,495 @@
|
||||
import { API_CONFIG } 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: {
|
||||
'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}`
|
||||
);
|
||||
}
|
||||
|
||||
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: {
|
||||
'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}`
|
||||
);
|
||||
}
|
||||
|
||||
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: {
|
||||
'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}`
|
||||
);
|
||||
}
|
||||
|
||||
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: {
|
||||
'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}`
|
||||
);
|
||||
}
|
||||
|
||||
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: {
|
||||
'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}`
|
||||
);
|
||||
}
|
||||
|
||||
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: {
|
||||
'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}`
|
||||
);
|
||||
}
|
||||
|
||||
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: {
|
||||
'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}`
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
getServiceImageUrl: (service: Service): string => {
|
||||
// If service has an uploaded image
|
||||
if (service.image && typeof service.image === 'string' && service.image.startsWith('/media/')) {
|
||||
return `${API_CONFIG.BASE_URL}${service.image}`;
|
||||
}
|
||||
|
||||
// If service has an image_url
|
||||
if (service.image_url) {
|
||||
if (service.image_url.startsWith('http')) {
|
||||
return service.image_url;
|
||||
}
|
||||
return `${API_CONFIG.BASE_URL}${service.image_url}`;
|
||||
}
|
||||
|
||||
// Fallback to default image
|
||||
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;
|
||||
});
|
||||
},
|
||||
};
|
||||
35
gnx-react/lib/config/api.ts
Normal file
35
gnx-react/lib/config/api.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* API Configuration
|
||||
* Centralized configuration for API endpoints
|
||||
*/
|
||||
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
export const API_CONFIG = {
|
||||
// Django API Base URL
|
||||
BASE_URL: API_BASE_URL,
|
||||
|
||||
// API Endpoints
|
||||
ENDPOINTS: {
|
||||
CONTACT: '/api/contact',
|
||||
CONTACT_SUBMISSIONS: '/api/contact/submissions',
|
||||
CONTACT_STATS: '/api/contact/submissions/stats',
|
||||
CONTACT_RECENT: '/api/contact/submissions/recent',
|
||||
SERVICES: '/api/services',
|
||||
SERVICES_FEATURED: '/api/services/featured',
|
||||
SERVICES_SEARCH: '/api/services/search',
|
||||
SERVICES_STATS: '/api/services/stats',
|
||||
SERVICES_CATEGORIES: '/api/services/categories',
|
||||
},
|
||||
|
||||
// Request timeout (in milliseconds)
|
||||
TIMEOUT: 10000,
|
||||
|
||||
// Retry configuration
|
||||
RETRY: {
|
||||
ATTEMPTS: 3,
|
||||
DELAY: 1000,
|
||||
}
|
||||
} as const;
|
||||
|
||||
export default API_CONFIG;
|
||||
208
gnx-react/lib/hooks/useAbout.ts
Normal file
208
gnx-react/lib/hooks/useAbout.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { aboutService, AboutPageData, AboutBanner, AboutService, AboutProcess, AboutJourney } from '../api/aboutService';
|
||||
|
||||
interface UseAboutReturn {
|
||||
data: AboutPageData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseAboutBannerReturn {
|
||||
data: AboutBanner[] | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseAboutServiceReturn {
|
||||
data: AboutService[] | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseAboutProcessReturn {
|
||||
data: AboutProcess[] | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseAboutJourneyReturn {
|
||||
data: AboutJourney[] | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch all about page data
|
||||
*/
|
||||
export const useAbout = (): UseAboutReturn => {
|
||||
const [data, setData] = useState<AboutPageData | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const result = await aboutService.getAboutPageData();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
console.error('Error fetching about page data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchData,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch about banners
|
||||
*/
|
||||
export const useAboutBanners = (): UseAboutBannerReturn => {
|
||||
const [data, setData] = useState<AboutBanner[] | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const result = await aboutService.getBanners();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
console.error('Error fetching about banners:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchData,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch about services
|
||||
*/
|
||||
export const useAboutServices = (): UseAboutServiceReturn => {
|
||||
const [data, setData] = useState<AboutService[] | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const result = await aboutService.getServices();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
console.error('Error fetching about services:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchData,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch about processes
|
||||
*/
|
||||
export const useAboutProcesses = (): UseAboutProcessReturn => {
|
||||
const [data, setData] = useState<AboutProcess[] | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const result = await aboutService.getProcesses();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
console.error('Error fetching about processes:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchData,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch about journeys
|
||||
*/
|
||||
export const useAboutJourneys = (): UseAboutJourneyReturn => {
|
||||
const [data, setData] = useState<AboutJourney[] | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const result = await aboutService.getJourneys();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
console.error('Error fetching about journeys:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchData,
|
||||
};
|
||||
};
|
||||
302
gnx-react/lib/hooks/useServices.ts
Normal file
302
gnx-react/lib/hooks/useServices.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { serviceService, Service, ServiceListResponse, ServiceStats, ServiceSearchResponse } from '../api/serviceService';
|
||||
|
||||
// Hook for fetching all services
|
||||
export const useServices = (params?: {
|
||||
featured?: boolean;
|
||||
category?: string;
|
||||
min_price?: number;
|
||||
max_price?: number;
|
||||
search?: string;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
}) => {
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState<{
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
} | null>(null);
|
||||
|
||||
const fetchServices = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.getServices(params);
|
||||
setServices(response.results);
|
||||
setPagination({
|
||||
count: response.count,
|
||||
next: response.next,
|
||||
previous: response.previous,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch services');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchServices();
|
||||
}, [fetchServices]);
|
||||
|
||||
return {
|
||||
services,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
refetch: fetchServices,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for fetching a single service by slug
|
||||
export const useService = (slug: string) => {
|
||||
const [service, setService] = useState<Service | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchService = useCallback(async () => {
|
||||
if (!slug) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.getServiceBySlug(slug);
|
||||
setService(response);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch service');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchService();
|
||||
}, [fetchService]);
|
||||
|
||||
return {
|
||||
service,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchService,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for fetching featured services
|
||||
export const useFeaturedServices = () => {
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchFeaturedServices = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.getFeaturedServices();
|
||||
setServices(response.results);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch featured services');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeaturedServices();
|
||||
}, [fetchFeaturedServices]);
|
||||
|
||||
return {
|
||||
services,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchFeaturedServices,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for searching services
|
||||
export const useServiceSearch = (query: string) => {
|
||||
const [results, setResults] = useState<Service[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const searchServices = useCallback(async (searchQuery: string) => {
|
||||
if (!searchQuery.trim()) {
|
||||
setResults([]);
|
||||
setCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.searchServices(searchQuery);
|
||||
setResults(response.results);
|
||||
setCount(response.count);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to search services');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
searchServices(query);
|
||||
}, 300); // Debounce search
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [query, searchServices]);
|
||||
|
||||
return {
|
||||
results,
|
||||
loading,
|
||||
error,
|
||||
count,
|
||||
search: searchServices,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for fetching service statistics
|
||||
export const useServiceStats = () => {
|
||||
const [stats, setStats] = useState<ServiceStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.getServiceStats();
|
||||
setStats(response);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch service stats');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [fetchStats]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchStats,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for managing service state (for admin operations)
|
||||
export const useServiceManagement = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const createService = useCallback(async (serviceData: Partial<Service>) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.createService(serviceData);
|
||||
return response;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create service');
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateService = useCallback(async (slug: string, serviceData: Partial<Service>) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.updateService(slug, serviceData);
|
||||
return response;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update service');
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deleteService = useCallback(async (slug: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
await serviceService.deleteService(slug);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete service');
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const uploadServiceImage = useCallback(async (slug: string, imageFile: File) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await serviceService.uploadServiceImage(slug, imageFile);
|
||||
return response;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to upload service image');
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
createService,
|
||||
updateService,
|
||||
deleteService,
|
||||
uploadServiceImage,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for navigation services (for header menu)
|
||||
export const useNavigationServices = () => {
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchNavigationServices = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
// Fetch only active services, ordered by display_order
|
||||
const response = await serviceService.getServices({
|
||||
ordering: 'display_order',
|
||||
page: 1
|
||||
});
|
||||
// Filter only active services (handle null values as active)
|
||||
const activeServices = response.results.filter(service => service.is_active !== false);
|
||||
setServices(activeServices);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch navigation services');
|
||||
// Set empty array on error to prevent navigation issues
|
||||
setServices([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchNavigationServices();
|
||||
}, [fetchNavigationServices]);
|
||||
|
||||
return {
|
||||
services,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchNavigationServices,
|
||||
};
|
||||
};
|
||||
36
gnx-react/lib/imageUtils.ts
Normal file
36
gnx-react/lib/imageUtils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Image utility functions
|
||||
|
||||
export const FALLBACK_IMAGES = {
|
||||
BLOG: '/images/blog/blog-poster.png',
|
||||
CASE_STUDY: '/images/case/poster.png',
|
||||
SERVICE: '/images/service/thumb-one.png',
|
||||
GALLERY: '/images/masonry/one.png',
|
||||
BANNER: '/images/banner/banner-bg.png',
|
||||
DEFAULT: '/images/logo.png'
|
||||
};
|
||||
|
||||
export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return fallback || FALLBACK_IMAGES.DEFAULT;
|
||||
}
|
||||
|
||||
// If it's already a full URL, return as is
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// If it starts with /, it's already a public path
|
||||
if (imageUrl.startsWith('/')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Otherwise, assume it's a public path and add /
|
||||
return `/${imageUrl}`;
|
||||
}
|
||||
|
||||
export function getImageAlt(title?: string, fallback: string = 'Image'): string {
|
||||
return title ? `${title} - Image` : fallback;
|
||||
}
|
||||
|
||||
// Export getValidImageAlt as an alias for getImageAlt for backward compatibility
|
||||
export const getValidImageAlt = getImageAlt;
|
||||
Reference in New Issue
Block a user