Image error resolve

This commit is contained in:
2025-10-10 17:46:04 +05:30
parent 535612899a
commit 5860d38d95

View File

@ -1,417 +1,254 @@
// components/EducationTraining.tsx // services/educationService.ts
'use client'; export interface ApiCourse {
import { useState, useEffect } from 'react'; id: number;
import Image from 'next/image'; title: string;
import Link from 'next/link'; description: string;
import { ChevronRight, Clock, Users, Award, Calendar } from 'lucide-react'; duration: string;
import { educationService, Course } from '../../services/educationService'; seats: number;
import { upcomingEventsService, UpcomingEvent } from '../../services/upcomingEventsService'; category: string;
level: string;
instructor: string;
price?: string;
startDate?: string;
imageUrl?: string;
eligibility: string[];
objectives: string[];
isActive: boolean;
createdDate?: string;
updatedDate?: string;
}
const EducationTraining: React.FC = () => { export interface Course {
const [mounted, setMounted] = useState(false); id: string;
const [courses, setCourses] = useState<Course[]>([]); title: string;
const [upcomingEvents, setUpcomingEvents] = useState<UpcomingEvent[]>([]); description: string;
const [loading, setLoading] = useState(true); duration: string;
const [error, setError] = useState<string | null>(null); seats: number;
const [selectedCategory, setSelectedCategory] = useState<string>('All'); category: string;
const [searchQuery, setSearchQuery] = useState<string>(''); level: string;
instructor: string;
price: string;
startDate: string;
image: string;
eligibility: string[];
objectives: string[];
}
const categories = ['All', 'Certification', 'Training', 'Workshop', 'Fellowship']; export interface CourseApplicationData {
courseId: number;
fullName: string;
email: string;
phone: string;
qualification: string;
experience?: string;
coverLetter?: string;
resumeUrl?: string;
}
useEffect(() => { class EducationService {
setMounted(true); private apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
loadData();
}, []);
const loadData = async () => { // Helper method to convert relative URLs to absolute URLs
try { private getFullImageUrl(imageUrl: string | undefined): string {
setLoading(true); if (!imageUrl || imageUrl.trim() === '') {
setError(null); return this.getRandomDefaultImage();
// Load both courses and upcoming events concurrently
const [fetchedCourses, fetchedEvents] = await Promise.all([
educationService.getActiveCourses(),
upcomingEventsService.getActiveUpcomingEvents()
]);
setCourses(fetchedCourses);
setUpcomingEvents(fetchedEvents);
} catch (err) {
setError('Failed to load courses and events. Please try again later.');
console.error('Error loading data:', err);
} finally {
setLoading(false);
}
};
if (!mounted) {
return null;
} }
// Filter courses based on category and search // If it's already a full URL, return as-is
const filteredCourses = courses.filter(course => { if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
const matchesCategory = selectedCategory === 'All' || course.category === selectedCategory; return imageUrl;
const matchesSearch = !searchQuery.trim() ||
course.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
course.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
course.instructor.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
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>
);
} }
// If it's a relative URL, prepend the backend API URL
const cleanUrl = imageUrl.startsWith('/') ? imageUrl.substring(1) : imageUrl;
return `${this.apiBaseUrl}/${cleanUrl}`;
}
return ( async getActiveCourses(): Promise<Course[]> {
<div className="min-h-screen"> try {
{/* Header Section */} const response = await fetch(`${this.apiBaseUrl}/api/courses/active`, {
<section cache: 'no-store'
className="py-4 relative overflow-hidden" });
style={{ backgroundColor: '#f4f4f4' }} if (!response.ok) {
> throw new Error(`HTTP error! status: ${response.status}`);
<div className="max-w-7xl mx-auto px-4 relative z-10"> }
{/* Breadcrumb */} const apiCourses: ApiCourse[] = await response.json();
<nav className="flex items-center space-x-2 text-sm mb-6"> return this.transformApiCoursesToCourses(apiCourses);
<Link } catch (error) {
href="/" console.error('Error fetching courses:', error);
className="hover:opacity-70 transition-opacity duration-200" return this.getFallbackCourses();
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 */} async getCourseById(id: number): Promise<Course | null> {
<div className="mb-2"> try {
<h1 className="text-4xl font-bold mb-4" style={{ color: '#012068' }}> const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
Education & Training const response = await fetch(`${this.apiBaseUrl}/api/courses/${id}`, {
</h1> headers: {
<p className="text-lg max-w-3xl leading-relaxed" style={{ color: '#333' }}> 'Content-Type': 'application/json',
Advance your trauma care expertise with structured training programs for doctors, nurses, students, and community partners. ...(token ? { Authorization: `Bearer ${token}` } : {}),
</p> },
</div> cache: 'no-store'
</div> });
</section>
{/* Error Message */} if (!response.ok) {
{error && ( if (response.status === 404) return null;
<div className="max-w-7xl mx-auto px-4 py-4"> throw new Error(`HTTP error! status: ${response.status}`);
<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 */} const apiCourse: ApiCourse = await response.json();
<section className="py-8" style={{ backgroundColor: '#fff' }}> return this.transformApiCourseToCourse(apiCourse);
<div className="max-w-7xl mx-auto px-4"> } catch (error) {
<h2 className="text-3xl font-bold mb-8 text-center" style={{ color: '#012068' }}> console.error(`Error fetching course ${id}:`, error);
Upcoming Training Programs return null;
</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' }}> async submitApplication(applicationData: CourseApplicationData): Promise<boolean> {
<div className="flex items-center mb-4"> try {
<div> const response = await fetch(`${this.apiBaseUrl}/api/course-applications`, {
<h3 className="text-lg font-bold" style={{ color: '#012068' }}> method: 'POST',
Online Webinar Series headers: {
</h3> 'Content-Type': 'application/json',
<div className="flex items-center text-sm"> },
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} /> body: JSON.stringify(applicationData),
<span>Monthly Sessions</span> });
</div>
</div> return response.ok;
</div> } catch (error) {
<p className="text-sm leading-relaxed" style={{ color: '#666' }}> console.error('Error submitting application:', error);
Monthly online sessions covering trauma ethics, young doctor support, and professional development in emergency medicine. return false;
</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' }}> private transformApiCoursesToCourses(apiCourses: ApiCourse[]): Course[] {
<div className="flex items-center mb-4"> return apiCourses.map(apiCourse => this.transformApiCourseToCourse(apiCourse));
<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 */} private transformApiCourseToCourse(apiCourse: ApiCourse): Course {
<section className="py-8" style={{ backgroundColor: '#fff' }}> return {
<div className="max-w-7xl mx-auto px-4"> id: apiCourse.id.toString(),
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8"> title: apiCourse.title,
{/* Category Filter */} description: apiCourse.description,
<div className="flex flex-wrap gap-2"> duration: apiCourse.duration,
{categories.map((category) => ( seats: apiCourse.seats,
<button category: apiCourse.category,
key={category} level: apiCourse.level,
onClick={() => setSelectedCategory(category)} instructor: apiCourse.instructor,
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${selectedCategory === category price: apiCourse.price || 'N/A',
? 'text-white' startDate: apiCourse.startDate || '',
: 'border-2' image: this.getFullImageUrl(apiCourse.imageUrl),
}`} eligibility: apiCourse.eligibility || [],
style={{ objectives: apiCourse.objectives || []
backgroundColor: selectedCategory === category ? '#012068' : 'transparent', };
borderColor: '#012068', }
color: selectedCategory === category ? 'white' : '#012068'
}}
>
{category}
</button>
))}
</div>
{/* Search */} private getRandomDefaultImage(): string {
<div className="relative"> const defaultImages = [
<input "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
type="text" "https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=300&fit=crop&crop=center",
placeholder="Search programs..." "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=300&fit=crop&crop=center",
value={searchQuery} "https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=400&h=300&fit=crop&crop=center",
onChange={handleSearchChange} "https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=400&h=300&fit=crop&crop=center",
className="border-2 rounded-lg px-4 py-2 pl-4 pr-10 text-sm focus:outline-none w-64" "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&crop=center"
style={{ borderColor: '#012068', color:'#333' }} ];
/> return defaultImages[Math.floor(Math.random() * defaultImages.length)];
<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 */} private getFallbackCourses(): Course[] {
{(selectedCategory !== 'All' || searchQuery.trim()) && ( return [
<div className="mb-4"> {
<p className="text-sm text-gray-600"> id: '1',
{filteredCourses.length === 0 title: "ATLS® (Advanced Trauma Life Support)",
? 'No courses found matching your criteria.' description: "Eligibility: MBBS + internship complete. Last Course: Aug 31 Sep 2, 2023 (60 doctors certified). Next Schedule: [#Incomplete Date TBD]",
: `Showing ${filteredCourses.length} of ${courses.length} course${courses.length !== 1 ? 's' : ''}` duration: "3 Days",
} seats: 60,
</p> category: "Certification",
</div> level: "Professional",
)} image: "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
</div> instructor: "Trauma Faculty Team",
</section> 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"]
}
];
}
}
{/* Courses Grid */} export const educationService = new EducationService();
<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;