Image error resolve

This commit is contained in:
2025-10-10 17:48:11 +05:30
parent 5860d38d95
commit d375cad253
2 changed files with 449 additions and 286 deletions

View File

@ -1,254 +1,417 @@
// services/educationService.ts // components/EducationTraining.tsx
export interface ApiCourse { 'use client';
id: number; import { useState, useEffect } from 'react';
title: string; import Image from 'next/image';
description: string; import Link from 'next/link';
duration: string; import { ChevronRight, Clock, Users, Award, Calendar } from 'lucide-react';
seats: number; import { educationService, Course } from '../../services/educationService';
category: string; import { upcomingEventsService, UpcomingEvent } from '../../services/upcomingEventsService';
level: string;
instructor: string;
price?: string;
startDate?: string;
imageUrl?: string;
eligibility: string[];
objectives: string[];
isActive: boolean;
createdDate?: string;
updatedDate?: string;
}
export interface Course { const EducationTraining: React.FC = () => {
id: string; const [mounted, setMounted] = useState(false);
title: string; const [courses, setCourses] = useState<Course[]>([]);
description: string; const [upcomingEvents, setUpcomingEvents] = useState<UpcomingEvent[]>([]);
duration: string; const [loading, setLoading] = useState(true);
seats: number; const [error, setError] = useState<string | null>(null);
category: string; const [selectedCategory, setSelectedCategory] = useState<string>('All');
level: string; const [searchQuery, setSearchQuery] = useState<string>('');
instructor: string;
price: string;
startDate: string;
image: string;
eligibility: string[];
objectives: string[];
}
export interface CourseApplicationData { const categories = ['All', 'Certification', 'Training', 'Workshop', 'Fellowship'];
courseId: number;
fullName: string;
email: string;
phone: string;
qualification: string;
experience?: string;
coverLetter?: string;
resumeUrl?: string;
}
class EducationService { useEffect(() => {
private apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'; setMounted(true);
loadData();
}, []);
// Helper method to convert relative URLs to absolute URLs const loadData = async () => {
private getFullImageUrl(imageUrl: string | undefined): string { try {
if (!imageUrl || imageUrl.trim() === '') { setLoading(true);
return this.getRandomDefaultImage(); setError(null);
}
// Load both courses and upcoming events concurrently
// If it's already a full URL, return as-is const [fetchedCourses, fetchedEvents] = await Promise.all([
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { educationService.getActiveCourses(),
return imageUrl; upcomingEventsService.getActiveUpcomingEvents()
} ]);
// If it's a relative URL, prepend the backend API URL setCourses(fetchedCourses);
const cleanUrl = imageUrl.startsWith('/') ? imageUrl.substring(1) : imageUrl; setUpcomingEvents(fetchedEvents);
return `${this.apiBaseUrl}/${cleanUrl}`; } catch (err) {
} setError('Failed to load courses and events. Please try again later.');
console.error('Error loading data:', err);
async getActiveCourses(): Promise<Course[]> { } finally {
try { setLoading(false);
const response = await fetch(`${this.apiBaseUrl}/api/courses/active`, { }
cache: 'no-store'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const apiCourses: ApiCourse[] = await response.json();
return this.transformApiCoursesToCourses(apiCourses);
} catch (error) {
console.error('Error fetching courses:', error);
return this.getFallbackCourses();
}
}
async getCourseById(id: number): Promise<Course | null> {
try {
const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
const response = await fetch(`${this.apiBaseUrl}/api/courses/${id}`, {
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
cache: 'no-store'
});
if (!response.ok) {
if (response.status === 404) return null;
throw new Error(`HTTP error! status: ${response.status}`);
}
const apiCourse: ApiCourse = await response.json();
return this.transformApiCourseToCourse(apiCourse);
} catch (error) {
console.error(`Error fetching course ${id}:`, error);
return null;
}
}
async submitApplication(applicationData: CourseApplicationData): Promise<boolean> {
try {
const response = await fetch(`${this.apiBaseUrl}/api/course-applications`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(applicationData),
});
return response.ok;
} catch (error) {
console.error('Error submitting application:', error);
return false;
}
}
private transformApiCoursesToCourses(apiCourses: ApiCourse[]): Course[] {
return apiCourses.map(apiCourse => this.transformApiCourseToCourse(apiCourse));
}
private transformApiCourseToCourse(apiCourse: ApiCourse): Course {
return {
id: apiCourse.id.toString(),
title: apiCourse.title,
description: apiCourse.description,
duration: apiCourse.duration,
seats: apiCourse.seats,
category: apiCourse.category,
level: apiCourse.level,
instructor: apiCourse.instructor,
price: apiCourse.price || 'N/A',
startDate: apiCourse.startDate || '',
image: this.getFullImageUrl(apiCourse.imageUrl),
eligibility: apiCourse.eligibility || [],
objectives: apiCourse.objectives || []
}; };
}
private getRandomDefaultImage(): string { if (!mounted) {
const defaultImages = [ return null;
"https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center", }
"https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=300&fit=crop&crop=center",
"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=300&fit=crop&crop=center",
"https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=400&h=300&fit=crop&crop=center",
"https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=400&h=300&fit=crop&crop=center",
"https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&crop=center"
];
return defaultImages[Math.floor(Math.random() * defaultImages.length)];
}
private getFallbackCourses(): Course[] { // Filter courses based on category and search
return [ const filteredCourses = courses.filter(course => {
{ const matchesCategory = selectedCategory === 'All' || course.category === selectedCategory;
id: '1', const matchesSearch = !searchQuery.trim() ||
title: "ATLS® (Advanced Trauma Life Support)", course.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
description: "Eligibility: MBBS + internship complete. Last Course: Aug 31 Sep 2, 2023 (60 doctors certified). Next Schedule: [#Incomplete Date TBD]", course.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
duration: "3 Days", course.instructor.toLowerCase().includes(searchQuery.toLowerCase());
seats: 60,
category: "Certification", return matchesCategory && matchesSearch;
level: "Professional", });
image: "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
instructor: "Trauma Faculty Team",
price: "N/A",
startDate: "2023-08-31",
eligibility: ["MBBS + internship complete"],
objectives: ["Advanced trauma life support skills", "Emergency trauma management"]
},
{
id: '2',
title: "ATCN® (Advanced Trauma Care for Nurses)",
description: "First Course: Apr 11-13, 2024 (manikin-based training). Participants: 40 critical care nurses from CMC and partner hospitals. Next Batch: [#Incomplete Date TBD]",
duration: "3 Days",
seats: 40,
category: "Training",
level: "Professional",
image: "https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=300&fit=crop&crop=center",
instructor: "Nursing Faculty Team",
price: "N/A",
startDate: "2024-04-11",
eligibility: ["Registered Nurse", "Critical care experience preferred"],
objectives: ["Advanced trauma nursing skills", "Manikin-based training proficiency"]
},
{
id: '3',
title: "Trauma First Responder Program",
description: "Partners: RCPSG Hope Foundation, local colleges. Locations: Walajapet, Auxilium College—250 students trained. Curriculum: CPR, airway support, bleeding control, scene assessment.",
duration: "Varies",
seats: 250,
category: "Workshop",
level: "Beginner",
image: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=300&fit=crop&crop=center",
instructor: "Community Trainers",
price: "N/A",
startDate: "2023-01-01",
eligibility: ["Students", "Community members"],
objectives: ["CPR proficiency", "Basic trauma response", "Scene safety assessment"]
},
{
id: '4',
title: "FNB in Trauma Surgery",
description: "3-year structured training program in acute surgery, ICU management, and research. Open to MS-qualified surgeons seeking specialized trauma surgery expertise.",
duration: "3 Years",
seats: 8,
category: "Certification",
level: "Advanced",
image: "https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=400&h=300&fit=crop&crop=center",
instructor: "Senior Trauma Surgeons",
price: "N/A",
startDate: "2025-07-01",
eligibility: ["MS qualification in Surgery", "Valid medical license"],
objectives: ["Advanced trauma surgery skills", "ICU management", "Research methodology"]
},
{
id: '5',
title: "Observerships & Electives",
description: "4-8 week clinical blocks for national and international residents. Includes ATLS® course access and hands-on trauma experience. Application by email required.",
duration: "4-8 Weeks",
seats: 20,
category: "Training",
level: "Intermediate",
image: "https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=400&h=300&fit=crop&crop=center",
instructor: "Clinical Faculty",
price: "N/A",
startDate: "2025-01-15",
eligibility: ["Medical residency status", "Valid medical credentials"],
objectives: ["Clinical observation skills", "Hands-on trauma experience", "ATLS certification"]
},
{
id: '6',
title: "Nursing Skills Lab",
description: "Trauma-focused skills laboratory sessions open quarterly (Q2, Q4). Includes chest tube insertion, airway management drills, and EFAST simulation training.",
duration: "2 Days",
seats: 30,
category: "Workshop",
level: "Intermediate",
image: "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&crop=center",
instructor: "Nursing Skills Faculty",
price: "N/A",
startDate: "2025-04-01",
eligibility: ["Licensed nurse", "Basic trauma knowledge"],
objectives: ["Chest tube insertion", "Airway management", "EFAST simulation"]
}
];
}
}
export const educationService = new EducationService(); const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mx-auto mb-4"></div>
<p className="text-gray-600">Loading courses...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen">
{/* Header Section */}
<section
className="py-4 relative overflow-hidden"
style={{ backgroundColor: '#f4f4f4' }}
>
<div className="max-w-7xl mx-auto px-4 relative z-10">
{/* Breadcrumb */}
<nav className="flex items-center space-x-2 text-sm mb-6">
<Link
href="/"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Home
</Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}>
Education & Training
</span>
</nav>
{/* Page Header */}
<div className="mb-2">
<h1 className="text-4xl font-bold mb-4" style={{ color: '#012068' }}>
Education & Training
</h1>
<p className="text-lg max-w-3xl leading-relaxed" style={{ color: '#333' }}>
Advance your trauma care expertise with structured training programs for doctors, nurses, students, and community partners.
</p>
</div>
</div>
</section>
{/* Error Message */}
{error && (
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800 text-sm">{error}</p>
<button
onClick={loadData}
className="text-red-600 underline text-sm mt-2"
>
Try again
</button>
</div>
</div>
)}
{/* Upcoming Training Section with Dynamic Cards */}
<section className="py-8" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 text-center" style={{ color: '#012068' }}>
Upcoming Training Programs
</h2>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{upcomingEvents.length > 0 ? (
upcomingEvents.map((event) => (
<div key={event.id} className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
<div className="flex items-center mb-4">
<div>
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
{event.title}
</h3>
<div className="flex items-center text-sm">
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
<span style={{ color: '#666' }}>{event.schedule}</span>
</div>
</div>
</div>
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
{event.description}
</p>
</div>
))
) : (
// Fallback to static cards if no events are loaded
<>
<div className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
<div className="flex items-center mb-4">
<div>
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
Simulation-based Team Drills
</h3>
<div className="flex items-center text-sm">
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
<span>Q3 2025</span>
</div>
</div>
</div>
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
Hands-on simulation training designed to improve team coordination and emergency response in high-pressure trauma situations.
</p>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
<div className="flex items-center mb-4">
<div>
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
Online Webinar Series
</h3>
<div className="flex items-center text-sm">
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
<span>Monthly Sessions</span>
</div>
</div>
</div>
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
Monthly online sessions covering trauma ethics, young doctor support, and professional development in emergency medicine.
</p>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
<div className="flex items-center mb-4">
<div>
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
Community Education
</h3>
<div className="flex items-center text-sm">
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
<span>Ongoing</span>
</div>
</div>
</div>
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
Road safety fairs and school education sessions to promote trauma prevention and basic first aid awareness in the community.
</p>
</div>
</>
)}
</div>
</div>
</section>
{/* Filter & Search */}
<section className="py-8" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
{/* Category Filter */}
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${selectedCategory === category
? 'text-white'
: 'border-2'
}`}
style={{
backgroundColor: selectedCategory === category ? '#012068' : 'transparent',
borderColor: '#012068',
color: selectedCategory === category ? 'white' : '#012068'
}}
>
{category}
</button>
))}
</div>
{/* Search */}
<div className="relative">
<input
type="text"
placeholder="Search programs..."
value={searchQuery}
onChange={handleSearchChange}
className="border-2 rounded-lg px-4 py-2 pl-4 pr-10 text-sm focus:outline-none w-64"
style={{ borderColor: '#012068', color:'#333' }}
/>
<button className="absolute inset-y-0 right-0 flex items-center px-3 rounded-lg"
style={{ backgroundColor: '#012068' }}>
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
</div>
{/* Results Info */}
{(selectedCategory !== 'All' || searchQuery.trim()) && (
<div className="mb-4">
<p className="text-sm text-gray-600">
{filteredCourses.length === 0
? 'No courses found matching your criteria.'
: `Showing ${filteredCourses.length} of ${courses.length} course${courses.length !== 1 ? 's' : ''}`
}
</p>
</div>
)}
</div>
</section>
{/* Courses Grid */}
<section className="pb-12" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4">
{filteredCourses.length === 0 && !error ? (
<div className="text-center py-12">
<p className="text-gray-500 text-lg mb-4">
{searchQuery.trim() || selectedCategory !== 'All'
? 'No courses match your search criteria.'
: 'No courses available at the moment.'
}
</p>
{(searchQuery.trim() || selectedCategory !== 'All') && (
<button
onClick={() => {
setSearchQuery('');
setSelectedCategory('All');
}}
className="px-4 py-2 text-sm border border-blue-900 text-blue-900 rounded hover:bg-blue-50"
>
Clear Filters
</button>
)}
</div>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 auto-rows-fr">
{filteredCourses.map((course) => (
<Link
key={course.id}
href={`/education-training/course-detail?id=${course.id}`}
className="group bg-white rounded-lg overflow-hidden border border-gray-300 hover:shadow-xl transition-all duration-300 flex flex-col h-full cursor-pointer"
>
{/* Image */}
<div className="relative h-48 overflow-hidden flex-shrink-0">
<Image
src={course.image}
alt={course.title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
{/* Content */}
<div className="flex flex-col flex-grow">
<div className="p-6 flex-grow flex flex-col">
<h3
className="text-lg font-semibold mb-3 group-hover:opacity-70 transition-opacity duration-300 h-14 overflow-hidden"
style={{
color: '#012068',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical'
}}
>
{course.title}
</h3>
<p
className="text-sm leading-relaxed mb-4 flex-grow overflow-hidden"
style={{
color: '#666',
display: '-webkit-box',
WebkitLineClamp: 4,
WebkitBoxOrient: 'vertical'
}}
>
{course.description}
</p>
<div className="mt-auto">
<span
className="inline-block px-3 py-1 text-xs font-medium rounded-full border-2"
style={{
color: '#e64838',
backgroundColor: 'transparent'
}}
>
{course.category}
</span>
</div>
</div>
<div className="px-6 pb-6 pt-2 border-t border-gray-100 flex-shrink-0">
<div className="flex items-end mb-3">
<span className="text-sm font-medium truncate" style={{ color: '#012068' }}>
{course.instructor}
</span>
</div>
<div className="flex justify-between items-center mb-3 text-sm" style={{ color: '#666' }}>
<div className="flex items-center">
<Clock className="w-4 h-4 mr-1 flex-shrink-0" />
<span className="truncate">{course.duration}</span>
</div>
<div className="flex items-center ml-2">
<Users className="w-4 h-4 mr-1 flex-shrink-0" />
<span>{course.seats}</span>
</div>
</div>
<div className="flex justify-between items-center">
<div className="text-sm" style={{ color: '#666' }}>
{course.startDate ? `Starts: ${new Date(course.startDate).toLocaleDateString()}` : 'Contact for dates'}
</div>
</div>
</div>
</div>
</Link>
))}
</div>
)}
</div>
</section>
{/* CTA */}
<section className="py-16" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-4xl mx-auto px-4 text-center">
<Award className="w-12 h-12 mx-auto mb-6" style={{ color: '#e64838' }} />
<h2 className="text-3xl font-bold mb-4" style={{ color: '#012068' }}>
Ready to Advance Your Trauma Care Expertise?
</h2>
<p className="text-lg mb-8 max-w-2xl mx-auto" style={{ color: '#666' }}>
Join our structured training programs designed to empower healthcare professionals, nurses, and community responders with critical trauma care skills.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/contact"
className="inline-block px-6 py-3 text-sm font-medium rounded hover:opacity-90 transition-opacity duration-300"
style={{
backgroundColor: '#012068',
color: 'white'
}}
>
Contact Admissions
</Link>
<Link
href="/#"
className="inline-block px-6 py-3 text-sm font-medium rounded border-2 hover:opacity-70 transition-opacity duration-300"
style={{
borderColor: '#012068',
color: '#012068'
}}
>
Download Brochure
</Link>
</div>
</div>
</section>
</div>
);
};
export default EducationTraining;

View File

@ -48,9 +48,27 @@ export interface CourseApplicationData {
class EducationService { class EducationService {
private apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'; private apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
// Helper method to convert relative URLs to absolute URLs
private getFullImageUrl(imageUrl: string | undefined): string {
if (!imageUrl || imageUrl.trim() === '') {
return this.getRandomDefaultImage();
}
// If it's already a full URL, return as-is
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// If it's a relative URL, prepend the backend API URL
const cleanUrl = imageUrl.startsWith('/') ? imageUrl.substring(1) : imageUrl;
return `${this.apiBaseUrl}/${cleanUrl}`;
}
async getActiveCourses(): Promise<Course[]> { async getActiveCourses(): Promise<Course[]> {
try { try {
const response = await fetch(`${this.apiBaseUrl}/api/courses/active`); const response = await fetch(`${this.apiBaseUrl}/api/courses/active`, {
cache: 'no-store'
});
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -58,33 +76,33 @@ class EducationService {
return this.transformApiCoursesToCourses(apiCourses); return this.transformApiCoursesToCourses(apiCourses);
} catch (error) { } catch (error) {
console.error('Error fetching courses:', error); console.error('Error fetching courses:', error);
return this.getFallbackCourses(); // Return fallback data if API fails return this.getFallbackCourses();
} }
} }
async getCourseById(id: number): Promise<Course | null> { async getCourseById(id: number): Promise<Course | null> {
try { try {
const token = localStorage.getItem('authToken'); // Or from cookies/session const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
const response = await fetch(`${this.apiBaseUrl}/api/courses/${id}`, { const response = await fetch(`${this.apiBaseUrl}/api/courses/${id}`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}),
}, },
}); cache: 'no-store'
});
if (!response.ok) { if (!response.ok) {
if (response.status === 404) return null; if (response.status === 404) return null;
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
}
const apiCourse: ApiCourse = await response.json();
return this.transformApiCourseToCourse(apiCourse);
} catch (error) {
console.error(`Error fetching course ${id}:`, error);
return null;
} }
const apiCourse: ApiCourse = await response.json();
return this.transformApiCourseToCourse(apiCourse);
} catch (error) {
console.error(`Error fetching course ${id}:`, error);
return null;
} }
}
async submitApplication(applicationData: CourseApplicationData): Promise<boolean> { async submitApplication(applicationData: CourseApplicationData): Promise<boolean> {
try { try {
@ -119,29 +137,12 @@ class EducationService {
instructor: apiCourse.instructor, instructor: apiCourse.instructor,
price: apiCourse.price || 'N/A', price: apiCourse.price || 'N/A',
startDate: apiCourse.startDate || '', startDate: apiCourse.startDate || '',
image: this.getImageUrl(apiCourse.imageUrl), image: this.getFullImageUrl(apiCourse.imageUrl),
eligibility: apiCourse.eligibility || [], eligibility: apiCourse.eligibility || [],
objectives: apiCourse.objectives || [] objectives: apiCourse.objectives || []
}; };
} }
private getImageUrl(imageUrl?: string): string {
if (imageUrl) {
// If imageUrl starts with /uploads/, prepend the full API path
if (imageUrl.startsWith('/uploads/')) {
return `${this.apiBaseUrl}/api/files${imageUrl}`; // This adds /api/files before /uploads/
}
// If it's already a full URL, return as is
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// Otherwise, assume it's a relative path and prepend base URL with API path
return `${this.apiBaseUrl}/api/files/${imageUrl}`;
}
// Return random default image if no imageUrl provided
return this.getRandomDefaultImage();
}
private getRandomDefaultImage(): string { private getRandomDefaultImage(): string {
const defaultImages = [ const defaultImages = [
"https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center", "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
@ -155,7 +156,6 @@ class EducationService {
} }
private getFallbackCourses(): Course[] { private getFallbackCourses(): Course[] {
// Return the original hardcoded courses as fallback
return [ return [
{ {
id: '1', id: '1',