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>
);
} }
return ( // If it's a relative URL, prepend the backend API URL
<div className="min-h-screen"> const cleanUrl = imageUrl.startsWith('/') ? imageUrl.substring(1) : imageUrl;
{/* Header Section */} return `${this.apiBaseUrl}/${cleanUrl}`;
<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 */} async getActiveCourses(): Promise<Course[]> {
<div className="mb-2"> try {
<h1 className="text-4xl font-bold mb-4" style={{ color: '#012068' }}> const response = await fetch(`${this.apiBaseUrl}/api/courses/active`, {
Education & Training cache: 'no-store'
</h1> });
<p className="text-lg max-w-3xl leading-relaxed" style={{ color: '#333' }}> if (!response.ok) {
Advance your trauma care expertise with structured training programs for doctors, nurses, students, and community partners. throw new Error(`HTTP error! status: ${response.status}`);
</p> }
</div> const apiCourses: ApiCourse[] = await response.json();
</div> return this.transformApiCoursesToCourses(apiCourses);
</section> } catch (error) {
console.error('Error fetching courses:', error);
return this.getFallbackCourses();
}
}
{/* Error Message */} async getCourseById(id: number): Promise<Course | null> {
{error && ( try {
<div className="max-w-7xl mx-auto px-4 py-4"> const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
<div className="bg-red-50 border border-red-200 rounded-lg p-4"> const response = await fetch(`${this.apiBaseUrl}/api/courses/${id}`, {
<p className="text-red-800 text-sm">{error}</p> headers: {
<button 'Content-Type': 'application/json',
onClick={loadData} ...(token ? { Authorization: `Bearer ${token}` } : {}),
className="text-red-600 underline text-sm mt-2" },
> cache: 'no-store'
Try again });
</button>
</div>
</div>
)}
{/* Upcoming Training Section with Dynamic Cards */} if (!response.ok) {
<section className="py-8" style={{ backgroundColor: '#fff' }}> if (response.status === 404) return null;
<div className="max-w-7xl mx-auto px-4"> throw new Error(`HTTP error! status: ${response.status}`);
<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' }}> const apiCourse: ApiCourse = await response.json();
<div className="flex items-center mb-4"> return this.transformApiCourseToCourse(apiCourse);
<div> } catch (error) {
<h3 className="text-lg font-bold" style={{ color: '#012068' }}> console.error(`Error fetching course ${id}:`, error);
Online Webinar Series return null;
</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' }}> 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',
Community Education 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>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 */} return response.ok;
<section className="py-8" style={{ backgroundColor: '#fff' }}> } catch (error) {
<div className="max-w-7xl mx-auto px-4"> console.error('Error submitting application:', error);
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8"> return false;
{/* 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 */} private transformApiCoursesToCourses(apiCourses: ApiCourse[]): Course[] {
<div className="relative"> return apiCourses.map(apiCourse => this.transformApiCourseToCourse(apiCourse));
<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 */} private transformApiCourseToCourse(apiCourse: ApiCourse): Course {
{(selectedCategory !== 'All' || searchQuery.trim()) && ( return {
<div className="mb-4"> id: apiCourse.id.toString(),
<p className="text-sm text-gray-600"> title: apiCourse.title,
{filteredCourses.length === 0 description: apiCourse.description,
? 'No courses found matching your criteria.' duration: apiCourse.duration,
: `Showing ${filteredCourses.length} of ${courses.length} course${courses.length !== 1 ? 's' : ''}` seats: apiCourse.seats,
} category: apiCourse.category,
</p> level: apiCourse.level,
</div> instructor: apiCourse.instructor,
)} price: apiCourse.price || 'N/A',
</div> startDate: apiCourse.startDate || '',
</section> image: this.getFullImageUrl(apiCourse.imageUrl),
eligibility: apiCourse.eligibility || [],
objectives: apiCourse.objectives || []
};
}
{/* Courses Grid */} private getRandomDefaultImage(): string {
<section className="pb-12" style={{ backgroundColor: '#fff' }}> const defaultImages = [
<div className="max-w-7xl mx-auto px-4"> "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
{filteredCourses.length === 0 && !error ? ( "https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=300&fit=crop&crop=center",
<div className="text-center py-12"> "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=300&fit=crop&crop=center",
<p className="text-gray-500 text-lg mb-4"> "https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=400&h=300&fit=crop&crop=center",
{searchQuery.trim() || selectedCategory !== 'All' "https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=400&h=300&fit=crop&crop=center",
? 'No courses match your search criteria.' "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&crop=center"
: 'No courses available at the moment.' ];
} return defaultImages[Math.floor(Math.random() * defaultImages.length)];
</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 */} private getFallbackCourses(): Course[] {
<div className="flex flex-col flex-grow"> return [
<div className="p-6 flex-grow flex flex-col"> {
<h3 id: '1',
className="text-lg font-semibold mb-3 group-hover:opacity-70 transition-opacity duration-300 h-14 overflow-hidden" title: "ATLS® (Advanced Trauma Life Support)",
style={{ description: "Eligibility: MBBS + internship complete. Last Course: Aug 31 Sep 2, 2023 (60 doctors certified). Next Schedule: [#Incomplete Date TBD]",
color: '#012068', duration: "3 Days",
display: '-webkit-box', seats: 60,
WebkitLineClamp: 2, category: "Certification",
WebkitBoxOrient: 'vertical' level: "Professional",
}} image: "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
> instructor: "Trauma Faculty Team",
{course.title} price: "N/A",
</h3> startDate: "2023-08-31",
<p eligibility: ["MBBS + internship complete"],
className="text-sm leading-relaxed mb-4 flex-grow overflow-hidden" objectives: ["Advanced trauma life support skills", "Emergency trauma management"]
style={{ },
color: '#666', {
display: '-webkit-box', id: '2',
WebkitLineClamp: 4, title: "ATCN® (Advanced Trauma Care for Nurses)",
WebkitBoxOrient: 'vertical' 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,
{course.description} category: "Training",
</p> level: "Professional",
<div className="mt-auto"> image: "https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=300&fit=crop&crop=center",
<span instructor: "Nursing Faculty Team",
className="inline-block px-3 py-1 text-xs font-medium rounded-full border-2" price: "N/A",
style={{ startDate: "2024-04-11",
color: '#e64838', eligibility: ["Registered Nurse", "Critical care experience preferred"],
backgroundColor: 'transparent' objectives: ["Advanced trauma nursing skills", "Manikin-based training proficiency"]
}} },
> {
{course.category} id: '3',
</span> title: "Trauma First Responder Program",
</div> description: "Partners: RCPSG Hope Foundation, local colleges. Locations: Walajapet, Auxilium College—250 students trained. Curriculum: CPR, airway support, bleeding control, scene assessment.",
</div> 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"]
}
];
}
}
<div className="px-6 pb-6 pt-2 border-t border-gray-100 flex-shrink-0"> export const educationService = new EducationService();
<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;