Further updates on 03-11-2025

This commit is contained in:
2025-11-03 13:45:52 +05:30
parent 5481f2e38a
commit 03f6e8432e
26 changed files with 2299 additions and 848 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 353 KiB

View File

@ -4,9 +4,6 @@ import Process from "../../components/about/process";
import Breadcrumb from "../../components/about/AboutBreadcrumb"; import Breadcrumb from "../../components/about/AboutBreadcrumb";
import Introduction from "../../components/about/Introduction"; import Introduction from "../../components/about/Introduction";
import MissionVision from "../../components/about/MissionVision"; import MissionVision from "../../components/about/MissionVision";
import Services from "../../components/about/Services";
import StatisticsTiles from "../../components/about/StatisticsTiles";
import PatientCareCards from "../../components/about/PatientCareCards";
export default function Home() { export default function Home() {
const breadcrumbItems = [ const breadcrumbItems = [
@ -24,9 +21,9 @@ export default function Home() {
<Introduction /> <Introduction />
<MissionVision /> <MissionVision />
<Process /> <Process />
<Services /> {/* <Services />
<StatisticsTiles /> <StatisticsTiles />
<PatientCareCards /> <PatientCareCards /> */}
<Footer /> <Footer />
</> </>
); );

View File

@ -1,13 +1,13 @@
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
import { Footer } from "../../components/Layouts/Footer" import { Footer } from "../../components/Layouts/Footer"
import EducationTraining from "../../components/education/EducationTraining"; import AcademicResearch from "@/components/education/EducationTraining";
export default function contact() { export default function contact() {
return ( return (
<> <>
<Header /> <Header />
<EducationTraining/> <AcademicResearch/>
<Footer /> <Footer />
</> </>
); );

View File

@ -2,6 +2,7 @@ import Header from "../components/Layouts/Header"; // Adjust path based on your
import { Footer } from "../components/Layouts/Footer"; import { Footer } from "../components/Layouts/Footer";
import HeroSection from "../components/home/HeroSection"; import HeroSection from "../components/home/HeroSection";
import EventsSection from "../components/home/EventSection"; import EventsSection from "../components/home/EventSection";
import PatientTestimonialSlider from "../components/home/PatientTestimonialSlider";
export default function faculty() { export default function faculty() {
return ( return (
@ -9,6 +10,7 @@ export default function faculty() {
<Header /> <Header />
<HeroSection/> <HeroSection/>
<EventsSection/> <EventsSection/>
<PatientTestimonialSlider/>
<Footer /> <Footer />
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import { Footer } from "../../components/Layouts/Footer"
import ResearchComponent from "../../components/research/ResearchComponent"; import ResearchComponent from "../../components/research/ResearchComponent";
export default function contact() { export default function research() {
return ( return (
<> <>
<Header /> <Header />

29
src/app/services/page.tsx Normal file
View File

@ -0,0 +1,29 @@
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
import { Footer } from "../../components/Layouts/Footer";
import Breadcrumb from "../../components/services/Breadcrumb";
import Services from "../../components/about/Services";
import StatisticsTiles from "../../components/about/StatisticsTiles";
import PatientCareCards from "../../components/about/PatientCareCards";
export default function services() {
const breadcrumbItems = [
{ label: "Home", href: "/" },
{ label: "Services", isActive: true }
];
return (
<>
<Header />
<Breadcrumb
items={breadcrumbItems}
title="Services"
description="Explore our wide range of medical and surgical services at CMC Hospital, Ranipet campus—delivering compassionate care and advanced treatment for every patient."
/>
<Services />
<StatisticsTiles />
<PatientCareCards />
<Footer />
</>
);
}

View File

@ -0,0 +1,18 @@
'use client'
import { useParams } from 'next/navigation';
import Header from "../../../components/Layouts/Header";
import { Footer } from "../../../components/Layouts/Footer";
import TraumaTestimonials from "../../../components/home/TraumaTestimonials";
export default function TestimonialDetailPage() {
const params = useParams();
const testimonialId = params?.id;
return (
<>
<Header />
<TraumaTestimonials testimonialId={testimonialId as string} />
<Footer />
</>
);
}

View File

@ -0,0 +1,13 @@
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
import { Footer } from "../../components/Layouts/Footer";
import TestimonialsListing from "../../components/testimonials/TestimonialsListing";
export default function testimonial() {
return (
<>
<Header />
<TestimonialsListing/>
<Footer />
</>
);
}

View File

@ -38,9 +38,9 @@ export function Footer() {
<ul className="space-y-3"> <ul className="space-y-3">
{[ {[
{ label: 'About CMC', href: '/about' }, { label: 'About CMC', href: '/about' },
{ label: 'Faculty Team', href: '/teamMember' }, { label: 'Our Team', href: '/teamMember' },
{ label: 'Academics', href: '/education-training' }, { label: 'Academics & Research', href: '/education-training' },
{ label: 'Research', href: '/research'}, { label: 'Services', href: '/services'},
].map((link) => ( ].map((link) => (
<li key={link.label}> <li key={link.label}>
<a <a
@ -55,7 +55,7 @@ export function Footer() {
<ul className="space-y-3"> <ul className="space-y-3">
{[ {[
{ label: 'Events', href: '/events' }, { label: 'Events', href: '/events' },
{ label: 'Publications', href: '/publications'}, { label: 'Blogs', href: '/publications'},
{ label: 'Career', href: '/career' }, { label: 'Career', href: '/career' },
{ label: 'Contact us', href: '/contact' }, { label: 'Contact us', href: '/contact' },
].map((link) => ( ].map((link) => (

View File

@ -14,11 +14,11 @@ const Header = () => {
const menuItems = [ const menuItems = [
{ href: "/", label: "Home" }, { href: "/", label: "Home" },
{ href: "/about", label: "About" }, { href: "/about", label: "About" },
{ href: "/teamMember", label: "Faculty Team" }, { href: "/teamMember", label: "Our Team" },
{ href: "/education-training", label: "Academics" }, { href: "/education-training", label: "Academics & Research" },
{ href: "/research", label: "Research" }, { href: "/services", label: "Services" },
{ href: "/events", label: "Events" }, { href: "/events", label: "Events" },
{ href: "/publications", label: "Publications" }, { href: "/publications", label: "Blogs" },
{ href: "/career", label: "Career" }, { href: "/career", label: "Career" },
{ href: "/contact", label: "Contact Us" }, { href: "/contact", label: "Contact Us" },
]; ];
@ -32,7 +32,7 @@ const Header = () => {
<Link href="/" onClick={closeAllMenus} className="flex items-center"> <Link href="/" onClick={closeAllMenus} className="flex items-center">
<div className="relative w-60 sm:w-80 h-14 sm:h-18 mr-3 rounded overflow-hidden"> <div className="relative w-60 sm:w-80 h-14 sm:h-18 mr-3 rounded overflow-hidden">
<Image <Image
src="/images/cmclogo1.svg" src="/images/cmclogo2.svg"
alt="CMC Logo" alt="CMC Logo"
fill fill
className="object-fill" className="object-fill"
@ -43,12 +43,12 @@ const Header = () => {
</div> </div>
{/* Desktop Navigation */} {/* Desktop Navigation */}
<nav className="hidden lg:flex items-start space-x-8"> <nav className="hidden lg:flex items-center space-x-3 xl:space-x-5">
{menuItems.map((item, idx) => ( {menuItems.map((item, idx) => (
<Link <Link
key={idx} key={idx}
href={item.href} href={item.href}
className="text-blue-900 hover:text-red-600 transition-colors font-medium" className="text-blue-900 hover:text-red-600 transition-colors font-medium text-sm xl:text-base whitespace-nowrap"
onClick={closeAllMenus} onClick={closeAllMenus}
> >
{item.label} {item.label}
@ -69,7 +69,7 @@ const Header = () => {
priority priority
/> />
</div> </div>
<span className="text-xs sm:text-sm font-semibold text-blue-900 mt-1"> <span className="text-xs sm:text-sm font-semibold text-blue-900 mt-1 whitespace-nowrap">
H-2024-1431 H-2024-1431
</span> </span>
</div> </div>

View File

@ -3,11 +3,11 @@ import { Building2, Home } from 'lucide-react';
const PatientCareCards = () => { const PatientCareCards = () => {
return ( return (
<section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}> <section className="py-8 sm:py-12" style={{ backgroundColor: '#ffffff' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
{/* Inpatient Services */} {/* Inpatient Services */}
<div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' }}> <div className="rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' , backgroundColor: '#f4f4f4' }}>
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
<div <div
className="w-10 h-10 rounded-full flex items-center justify-center mr-3" className="w-10 h-10 rounded-full flex items-center justify-center mr-3"
@ -26,7 +26,7 @@ const PatientCareCards = () => {
</div> </div>
{/* Outpatient Services */} {/* Outpatient Services */}
<div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' }}> <div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068', backgroundColor: '#f4f4f4' }}>
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
<div <div
className="w-10 h-10 rounded-full flex items-center justify-center mr-3" className="w-10 h-10 rounded-full flex items-center justify-center mr-3"

View File

@ -5,7 +5,7 @@ const Services = () => {
const services = [ const services = [
{ {
icon: <Users className="w-6 h-6" />, icon: <Users className="w-6 h-6" />,
title: "Injury Prevention Outreach Activity", title: "Injury Prevention - Outreach Activity",
description: "Community-based programs including first responder training for laypersons, schools and workplace groups. Education in helmet use, bleeding control, safe transport practices and initial life-saving care." description: "Community-based programs including first responder training for laypersons, schools and workplace groups. Education in helmet use, bleeding control, safe transport practices and initial life-saving care."
}, },
{ {
@ -26,7 +26,7 @@ const Services = () => {
]; ];
return ( return (
<section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}> <section className="py-8 sm:py-12" style={{ backgroundColor: '#ffffff' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<h2 className="text-2xl sm:text-3xl font-semibold text-center mb-2" style={{ color: '#012068' }}> <h2 className="text-2xl sm:text-3xl font-semibold text-center mb-2" style={{ color: '#012068' }}>
Our Services Our Services
@ -38,7 +38,7 @@ const Services = () => {
{services.map((service, index) => ( {services.map((service, index) => (
<div <div
key={index} key={index}
className="bg-white rounded-lg p-6 border border-gray-300 hover:shadow-lg transition-shadow duration-300" className="rounded-lg p-6 border border-gray-300 hover:shadow-lg transition-shadow duration-300" style={{ backgroundColor: '#f4f4f4' }}
> >
<div className="flex items-start space-x-4"> <div className="flex items-start space-x-4">
<div <div

View File

@ -26,7 +26,7 @@ const StatisticsTiles = () => {
]; ];
return ( return (
<section className="py-8 sm:py-12 bg-white"> <section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<h2 <h2
className="text-2xl sm:text-3xl font-semibold text-center mb-8 sm:mb-12" className="text-2xl sm:text-3xl font-semibold text-center mb-8 sm:mb-12"
@ -39,7 +39,7 @@ const StatisticsTiles = () => {
<div <div
key={index} key={index}
className="border border-gray-300 rounded-lg p-5 h-full hover:shadow-lg transition-shadow duration-300" className="border border-gray-300 rounded-lg p-5 h-full hover:shadow-lg transition-shadow duration-300"
style={{ backgroundColor: '#f4f4f4' }} style={{ backgroundColor: '#ffffff' }}
> >
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">

View File

@ -1,45 +1,8 @@
'use client' 'use client'
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { getActiveMilestones, type Milestone } from "../../services/milestoneService";
export default function Process() { // Define CalendarIcon at the top level
const [activeStep, setActiveStep] = useState(-1);
const [scrollProgress, setScrollProgress] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const timelineRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
if (!containerRef.current || !timelineRef.current) return;
const container = containerRef.current;
const timeline = timelineRef.current;
const rect = container.getBoundingClientRect();
const timelineHeight = timeline.scrollHeight - timeline.clientHeight;
// Calculate when section enters viewport
if (rect.top <= 0 && rect.bottom >= window.innerHeight) {
// Section is sticky, scroll the timeline
const progress = Math.abs(rect.top) / (rect.height - window.innerHeight);
const clampedProgress = Math.max(0, Math.min(1, progress));
setScrollProgress(clampedProgress);
timeline.scrollTop = clampedProgress * timelineHeight;
} else if (rect.top > 0) {
// Before sticky
setScrollProgress(0);
timeline.scrollTop = 0;
} else {
// After sticky
setScrollProgress(1);
timeline.scrollTop = timelineHeight;
}
};
window.addEventListener('scroll', handleScroll);
handleScroll(); // Initial check
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const CalendarIcon = ( const CalendarIcon = (
<svg <svg
width={34} width={34}
@ -58,147 +21,149 @@ export default function Process() {
</svg> </svg>
); );
const steps = [ export default function Process() {
{ const [activeStep, setActiveStep] = useState(0);
number: "1", const [milestones, setMilestones] = useState<Milestone[]>([]);
title: "2017", const [loading, setLoading] = useState(true);
description: "Formation of Trauma Services", const containerRef = useRef<HTMLDivElement>(null);
icon: CalendarIcon, const timelineRef = useRef<HTMLDivElement>(null);
}, const stepRefs = useRef<HTMLDivElement[]>([]);
{
number: "2",
title: "2020",
description: "Establishment of the Department of Trauma Surgery under Dr. Sukria Nayak",
icon: CalendarIcon,
},
{
number: "3",
title: "May 2021",
description: "Development of Trauma Protocols (MHTP, TROB, TRIP, Airway, Obstetric Trauma)",
icon: CalendarIcon,
},
{
number: "4",
title: "June 2022",
description: "Inauguration of CMC Ranipet Campus: Level 1 Trauma Centre",
icon: CalendarIcon,
},
{
number: "5",
title: "November 2022",
description: "Formation of T-ReCS (Trauma Registry CMC Pilot Study) under TCI-CMC Research Scholar Program",
icon: CalendarIcon,
},
{
number: "6",
title: "November 2022",
description: "Integration of Multidisciplinary Trauma Services",
icon: CalendarIcon,
},
{
number: "7",
title: "November 2022",
description: "Formation of Neurotrauma Unit",
icon: CalendarIcon,
},
{
number: "8",
title: "January 2023",
description: "Accreditation of Trauma Radiology Fellowship",
icon: CalendarIcon,
},
{
number: "9",
title: "August 2023",
description: "Inaugural ATLS® Course at CMC Vellore",
icon: CalendarIcon,
},
{
number: "10",
title: "September 2023",
description: "First Research Publication from the Department",
icon: CalendarIcon,
},
{
number: "11",
title: "October 2023",
description: "ACTraM Conference: Advances in Chest Trauma Management",
icon: CalendarIcon,
},
{
number: "12",
title: "October 2023",
description: "CME Cadaveric Workshop",
icon: CalendarIcon,
},
{
number: "13",
title: "December 2023",
description: "ICMR Trauma Quality Improvement Programme (TQIP) Initiated at CMC",
icon: CalendarIcon,
},
{
number: "14",
title: "February 2024",
description: "HOPE Grant & RCPSG Trauma First Responder Outreach Programme",
icon: CalendarIcon,
},
{
number: "15",
title: "April 2024",
description: "Launch of Masters of Trauma Online Lecture Series",
icon: CalendarIcon,
},
{
number: "16",
title: "April 2024",
description: "Inaugural ATCN® Course for Nurses",
icon: CalendarIcon,
},
{
number: "17",
title: "July 2024",
description: "MOU with GVK EMRI for Strengthening Pre-hospital Trauma Care",
icon: CalendarIcon,
},
{
number: "18",
title: "July 2024",
description: "Accreditation for FNB in Trauma and Acute Care Surgery",
icon: CalendarIcon,
},
{
number: "19",
title: "January 2025",
description: "Trauma Quality Workshop under ICMR-TQIP at CMC Vellore",
icon: CalendarIcon,
},
{
number: "20",
title: "March 2025",
description: "Formation of CMC Trauma Quality Improvement Committee",
icon: CalendarIcon,
},
{
number: "21",
title: "July 2025",
description: "Establishment of Trauma Orthopaedics Unit",
icon: CalendarIcon,
},
];
// Calculate extra height needed for scroll effect // Fetch milestones from API
const extraHeight = 2000; useEffect(() => {
async function fetchMilestones() {
try {
setLoading(true);
const data = await getActiveMilestones();
// Sort by milestoneDate in descending order (newest first)
const sortedData = [...data].sort((a, b) => {
const dateA = new Date(a.milestoneDate || 0).getTime();
const dateB = new Date(b.milestoneDate || 0).getTime();
return dateB - dateA; // Descending order (newest first)
});
setMilestones(sortedData);
} catch (error) {
console.error('Failed to fetch milestones:', error);
} finally {
setLoading(false);
}
}
fetchMilestones();
}, []);
// Transform API data to match the component's step format
const steps = milestones.map((milestone, index) => ({
number: (index + 1).toString(),
title: milestone.title,
description: milestone.description,
icon: CalendarIcon,
}));
// Initialize step refs array
useEffect(() => {
stepRefs.current = stepRefs.current.slice(0, steps.length);
}, [steps.length]);
// Calculate extra scroll height based on number of items
const extraHeight = steps.length > 0 ? Math.max(800, steps.length * 150) : 0;
useEffect(() => {
const handleScroll = () => {
if (!containerRef.current || !timelineRef.current) return;
const container = containerRef.current;
const timeline = timelineRef.current;
const rect = container.getBoundingClientRect();
const windowHeight = window.innerHeight;
const containerHeight = container.offsetHeight;
// Calculate when to start the scroll effect (when "Milestones" text reaches near top)
const scrollStartOffset = 100; // Adjust this to control when scrolling starts
if (rect.top <= scrollStartOffset && rect.bottom >= windowHeight) {
// Section is in the sticky zone
const scrollableHeight = containerHeight - windowHeight;
const scrolled = Math.abs(rect.top - scrollStartOffset);
const progress = Math.max(0, Math.min(1, scrolled / scrollableHeight));
// Scroll the timeline
const timelineHeight = timeline.scrollHeight - timeline.clientHeight;
timeline.scrollTop = progress * timelineHeight;
// Calculate which step should be active based on scroll position
const viewportCenter = timeline.scrollTop + timeline.clientHeight / 3;
let newActiveStep = 0;
stepRefs.current.forEach((ref, index) => {
if (ref) {
const stepTop = ref.offsetTop;
if (stepTop <= viewportCenter) {
newActiveStep = index;
}
}
});
setActiveStep(newActiveStep);
} else if (rect.top > scrollStartOffset) {
// Before sticky zone
timeline.scrollTop = 0;
setActiveStep(0);
} else {
// After sticky zone
const timelineHeight = timeline.scrollHeight - timeline.clientHeight;
timeline.scrollTop = timelineHeight;
setActiveStep(steps.length - 1);
}
};
window.addEventListener('scroll', handleScroll);
handleScroll(); // Initial check
return () => window.removeEventListener('scroll', handleScroll);
}, [steps.length]);
// Loading state
if (loading) {
return ( return (
<div <div className="bg-white py-8 sm:py-12 min-h-screen flex items-center justify-center">
ref={containerRef} <div className="text-center">
className="bg-white py-10 sm:py-10" <div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 mb-4" style={{ borderColor: "#012068" }}></div>
style={{ minHeight: `calc(100vh + ${extraHeight}px)` }} <p className="text-lg" style={{ color: "#012068" }}>Loading milestones...</p>
> </div>
<div className="sticky top-0 h-screen flex items-start pt-12 sm:pt-20"> </div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 w-full"> );
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-start"> }
// Empty state
if (steps.length === 0) {
return (
<div className="bg-white py-8 sm:py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-xl mb-4" style={{ color: "#e64838" }}>
Milestones
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold leading-tight mb-6" style={{ color: "#012068" }}>
Our Journey in Trauma Care
</h2>
<p className="text-base sm:text-lg leading-relaxed" style={{ color: '#333' }}>
No milestones available at the moment.
</p>
</div>
</div>
);
}
// For small number of items (1-3), use static layout without sticky scroll
const isSmallList = steps.length <= 3;
if (isSmallList) {
return (
<div className="bg-white py-8 sm:py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
{/* Left Column */} {/* Left Column */}
<div className="flex flex-col justify-start"> <div className="flex flex-col justify-start">
<div> <div>
@ -211,40 +176,147 @@ export default function Process() {
> >
Our Journey in Trauma Care Our Journey in Trauma Care
</h2> </h2>
<p className="text-base sm:text-lg leading-relaxed mb-8" style={{ color: '#333' }}> <p className="text-base sm:text-lg leading-relaxed" style={{ color: '#333' }}>
From the formation of Trauma Services in 2017 to establishing a Level-1 Trauma Facility and comprehensive trauma care programs, we continue to expand our emergency care services with dedication and excellence.
</p>
</div>
</div>
{/* Right Column - Static Timeline */}
<div className="relative">
{steps.map((step, index) => (
<div
key={index}
className={`relative group transition-all duration-500 ${
index !== 0 ? "mt-10" : ""
}`}
onMouseEnter={() => setActiveStep(index)}
onMouseLeave={() => setActiveStep(-1)}
>
{/* Connecting Line */}
{index !== 0 && (
<div className="absolute left-6 -top-10 w-0.5 h-10 bg-gray-300 overflow-hidden">
<div
className="w-full h-full transition-all duration-700"
style={{ backgroundColor: "#012068" }}
/>
</div>
)}
{/* Step Content */}
<div className="flex items-start gap-4 sm:gap-6 pl-2">
{/* Number Circle */}
<div
className={`relative z-10 flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center text-white font-semibold text-lg transition-all duration-500 transform ${
activeStep === index ? "scale-110 shadow-lg ring-4" : "scale-100 hover:scale-105"
}`}
style={{
backgroundColor: activeStep === index ? "#012068" : "#333",
"--tw-ring-color": activeStep === index ? "#01206833" : "transparent",
} as React.CSSProperties}
>
{step.number}
</div>
{/* Step Card */}
<div
className={`flex-1 p-4 sm:p-6 rounded-lg border transition-all duration-500 transform min-h-[180px] ${
activeStep === index
? "border-transparent text-white shadow-2xl scale-105 -translate-y-2"
: "border-gray-300 hover:shadow-lg hover:-translate-y-1"
}`}
style={{
backgroundColor: activeStep === index ? "#012068" : "#f4f4f4",
color: activeStep === index ? "white" : "#333",
}}
>
{/* Icon */}
<div
className="mb-4 transition-all duration-500"
style={{
color: activeStep === index ? "white" : "black",
}}
>
{step.icon}
</div>
{/* Text */}
<h3 className="text-lg sm:text-xl font-semibold mb-3 transition-all duration-300 break-words">
{step.title}
</h3>
<p className="text-sm sm:text-base leading-relaxed transition-all duration-500 break-words whitespace-normal">
{step.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
// For larger lists (4+), use sticky scroll behavior
return (
<div
ref={containerRef}
className="bg-white relative mb-0"
style={{
minHeight: `calc(100vh + ${extraHeight}px)`,
}}
>
<div className="sticky top-0 h-screen flex items-center overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
{/* Left Column - Fixed Header */}
<div className="flex flex-col justify-start lg:sticky lg:top-1/4">
<div>
<div className="text-xl mb-4" style={{ color: "#e64838" }}>
Milestones
</div>
<h2
className="text-3xl sm:text-4xl md:text-5xl font-bold leading-tight mb-6"
style={{ color: "#012068" }}
>
Our Journey in Trauma Care
</h2>
<p className="text-base sm:text-lg leading-relaxed" style={{ color: '#333' }}>
From the formation of Trauma Services in 2017 to establishing a Level-1 Trauma Facility and comprehensive trauma care programs, we continue to expand our emergency care services with dedication and excellence. From the formation of Trauma Services in 2017 to establishing a Level-1 Trauma Facility and comprehensive trauma care programs, we continue to expand our emergency care services with dedication and excellence.
</p> </p>
</div> </div>
</div> </div>
{/* Right Column - Auto-scrolling Timeline */} {/* Right Column - Auto-scrolling Timeline */}
<div className="relative h-[600px]"> <div className="relative h-[70vh] max-h-[600px]">
<div <div
ref={timelineRef} ref={timelineRef}
className="h-full overflow-y-auto pr-2 pl-2 py-2 [&::-webkit-scrollbar]:hidden pointer-events-none" className="h-full overflow-y-auto pr-4 pl-4 [&::-webkit-scrollbar]:hidden pointer-events-none"
style={{ style={{
scrollbarWidth: 'none', scrollbarWidth: 'none',
msOverflowStyle: 'none' msOverflowStyle: 'none'
}} }}
> >
<div className="py-4">
{steps.map((step, index) => ( {steps.map((step, index) => (
<div <div
key={index} key={index}
ref={(el) => {
if (el) {
stepRefs.current[index] = el;
}
}}
className={`relative group transition-all duration-500 ${ className={`relative group transition-all duration-500 ${
index !== 0 ? "mt-8 sm:mt-12" : "" index !== 0 ? "mt-10" : ""
} ${index === 0 ? "pt-2" : ""}`} }`}
onMouseEnter={() => setActiveStep(index)}
onMouseLeave={() => setActiveStep(-1)}
> >
{/* Connecting Line */} {/* Connecting Line */}
{index !== 0 && ( {index !== 0 && (
<div className="absolute left-6 -top-8 sm:-top-12 w-0.5 h-8 sm:h-12 bg-gray-300 overflow-hidden"> <div className="absolute left-6 -top-10 w-0.5 h-10 bg-gray-300 overflow-hidden">
<div <div
className={`w-full transition-all duration-700 ease-out origin-bottom ${ className={`w-full transition-all duration-700 ease-out origin-top ${
activeStep >= index || activeStep >= index ? "h-full scale-y-100" : "h-0 scale-y-0"
(activeStep === -1 && index === 0)
? "h-full scale-y-100"
: "h-0 scale-y-0"
}`} }`}
style={{ backgroundColor: "#012068" }} style={{ backgroundColor: "#012068" }}
/> />
@ -256,22 +328,13 @@ export default function Process() {
{/* Number Circle */} {/* Number Circle */}
<div <div
className={`relative z-10 flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center text-white font-semibold text-lg transition-all duration-500 transform ${ className={`relative z-10 flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center text-white font-semibold text-lg transition-all duration-500 transform ${
activeStep === index || activeStep === index
(activeStep === -1 && index === 0)
? "scale-110 shadow-lg ring-4" ? "scale-110 shadow-lg ring-4"
: "scale-100 hover:scale-105" : "scale-100"
}`} }`}
style={{ style={{
backgroundColor: backgroundColor: activeStep === index ? "#012068" : "#333",
activeStep === index || "--tw-ring-color": activeStep === index ? "#01206833" : "transparent",
(activeStep === -1 && index === 0)
? "#012068"
: "#333",
"--tw-ring-color":
activeStep === index ||
(activeStep === -1 && index === 0)
? "#012068" + "33"
: "transparent",
} as React.CSSProperties} } as React.CSSProperties}
> >
{step.number} {step.number}
@ -279,49 +342,31 @@ export default function Process() {
{/* Step Card */} {/* Step Card */}
<div <div
className={`flex-1 p-4 sm:p-6 rounded-lg border transition-all duration-500 transform ${ className={`flex-1 p-4 sm:p-6 rounded-lg border transition-all duration-500 transform min-h-[180px] ${
activeStep === index || activeStep === index
(activeStep === -1 && index === 0)
? "border-transparent text-white shadow-2xl scale-105 -translate-y-2" ? "border-transparent text-white shadow-2xl scale-105 -translate-y-2"
: "border-gray-300 hover:shadow-lg hover:-translate-y-1" : "border-gray-300"
}`} }`}
style={{ style={{
backgroundColor: backgroundColor: activeStep === index ? "#012068" : "#f4f4f4",
activeStep === index || color: activeStep === index ? "white" : "#333",
(activeStep === -1 && index === 0)
? "#012068"
: "#f4f4f4",
color:
activeStep === index ||
(activeStep === -1 && index === 0)
? "white"
: "#333",
}} }}
> >
{/* Icon */} {/* Icon */}
<div <div
className={`mb-4 transition-all duration-500 transform ${ className="mb-4 transition-all duration-500"
activeStep === index ||
(activeStep === -1 && index === 0)
? "text-white"
: "scale-100 group-hover:scale-105"
}`}
style={{ style={{
color: color: activeStep === index ? "white" : "black",
activeStep === index ||
(activeStep === -1 && index === 0)
? "white"
: "black",
}} }}
> >
{step.icon} {step.icon}
</div> </div>
{/* Text */} {/* Text */}
<h3 className="text-lg sm:text-xl font-semibold mb-3 transition-all duration-300"> <h3 className="text-lg sm:text-xl font-semibold mb-3 transition-all duration-300 break-words">
{step.title} {step.title}
</h3> </h3>
<p className="text-sm sm:text-base leading-relaxed transition-all duration-500"> <p className="text-sm sm:text-base leading-relaxed transition-all duration-500 break-words whitespace-normal">
{step.description} {step.description}
</p> </p>
</div> </div>
@ -334,5 +379,6 @@ export default function Process() {
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@ -1,13 +1,22 @@
// components/EducationTraining.tsx
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { ChevronRight, Clock, Users, Award, Calendar } from 'lucide-react'; import {
ChevronRight,
Clock,
Users,
Award,
Calendar,
BookOpen,
MapPin,
GraduationCap,
FlaskConical
} from 'lucide-react';
import { educationService, Course } from '../../services/educationService'; import { educationService, Course } from '../../services/educationService';
import { upcomingEventsService, UpcomingEvent } from '../../services/upcomingEventsService'; import { upcomingEventsService, UpcomingEvent } from '../../services/upcomingEventsService';
const EducationTraining: React.FC = () => { const AcademicResearch: React.FC = () => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [courses, setCourses] = useState<Course[]>([]); const [courses, setCourses] = useState<Course[]>([]);
const [upcomingEvents, setUpcomingEvents] = useState<UpcomingEvent[]>([]); const [upcomingEvents, setUpcomingEvents] = useState<UpcomingEvent[]>([]);
@ -15,9 +24,70 @@ const EducationTraining: React.FC = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [selectedCategory, setSelectedCategory] = useState<string>('All'); const [selectedCategory, setSelectedCategory] = useState<string>('All');
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
const [activeTab, setActiveTab] = useState<'education' | 'research'>('education');
const categories = ['All', 'Certification', 'Training', 'Workshop', 'Fellowship']; const categories = ['All', 'Certification', 'Training', 'Workshop', 'Fellowship'];
// Research Projects Data
const ongoingProjects = [
{
icon: <BookOpen className="w-6 h-6" />,
title: "Trauma Registry CMC Pilot Study (T-ReCS)",
subtitle: "In association with TCI",
description: "TRECS is a comprehensive digital platform designed to systematically capture, store, and analyze trauma patient data at our Ranipet Level-1 Trauma Center. The platform enables monitoring of clinical outcomes, identification of gaps in care, and improvement of patient management protocols.",
features: [
"Patient Demographics & Injury Details: Captures age, gender, mechanism of injury, and severity scoring",
"Clinical Data Tracking: Includes vital signs at admission, laboratory investigations, imaging, operative interventions, and ICU care",
"Outcome Analysis: Tracks mortality, morbidity, complications, length of stay, and functional recovery",
"Quality Improvement: Supports audits, departmental reviews, and research projects",
"Research Support: TRECS data contributes to peer-reviewed publications, national trauma policies, and predictive modeling"
]
},
{
icon: <FlaskConical className="w-6 h-6" />,
title: "Trauma Quality Improvement Programme (TQIP)",
subtitle: "In association with ICMR",
description: "TQIP is an evidence-based framework adopted to ensure excellence in patient care through continuous monitoring and benchmarking. The program integrates data-driven approaches to improve safety, reduce complications, and standardize protocols.",
features: [
"Performance Metrics: Tracks key indicators including time to intervention, mortality, ICU stay, and surgical outcomes",
"Benchmarking: Compares departmental outcomes with national and international trauma centers",
"Clinical Audits: Regularly conducted to assess adherence to trauma protocols",
"Education & Training: Provides feedback loops for residents, nursing staff, and trauma coordinators",
"Quality Initiatives: Implements targeted interventions for overstay management and Golden Hour optimization"
]
}
];
const milestones = [
{
year: "2023",
event: "ACTraM 2023",
description: "Presented 3 comprehensive audits covering surgical intervention times, ICU stay duration, and patient outcome metrics",
position: "left"
},
{
year: "2022",
event: "Q4 Resource Audit",
description: "Completed comprehensive audit on trauma patient overstay patterns and healthcare resource utilization optimization",
position: "right"
}
];
const collaborators = [
{ name: "American College of Surgeons", location: "", short: "ACS", logo: "/logos/acs.jpg" },
{ name: "BMJ Global", location: "", short: "BMJ", logo: "/logos/bmj.png" },
{ name: "Michigan University", location: "", short: "UM", logo: "/logos/michigan.jpg" },
{ name: "Indian Council of Medical Research", location: "", short: "ICMR", logo: "/logos/icmr.png" },
{ name: "ISTAC", location: "", short: "ISTAC", logo: "/logos/istac.png" },
{ name: "All India Institute of Medical Sciences", location: "", short: "AIIMS", logo: "/logos/aiims.png" },
{ name: "JPNATC AIIMS Delhi", location: "New Delhi", short: "JPNATC", logo: "/logos/jpnatc.png" },
{ name: "JIPMER", location: "", short: "JIPMER", logo: "/logos/jipmer.png" },
{ name: "IPGMER", location: "Kolkata", short: "IPGMER", logo: "/logos/ipgmer.png" },
{ name: "TCI", location: "", short: "TCI", logo: "/logos/tci.png" },
{ name: "GVK EMRI", location: "", short: "GVK EMRI", logo: "/logos/gvk-emri.jpg" },
{ name: "TAEI", location: "", short: "TAEI", logo: "/logos/taei.png" }
];
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
loadData(); loadData();
@ -28,7 +98,6 @@ const EducationTraining: React.FC = () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
// Load both courses and upcoming events concurrently
const [fetchedCourses, fetchedEvents] = await Promise.all([ const [fetchedCourses, fetchedEvents] = await Promise.all([
educationService.getActiveCourses(), educationService.getActiveCourses(),
upcomingEventsService.getActiveUpcomingEvents() upcomingEventsService.getActiveUpcomingEvents()
@ -48,7 +117,6 @@ const EducationTraining: React.FC = () => {
return null; return null;
} }
// Filter courses based on category and search
const filteredCourses = courses.filter(course => { const filteredCourses = courses.filter(course => {
const matchesCategory = selectedCategory === 'All' || course.category === selectedCategory; const matchesCategory = selectedCategory === 'All' || course.category === selectedCategory;
const matchesSearch = !searchQuery.trim() || const matchesSearch = !searchQuery.trim() ||
@ -68,22 +136,19 @@ const EducationTraining: React.FC = () => {
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-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> <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> <p className="text-gray-600">Loading content...</p>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen"> <div className="min-h-screen bg-white">
{/* Header Section */} {/* Header Section */}
<section <section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
className="py-4 relative overflow-hidden" <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
style={{ backgroundColor: '#f4f4f4' }}
>
<div className="max-w-7xl mx-auto px-4 relative z-10">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center space-x-2 text-sm mb-6"> <nav className="flex items-center space-x-2 text-sm">
<Link <Link
href="/" href="/"
className="hover:opacity-70 transition-opacity duration-200" className="hover:opacity-70 transition-opacity duration-200"
@ -93,19 +158,53 @@ const EducationTraining: React.FC = () => {
</Link> </Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} /> <ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}> <span className="font-medium" style={{ color: '#e64838' }}>
Education & Training Academic & Research
</span> </span>
</nav> </nav>
{/* Page Header */} {/* Page Header */}
<div className="mb-2"> <div className="mt-6">
<h1 className="text-4xl font-bold mb-4" style={{ color: '#012068' }}> <h1 className="text-4xl font-bold mb-4" style={{ color: '#012068' }}>
Education & Training Academic & Research
</h1> </h1>
<p className="text-lg max-w-3xl leading-relaxed" style={{ color: '#333' }}> <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. Advancing trauma care through innovative research, evidence-based practices, and comprehensive training programs
</p> </p>
</div> </div>
{/* Tab Navigation */}
<div className="mt-8 flex gap-4 border-b-2" style={{ borderColor: '#e5e7eb' }}>
<button
onClick={() => setActiveTab('education')}
className={`pb-4 px-6 font-semibold transition-all duration-200 relative ${
activeTab === 'education' ? 'border-b-4' : ''
}`}
style={{
color: activeTab === 'education' ? '#012068' : '#666',
borderColor: activeTab === 'education' ? '#e64838' : 'transparent'
}}
>
<div className="flex items-center gap-2">
<GraduationCap className="w-5 h-5" />
<span>Education & Training</span>
</div>
</button>
<button
onClick={() => setActiveTab('research')}
className={`pb-4 px-6 font-semibold transition-all duration-200 relative ${
activeTab === 'research' ? 'border-b-4' : ''
}`}
style={{
color: activeTab === 'research' ? '#012068' : '#666',
borderColor: activeTab === 'research' ? '#e64838' : 'transparent'
}}
>
<div className="flex items-center gap-2">
<FlaskConical className="w-5 h-5" />
<span>Research</span>
</div>
</button>
</div>
</div> </div>
</section> </section>
@ -124,8 +223,11 @@ const EducationTraining: React.FC = () => {
</div> </div>
)} )}
{/* Upcoming Training Section with Dynamic Cards */} {/* Education & Training Tab Content */}
<section className="py-8" style={{ backgroundColor: '#fff' }}> {activeTab === 'education' && (
<>
{/* Upcoming Training Section */}
<section className="py-12" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 text-center" style={{ color: '#012068' }}> <h2 className="text-3xl font-bold mb-8 text-center" style={{ color: '#012068' }}>
Upcoming Training Programs Upcoming Training Programs
@ -133,7 +235,11 @@ const EducationTraining: React.FC = () => {
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{upcomingEvents.length > 0 ? ( {upcomingEvents.length > 0 ? (
upcomingEvents.map((event) => ( 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
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 className="flex items-center mb-4">
<div> <div>
<h3 className="text-lg font-bold" style={{ color: '#012068' }}> <h3 className="text-lg font-bold" style={{ color: '#012068' }}>
@ -151,7 +257,6 @@ const EducationTraining: React.FC = () => {
</div> </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="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 className="flex items-center mb-4">
@ -212,6 +317,9 @@ const EducationTraining: React.FC = () => {
{/* Filter & Search */} {/* Filter & Search */}
<section className="py-8" style={{ backgroundColor: '#fff' }}> <section className="py-8" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 text-center" style={{ color: '#012068' }}>
Available Courses
</h2>
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
{/* Category Filter */} {/* Category Filter */}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@ -219,9 +327,8 @@ const EducationTraining: React.FC = () => {
<button <button
key={category} key={category}
onClick={() => setSelectedCategory(category)} onClick={() => setSelectedCategory(category)}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${selectedCategory === category className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
? 'text-white' selectedCategory === category ? 'text-white' : 'border-2'
: 'border-2'
}`} }`}
style={{ style={{
backgroundColor: selectedCategory === category ? '#012068' : 'transparent', backgroundColor: selectedCategory === category ? '#012068' : 'transparent',
@ -244,8 +351,10 @@ const EducationTraining: React.FC = () => {
className="border-2 rounded-lg px-4 py-2 pl-4 pr-10 text-sm focus:outline-none w-64" 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' }} style={{ borderColor: '#012068', color: '#333' }}
/> />
<button className="absolute inset-y-0 right-0 flex items-center px-3 rounded-lg" <button
style={{ backgroundColor: '#012068' }}> 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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
@ -376,7 +485,7 @@ const EducationTraining: React.FC = () => {
</div> </div>
</section> </section>
{/* CTA */} {/* CTA Section */}
<section className="py-16" style={{ backgroundColor: '#f4f4f4' }}> <section className="py-16" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-4xl mx-auto px-4 text-center"> <div className="max-w-4xl mx-auto px-4 text-center">
<Award className="w-12 h-12 mx-auto mb-6" style={{ color: '#e64838' }} /> <Award className="w-12 h-12 mx-auto mb-6" style={{ color: '#e64838' }} />
@ -410,8 +519,151 @@ const EducationTraining: React.FC = () => {
</div> </div>
</div> </div>
</section> </section>
</>
)}
{/* Research Tab Content */}
{activeTab === 'research' && (
<>
{/* Ongoing Projects Section */}
<section className="py-12">
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-semibold mb-12 text-center" style={{ color: '#012068' }}>
Ongoing Projects
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{ongoingProjects.map((project, index) => (
<div
key={index}
className="rounded-lg p-6 hover:shadow-lg transition-shadow duration-300"
style={{ backgroundColor: '#f4f4f4' }}
>
<div className="flex items-start mb-4">
<div
className="p-3 rounded-lg mr-4 flex-shrink-0"
style={{ color: '#012068' }}
>
{project.icon}
</div>
<div>
<h3 className="text-xl font-semibold mb-1" style={{ color: '#012068' }}>
{project.title}
</h3>
<p className="text-sm font-medium mb-3" style={{ color: '#e64838' }}>
{project.subtitle}
</p>
</div>
</div>
<p className="text-gray-700 leading-relaxed mb-4">
{project.description}
</p>
<div className="space-y-2">
<h4 className="text-sm font-semibold" style={{ color: '#012068' }}>Key Features:</h4>
<ul className="space-y-2">
{project.features.map((feature, idx) => (
<li key={idx} className="text-sm text-gray-700 leading-relaxed flex">
<span className="mr-2" style={{ color: '#e64838' }}></span>
<span>{feature}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
</section>
{/* Past Work Section */}
<section className="py-12" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-semibold mb-12 text-center" style={{ color: '#012068' }}>
Past Work
</h2>
<div className="relative">
{/* Timeline line */}
<div
className="absolute left-1/2 h-full w-0.5"
style={{ backgroundColor: '#012068' }}
></div>
{milestones.map((milestone, index) => (
<div key={index} className={`flex items-center mb-12 ${milestone.position === 'left' ? 'flex-row-reverse' : ''}`}>
<div className={`w-1/2 ${milestone.position === 'left' ? 'pr-8 text-right' : 'pl-8'}`}>
<div
className="p-6 rounded-lg shadow-sm group"
style={{ backgroundColor: 'white', borderColor: '#f4f4f4' }}
>
<div className="flex items-center mb-3">
<span
className="text-2xl font-bold mr-3 group-hover:scale-110 transition-transform duration-300"
style={{ color: '#e64838' }}
>
{milestone.year}
</span>
<Award className="w-5 h-5 group-hover:rotate-12 transition-transform duration-300" style={{ color: '#012068' }} />
</div>
<h3 className="text-xl font-semibold mb-2 group-hover:text-opacity-80 transition-all duration-300" style={{ color: '#012068' }}>
{milestone.event}
</h3>
<p className="text-gray-700 group-hover:text-gray-600 transition-colors duration-300">
{milestone.description}
</p>
</div>
</div>
{/* Timeline dot */}
<div
className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 rounded-full border-4 border-white shadow-lg hover:scale-125 transition-transform duration-300"
style={{ backgroundColor: '#e64838' }}
></div>
</div>
))}
</div>
</div>
</section>
{/* Collaborators Section */}
<section className="py-12">
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-semibold mb-4 text-center" style={{ color: '#012068' }}>
Collaborators
</h2>
<p className="text-center text-gray-600 mb-12">
Our research partnerships span leading medical institutions worldwide
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-8">
{collaborators.map((collaborator, index) => (
<div
key={index}
className="text-center p-6 rounded-lg border hover:shadow-md transition-shadow duration-300 flex flex-col items-center justify-center"
style={{ backgroundColor: '#f4f4f4', borderColor: '#f4f4f4' }}
>
<div className="w-24 h-24 mb-4 flex items-center justify-center">
<img
src={collaborator.logo}
alt={`${collaborator.short} logo`}
className="max-w-full max-h-full object-contain"
/>
</div>
<h3 className="font-semibold text-sm" style={{ color: '#012068' }}>
{collaborator.short}
</h3>
{collaborator.location && (
<div className="flex items-center justify-center text-xs text-gray-500 mt-1">
<MapPin className="w-3 h-3 mr-1" />
{collaborator.location}
</div>
)}
</div>
))}
</div>
</div>
</section>
</>
)}
</div> </div>
); );
}; };
export default EducationTraining; export default AcademicResearch;

View File

@ -13,7 +13,6 @@ const EventDetail = () => {
const [eventData, setEventData] = useState<Event | null>(null); const [eventData, setEventData] = useState<Event | null>(null);
const [isBookmarked, setIsBookmarked] = useState(false); const [isBookmarked, setIsBookmarked] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [bookingStatus, setBookingStatus] = useState<'idle' | 'booking' | 'success' | 'error'>('idle');
useEffect(() => { useEffect(() => {
const fetchEventData = async () => { const fetchEventData = async () => {
@ -25,7 +24,6 @@ const EventDetail = () => {
console.log('Fetching event with ID:', eventId); console.log('Fetching event with ID:', eventId);
setIsLoading(true); setIsLoading(true);
try { try {
// Convert string ID to number for API call
const numericId = parseInt(eventId, 10); const numericId = parseInt(eventId, 10);
if (isNaN(numericId)) { if (isNaN(numericId)) {
console.error('Invalid event ID:', eventId); console.error('Invalid event ID:', eventId);
@ -77,17 +75,28 @@ const EventDetail = () => {
} }
}; };
const handleBookSeat = async () => { // NEW FUNCTION: Handle book seat click - redirects to admin-provided link
const handleBookSeat = () => {
if (!eventData) return; if (!eventData) return;
setBookingStatus('booking');
// Check if admin has provided a custom booking link
if (eventData.bookSeatLink && eventData.bookSeatLink.trim() !== '') {
// Validate if it's a proper URL
try { try {
// Replace with actual booking API call // If the URL doesn't start with http:// or https://, add https://
await new Promise(resolve => setTimeout(resolve, 1500)); let url = eventData.bookSeatLink;
setBookingStatus('success'); if (!url.startsWith('http://') && !url.startsWith('https://')) {
setTimeout(() => setBookingStatus('idle'), 2000); url = 'https://' + url;
}
// Open in new tab
window.open(url, '_blank', 'noopener,noreferrer');
} catch (error) { } catch (error) {
setBookingStatus('error'); console.error('Invalid booking link:', error);
setTimeout(() => setBookingStatus('idle'), 2000); alert('Invalid registration link. Please contact the organizers.');
}
} else {
// Fallback: Show alert if no link is configured
alert('Registration link not configured for this event. Please contact the organizers at ' + eventData.email);
} }
}; };
@ -113,24 +122,6 @@ const EventDetail = () => {
router.back(); router.back();
}; };
const getBookingButtonText = () => {
switch (bookingStatus) {
case 'booking': return 'Booking...';
case 'success': return 'Booked!';
case 'error': return 'Try Again';
default: return 'Book Your Seat';
}
};
const getBookingButtonStyle = () => {
switch (bookingStatus) {
case 'booking': return 'bg-gray-600 cursor-not-allowed';
case 'success': return 'bg-green-600 hover:bg-green-700';
case 'error': return 'bg-red-600 hover:bg-red-700';
default: return 'hover:opacity-90';
}
};
// Helper functions to format API data for display // Helper functions to format API data for display
const formatPrice = (event: Event) => { const formatPrice = (event: Event) => {
if (event.fee && event.fee.length > 0) { if (event.fee && event.fee.length > 0) {
@ -171,7 +162,6 @@ const EventDetail = () => {
return fallbackImages; return fallbackImages;
} }
// Ensure we have at least 2 images for the layout
while (validImages.length < 2) { while (validImages.length < 2) {
validImages.push(fallbackImages[validImages.length % fallbackImages.length]); validImages.push(fallbackImages[validImages.length % fallbackImages.length]);
} }
@ -179,7 +169,6 @@ const EventDetail = () => {
return validImages.slice(0, 2); return validImages.slice(0, 2);
}; };
// Format description data for display
const getFormattedDescription = (event: Event) => { const getFormattedDescription = (event: Event) => {
return { return {
overview: event.description || 'Join leading medical professionals for this comprehensive event focusing on the latest developments in healthcare.', overview: event.description || 'Join leading medical professionals for this comprehensive event focusing on the latest developments in healthcare.',
@ -207,29 +196,18 @@ const EventDetail = () => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}> <div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
{/* Breadcrumb Section */}
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}> <section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<nav className="flex items-center space-x-2 text-sm"> <nav className="flex items-center space-x-2 text-sm">
<Link <Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
href="/"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Home Home
</Link> </Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} /> <ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<a <a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
href="/events"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Medical Events Medical Events
</a> </a>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} /> <ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}> <span className="font-medium" style={{ color: '#e64838' }}>Loading...</span>
Loading...
</span>
</nav> </nav>
</div> </div>
</section> </section>
@ -259,29 +237,18 @@ const EventDetail = () => {
if (!eventData) { if (!eventData) {
return ( return (
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}> <div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
{/* Breadcrumb Section */}
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}> <section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<nav className="flex items-center space-x-2 text-sm"> <nav className="flex items-center space-x-2 text-sm">
<Link <Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
href="/"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Home Home
</Link> </Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} /> <ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<a <a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
href="/events"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Medical Events Medical Events
</a> </a>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} /> <ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}> <span className="font-medium" style={{ color: '#e64838' }}>Event Not Found</span>
Event Not Found
</span>
</nav> </nav>
</div> </div>
</section> </section>
@ -317,19 +284,11 @@ const EventDetail = () => {
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}> <section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<nav className="flex items-center space-x-2 text-sm"> <nav className="flex items-center space-x-2 text-sm">
<Link <Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
href="/"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Home Home
</Link> </Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} /> <ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<a <a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
href="/events"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Medical Events Medical Events
</a> </a>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} /> <ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
@ -400,14 +359,10 @@ const EventDetail = () => {
</div> </div>
<button <button
onClick={handleBookSeat} onClick={handleBookSeat}
disabled={bookingStatus === 'booking'} className="w-full sm:w-auto px-6 py-2 text-sm rounded-lg transition-all duration-200 hover:opacity-90"
className={`w-full sm:w-auto px-6 py-2 text-sm rounded-lg transition-all duration-200 ${getBookingButtonStyle()}`} style={{ backgroundColor: '#012068', color: '#f4f4f4' }}
style={{
backgroundColor: bookingStatus === 'idle' ? '#012068' : undefined,
color: '#f4f4f4'
}}
> >
{getBookingButtonText()} Book Your Seat
</button> </button>
</div> </div>
</div> </div>

View File

@ -78,16 +78,26 @@ const TeamListing: React.FC<TeamListingProps> = ({
const TeamMemberCard: React.FC<{ member: TeamMember }> = ({ member }) => { const TeamMemberCard: React.FC<{ member: TeamMember }> = ({ member }) => {
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
const [imageLoading, setImageLoading] = useState(true); const [imageLoading, setImageLoading] = useState(true);
const isTrainee = member.category === 'TRAINEE_FELLOW';
// Check if image URL is valid and not a default placeholder
const hasValidImage = member.image &&
member.image.trim() !== '' &&
!member.image.includes('default-avatar') &&
!member.image.includes('placeholder.') &&
!member.image.includes('robot') &&
// Only filter if URL ENDS with /profile-image (backend default endpoint)
!(member.image.endsWith('/profile-image') ||
member.image.includes('/profile-image?') ||
member.image === 'https://via.placeholder.com/400');
const shouldShowImage = !isTrainee && hasValidImage;
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => { const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
const target = e.target as HTMLImageElement;
console.error('Image failed to load:', member.image); console.error('Image failed to load:', member.image);
console.error('Member:', member.name, 'Professor ID:', member.professorId); console.error('Member:', member.name, 'Professor ID:', member.professorId);
if (!imageError) {
setImageError(true); setImageError(true);
target.src = '/images/default-avatar.jpg'; setImageLoading(false);
}
}; };
const handleImageLoad = () => { const handleImageLoad = () => {
@ -97,10 +107,16 @@ const TeamListing: React.FC<TeamListingProps> = ({
return ( return (
<div <div
className="group cursor-pointer bg-white rounded-lg border border-gray-300 overflow-hidden hover:shadow-lg transition-all duration-300" className={`bg-white rounded-lg border border-gray-300 overflow-hidden transition-all duration-300 ${
onClick={() => handleMemberClick(member)} !isTrainee ? 'group cursor-pointer hover:shadow-lg' : 'cursor-default'
}`}
onClick={() => !isTrainee && handleMemberClick(member)}
> >
<div className="relative aspect-square overflow-hidden"> {/* Only show image section for non-trainees */}
{!isTrainee && (
<div className="relative aspect-square overflow-hidden bg-gray-50">
{shouldShowImage && !imageError ? (
<>
{imageLoading && ( {imageLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100"> <div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
@ -117,13 +133,23 @@ const TeamListing: React.FC<TeamListingProps> = ({
onLoad={handleImageLoad} onLoad={handleImageLoad}
loading="lazy" loading="lazy"
/> />
</>
{imageError && ( ) : (
<div className="absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded"> <div
Default className="w-full h-full flex items-center justify-center"
style={{ backgroundColor: '#e0e5eb' }}
>
<svg
className="w-32 h-32 text-gray-400"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div> </div>
)} )}
</div> </div>
)}
<div className="p-4 sm:p-6"> <div className="p-4 sm:p-6">
<h3 className="text-lg font-medium mb-2 group-hover:opacity-70 transition-opacity" style={{ color: '#012068' }}> <h3 className="text-lg font-medium mb-2 group-hover:opacity-70 transition-opacity" style={{ color: '#012068' }}>
@ -132,6 +158,8 @@ const TeamListing: React.FC<TeamListingProps> = ({
<p className="text-sm leading-relaxed" style={{ color: '#e64838' }}> <p className="text-sm leading-relaxed" style={{ color: '#e64838' }}>
{member.position} {member.position}
</p> </p>
{!isTrainee && (
<>
{member.department && ( {member.department && (
<p className="text-xs mt-1" style={{ color: '#666' }}> <p className="text-xs mt-1" style={{ color: '#666' }}>
{member.department} {member.department}
@ -142,7 +170,8 @@ const TeamListing: React.FC<TeamListingProps> = ({
{member.specialty} {member.specialty}
</p> </p>
)} )}
</>
)}
</div> </div>
</div> </div>
); );
@ -233,7 +262,7 @@ const TeamListing: React.FC<TeamListingProps> = ({
<h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}>
Trainees & Fellows Trainees & Fellows
</h2> </h2>
<p className="text-sm" style={{ color: '#666' }}> <p className="text-sm mb-2" style={{ color: '#666' }}>
Medical trainees, residents, and fellows advancing their skills and contributing to patient care Medical trainees, residents, and fellows advancing their skills and contributing to patient care
</p> </p>
</div> </div>

View File

@ -21,6 +21,7 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
const [loading, setLoading] = useState(!memberData); const [loading, setLoading] = useState(!memberData);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showSocialShare, setShowSocialShare] = useState(false); const [showSocialShare, setShowSocialShare] = useState(false);
const [imageError, setImageError] = useState(false);
useEffect(() => { useEffect(() => {
const loadMemberData = async () => { const loadMemberData = async () => {
@ -72,9 +73,24 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
); );
} }
const isTrainee = member.category === 'TRAINEE_FELLOW';
// Check if image URL is valid and not a default placeholder
const hasValidImage = member.image &&
member.image.trim() !== '' &&
!member.image.includes('default-avatar') &&
!member.image.includes('placeholder.') &&
!member.image.includes('robot') &&
// Only filter if URL ENDS with /profile-image (backend default endpoint)
!(member.image.endsWith('/profile-image') ||
member.image.includes('/profile-image?') ||
member.image === 'https://via.placeholder.com/400');
const shouldShowImage = !isTrainee && hasValidImage && !imageError;
// Create a comprehensive description from the member's details // Create a comprehensive description from the member's details
const getFullDescription = () => { const getFullDescription = () => {
return member.description || `${member.name} is a dedicated member of our faculty with expertise in ${member.specialty?.toLowerCase() || 'medical practice'}. They bring valuable experience in surgical education, patient care, and clinical research to Christian Medical College, Vellore.`; return member.description || `${member.name} is a dedicated member of our ${isTrainee ? 'trainee program' : 'faculty'} with expertise in ${member.specialty?.toLowerCase() || 'medical practice'}. They bring valuable experience in ${isTrainee ? 'clinical training' : 'surgical education'}, patient care, and clinical research to Christian Medical College, Vellore.`;
}; };
// Parse phone numbers if multiple are provided // Parse phone numbers if multiple are provided
@ -87,6 +103,11 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
: member.phone; : member.phone;
}; };
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
console.error('Image failed to load:', member.image);
setImageError(true);
};
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white">
{/* Breadcrumb Section */} {/* Breadcrumb Section */}
@ -120,6 +141,11 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}> <h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
{member.name} {member.name}
</h1> </h1>
{isTrainee && (
<span className="ml-4 px-3 py-1 text-xs font-medium rounded-full bg-gray-200 text-gray-700">
Trainee/Fellow
</span>
)}
</div> </div>
<p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}> <p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
{member.designation || member.position} {member.experience && `with ${member.experience} of experience`} at CMC Vellore {member.designation || member.position} {member.experience && `with ${member.experience} of experience`} at CMC Vellore
@ -134,17 +160,29 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
{/* Sidebar */} {/* Sidebar */}
<div className="xl:col-span-1"> <div className="xl:col-span-1">
<div className="bg-white rounded-md border border-gray-300 overflow-hidden sticky top-8"> <div className="bg-white rounded-md border border-gray-300 overflow-hidden sticky top-8">
{/* Profile Image */} {/* Profile Image or Avatar */}
<div className="aspect-square relative overflow-hidden"> <div className="aspect-square relative overflow-hidden bg-gray-50">
{shouldShowImage ? (
<img <img
src={member.image} src={member.image}
alt={member.name} alt={member.name}
className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-300" className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-300"
onError={(e) => { onError={handleImageError}
const target = e.target as HTMLImageElement;
target.src = '/images/default-avatar.jpg';
}}
/> />
) : (
<div
className="w-full h-full flex items-center justify-center"
style={{ backgroundColor: '#e0e5eb' }}
>
<svg
className="w-48 h-48 text-gray-400"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div>
)}
</div> </div>
{/* Profile Info */} {/* Profile Info */}
@ -160,7 +198,7 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
</div> </div>
<p className="text-sm mb-6 leading-relaxed" style={{ color: '#333' }}> <p className="text-sm mb-6 leading-relaxed" style={{ color: '#333' }}>
{member.description || `${member.name} is a dedicated faculty member at CMC Vellore.`} {member.description || `${member.name} is a dedicated ${isTrainee ? 'trainee' : 'faculty member'} at CMC Vellore.`}
</p> </p>
<div className="space-y-4 mb-6"> <div className="space-y-4 mb-6">

View File

@ -0,0 +1,164 @@
'use client'
import React, { useState, useEffect } from 'react';
import testimonialService, { Testimonial } from '@/services/testimonialService';
export default function PatientTestimonialCarousel() {
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Fetch testimonials on component mount
useEffect(() => {
fetchTestimonials();
}, []);
const fetchTestimonials = async () => {
try {
setLoading(true);
setError(null);
const data = await testimonialService.getActiveTestimonials();
// Only show first 3 testimonials
setTestimonials(data.slice(0, 3));
} catch (err) {
console.error('Error fetching testimonials:', err);
setError('Failed to load testimonials. Please try again later.');
} finally {
setLoading(false);
}
};
// Loading state
if (loading) {
return (
<div className="py-12 px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 bg-white max-w-7xl mx-auto">
<div className="text-center py-20">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: '#012068' }}></div>
<p className="mt-4 text-gray-600">Loading testimonials...</p>
</div>
</div>
);
}
// Error state
if (error) {
return (
<div className="py-12 px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 bg-white max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h2 className="text-2xl md:text-3xl font-bold" style={{ color: '#012068' }}>
Patient Testimonials
</h2>
</div>
<div className="text-center py-20">
<p className="text-gray-600 mb-4">{error}</p>
<button
onClick={fetchTestimonials}
className="px-6 py-2 text-sm rounded-lg font-medium text-white"
style={{ backgroundColor: '#012068' }}
>
Try Again
</button>
</div>
</div>
);
}
// Empty state
if (testimonials.length === 0) {
return (
<div className="py-12 px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 bg-white max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h2 className="text-2xl md:text-3xl font-bold" style={{ color: '#012068' }}>
Patient Testimonials
</h2>
</div>
<div className="text-center py-8">
<p className="text-gray-500">No testimonials available.</p>
</div>
</div>
);
}
return (
<div className="py-12 px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 bg-white max-w-7xl mx-auto">
{/* Section Header */}
<div className="flex justify-between items-center mb-8">
<h2 className="text-2xl md:text-3xl font-bold" style={{ color: '#012068' }}>
Patient Testimonials
</h2>
</div>
{/* Testimonial Cards Grid - Fixed to show only 3 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{testimonials.map((testimonial) => (
<div
key={testimonial.id}
className="cursor-pointer"
onClick={() => window.location.href = `/testimonials/${testimonial.id}`}
>
{/* Card */}
<div className="rounded-lg overflow-hidden shadow-sm border border-gray-200 bg-white hover:shadow-md transition-shadow duration-200">
{/* Image/Story Content */}
<div
className="relative h-48 md:h-56 overflow-hidden"
style={{ backgroundColor: '#f4f4f4' }}
>
<div className="w-full h-full flex items-center justify-center p-6">
<div className="text-center">
<svg
className="w-16 h-16 mx-auto mb-3 opacity-30"
style={{ color: '#012068' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
<p className="text-xs leading-relaxed line-clamp-4 px-2" style={{ color: '#333' }}>
{testimonial.story}
</p>
</div>
</div>
</div>
{/* Card Content */}
<div className="p-4 md:p-5">
{/* Name and Age - As Heading */}
<h3 className="text-base md:text-lg font-semibold mb-2" style={{ color: '#012068' }}>
{testimonial.name}{testimonial.age ? `, ${testimonial.age}` : ''}
</h3>
{/* Category - As Date */}
<p className="text-xs mb-3" style={{ color: '#e64838' }}>
{testimonial.category || 'Patient Story'}
</p>
</div>
</div>
{/* Title Outside Card */}
<div className="mt-4">
<h4 className="text-sm font-medium leading-tight" style={{ color: '#012068' }}>
{testimonial.title || 'A Journey of Hope and Recovery'}
</h4>
</div>
</div>
))}
</div>
{/* View More Button */}
<div className="flex justify-center mt-8">
<button
className="px-6 py-2 rounded-lg text-sm font-medium text-white hover:opacity-90 transition-opacity duration-200"
style={{ backgroundColor: '#e64838' }}
onClick={() => window.location.href = '/testimonials'}
>
View more
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,268 @@
// Updated TraumaTestimonials component - No left panel, with breadcrumb
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { ChevronRight } from 'lucide-react';
import testimonialService, { Testimonial } from '../../services/testimonialService';
interface TraumaTestimonialsProps {
testimonialId?: string;
}
export default function TraumaTestimonials({ testimonialId }: TraumaTestimonialsProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchTestimonials();
}, []);
// Set active index when testimonialId changes
useEffect(() => {
if (testimonialId && testimonials.length > 0) {
const index = testimonials.findIndex(t => t.id.toString() === testimonialId);
if (index !== -1) {
setActiveIndex(index);
}
}
}, [testimonialId, testimonials]);
const fetchTestimonials = async () => {
try {
setLoading(true);
setError(null);
const data = await testimonialService.getActiveTestimonials();
setTestimonials(data);
} catch (err) {
console.error('Error fetching testimonials:', err);
setError('Failed to load testimonials. Please try again later.');
} finally {
setLoading(false);
}
};
const nextTestimonial = () => {
if (activeIndex < testimonials.length - 1) {
const nextIndex = activeIndex + 1;
setActiveIndex(nextIndex);
// Update URL without page reload
window.history.pushState({}, '', `/testimonials/${testimonials[nextIndex].id}`);
}
};
const prevTestimonial = () => {
if (activeIndex > 0) {
const prevIndex = activeIndex - 1;
setActiveIndex(prevIndex);
// Update URL without page reload
window.history.pushState({}, '', `/testimonials/${testimonials[prevIndex].id}`);
}
};
if (loading) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-3 border-gray-200 border-t-transparent" style={{ borderTopColor: '#012068' }}></div>
</div>
</div>
);
}
if (error || testimonials.length === 0) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<p className="text-gray-600">{error || 'No testimonials available.'}</p>
</div>
);
}
const current = testimonials[activeIndex];
return (
<div className="bg-white">
{/* Breadcrumb Section */}
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6">
<nav className="flex items-center space-x-2 text-sm">
<Link
href="/"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Home
</Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<Link
href="/testimonials"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
Testimonials
</Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}>
{current.name}
</span>
</nav>
</div>
</section>
{/* Top Section */}
<div className="border-b border-gray-100">
<div className="max-w-7xl mx-auto px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 py-12">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl md:text-3xl font-bold" style={{ color: '#012068' }}>
Patient Stories
</h1>
<p className="mt-2 text-xs" style={{ color: '#333' }}>Stories of hope and recovery</p>
</div>
<a
href="https://givecmc.org/trauma-centre/"
target="_blank"
rel="noopener noreferrer"
className="hidden md:inline-block px-8 py-3 text-sm font-medium text-white hover:opacity-90 transition-opacity"
style={{ backgroundColor: '#e64838' }}
>
Donate Now
</a>
</div>
</div>
</div>
{/* Main Content - Single Column */}
<div className="max-w-5xl mx-auto px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 py-12">
{/* Category Badge */}
<div className="inline-block px-3 py-1 text-xs font-medium mb-8 rounded-full"
style={{
backgroundColor: 'rgba(230, 72, 56, 0.08)',
color: '#e64838'
}}>
{current.category}
</div>
{/* Title */}
<h2 className="text-2xl md:text-3xl font-bold mb-4" style={{ color: '#012068' }}>
{current.name}, {current.age}
</h2>
<p className="text-base leading-relaxed mb-12" style={{ color: '#666' }}>
{current.title}
</p>
{/* Content Sections */}
<div className="space-y-12">
<div>
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-px" style={{ backgroundColor: '#e64838' }}></div>
<h3 className="text-sm font-semibold tracking-widest" style={{ color: '#012068' }}>
THE JOURNEY
</h3>
</div>
<p className="text-base leading-relaxed" style={{ color: '#333' }}>
{current.story}
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-px" style={{ backgroundColor: '#e64838' }}></div>
<h3 className="text-sm font-semibold tracking-widest" style={{ color: '#012068' }}>
THE OUTCOME
</h3>
</div>
<p className="text-base leading-relaxed" style={{ color: '#333' }}>
{current.outcome}
</p>
</div>
<div className="bg-gray-50 p-8 border-l-4" style={{ borderColor: '#e64838' }}>
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-px" style={{ backgroundColor: '#e64838' }}></div>
<h3 className="text-sm font-semibold tracking-widest" style={{ color: '#012068' }}>
COMMUNITY IMPACT
</h3>
</div>
<p className="text-base leading-relaxed" style={{ color: '#333' }}>
{current.impact}
</p>
</div>
</div>
{/* Navigation */}
<div className="flex items-center justify-between mt-16 pt-8 border-t border-gray-100">
<button
onClick={prevTestimonial}
disabled={activeIndex === 0}
className="flex items-center gap-2 text-sm font-medium disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-70 transition-opacity"
style={{ color: '#012068' }}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Previous Story
</button>
<div className="text-sm" style={{ color: '#666' }}>
{activeIndex + 1} of {testimonials.length}
</div>
<button
onClick={nextTestimonial}
disabled={activeIndex === testimonials.length - 1}
className="flex items-center gap-2 text-sm font-medium disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-70 transition-opacity"
style={{ color: '#012068' }}
>
Next Story
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
{/* Back to All Stories Link */}
<div className="mt-8 text-center">
<Link
href="/testimonials"
className="inline-flex items-center gap-2 text-sm font-medium hover:opacity-70 transition-opacity"
style={{ color: '#012068' }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to All Stories
</Link>
</div>
</div>
{/* Bottom CTA */}
<div className="border-t border-gray-100 bg-gray-50">
<div className="max-w-7xl mx-auto px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 py-12">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h3 className="text-xl font-semibold mb-2" style={{ color: '#012068' }}>
Support Emergency Trauma Care
</h3>
<p className="text-sm leading-relaxed" style={{ color: '#333' }}>
Your donation ensures trauma victims receive immediate, life-saving treatment.
</p>
</div>
<a
href="https://givecmc.org/trauma-centre/"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-3 text-sm font-medium text-white hover:opacity-90 transition-opacity whitespace-nowrap"
style={{ backgroundColor: '#e64838' }}
>
Donate Now
</a>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,74 @@
'use client'
import React from 'react';
import Link from 'next/link';
import { ChevronRight } from 'lucide-react';
interface BreadcrumbItem {
label: string;
href?: string;
isActive?: boolean;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
title: string;
description?: string;
className?: string;
}
const Breadcrumb: React.FC<BreadcrumbProps> = ({
items,
title,
description,
className = ""
}) => {
return (
<section className={`py-4 ${className}`} style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4">
{/* Breadcrumb Navigation */}
<nav className="flex items-center space-x-2 text-sm">
{items.map((item, index) => (
<React.Fragment key={index}>
{item.href && !item.isActive ? (
<Link
href={item.href}
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
{item.label}
</Link>
) : (
<span
className={item.isActive ? "font-medium" : ""}
style={{ color: item.isActive ? '#e64838' : '#012068' }}
>
{item.label}
</span>
)}
{index < items.length - 1 && (
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
)}
</React.Fragment>
))}
</nav>
{/* Page Header */}
<div className="mt-6">
<div className="flex items-center mb-4">
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
{title}
</h1>
</div>
{description && (
<p className="text-base max-w-4xl leading-relaxed"style={{ color: '#333' }}>
{description}
</p>
)}
</div>
</div>
</section>
);
};
export default Breadcrumb;

View File

@ -0,0 +1,276 @@
'use client'
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { ChevronRight } from 'lucide-react';
import testimonialService, { Testimonial } from '@/services/testimonialService';
import TraumaStats from './TraumaStats';
interface TestimonialsListingProps {
title?: string;
onTestimonialClick?: (testimonial: Testimonial) => void;
}
const TestimonialsListing: React.FC<TestimonialsListingProps> = ({
title = "Patient Stories",
onTestimonialClick
}) => {
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadTestimonials = async () => {
try {
setLoading(true);
const data = await testimonialService.getActiveTestimonials();
setTestimonials(data);
setError(null);
} catch (err) {
console.error('Failed to load testimonials:', err);
setError('Failed to load testimonials. Please try again later.');
} finally {
setLoading(false);
}
};
loadTestimonials();
}, []);
// Group testimonials by category
const traumaCases = testimonials.filter(t => t.category === 'Trauma' || t.category === 'Road Accident');
const surgicalCases = testimonials.filter(t => t.category === 'Surgery' || t.category === 'Surgical');
const otherCases = testimonials.filter(t => !['Trauma', 'Road Accident', 'Surgery', 'Surgical'].includes(t.category || ''));
const handleTestimonialClick = (testimonial: Testimonial) => {
if (onTestimonialClick) {
onTestimonialClick(testimonial);
} else {
window.location.href = `/testimonials/${testimonial.id}`;
}
};
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-600 mx-auto mb-4"></div>
<p>Loading testimonials...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<p className="text-red-600 mb-4">{error}</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Retry
</button>
</div>
</div>
);
}
const TestimonialCard: React.FC<{ testimonial: Testimonial }> = ({ testimonial }) => {
return (
<div
className="group cursor-pointer bg-white rounded-lg border border-gray-300 overflow-hidden hover:shadow-lg transition-all duration-300"
onClick={() => handleTestimonialClick(testimonial)}
>
<div className="relative aspect-square overflow-hidden">
<div
className="w-full h-full flex items-center justify-center p-6 transition-transform duration-300 group-hover:scale-105"
style={{ backgroundColor: '#f4f4f4' }}
>
<div className="text-center">
<svg
className="w-16 h-16 mx-auto mb-3 opacity-30"
style={{ color: '#012068' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
<p className="text-xs leading-relaxed line-clamp-4 px-2" style={{ color: '#333' }}>
{testimonial.story}
</p>
</div>
</div>
</div>
<div className="p-4 sm:p-6">
<h3 className="text-lg font-medium mb-2 group-hover:opacity-70 transition-opacity" style={{ color: '#012068' }}>
{testimonial.name}{testimonial.age ? `, ${testimonial.age}` : ''}
</h3>
<p className="text-sm leading-relaxed mb-1" style={{ color: '#e64838' }}>
{testimonial.category || 'Patient Story'}
</p>
{testimonial.title && (
<p className="text-xs mt-2 font-medium line-clamp-2" style={{ color: '#333' }}>
{testimonial.title}
</p>
)}
</div>
</div>
);
};
return (
<div className="min-h-screen">
{/* Breadcrumb Section */}
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
<div className="max-w-7xl mx-auto px-4">
<nav className="flex items-center space-x-2 text-sm">
<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' }}>
Testimonials
</span>
</nav>
<div className="mt-6">
<div className="flex items-center mb-4">
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
{title}
</h1>
</div>
<p className="text-base max-w-3xl leading-relaxed" style={{ color: '#333' }}>
Real stories of hope, recovery, and resilience from patients treated at Christian Medical College, Vellore. Each story represents the dedication of our medical team and the strength of our patients.
</p>
</div>
</div>
</section>
{/* Stats Section */}
<TraumaStats />
{/* Trauma Cases Section */}
{traumaCases.length > 0 && (
<section className="py-4" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4">
<div className="mb-6">
<h2 className="text-2xl font-bold mb-2" style={{ color: '#012068' }}>
Trauma & Emergency Cases
</h2>
<p className="text-sm" style={{ color: '#666' }}>
Stories of survival and recovery from critical trauma and emergency situations
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{traumaCases.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
</div>
</section>
)}
{/* Surgical Cases Section */}
{surgicalCases.length > 0 && (
<section className="py-8" style={{ backgroundColor: '#f9f9f9' }}>
<div className="max-w-7xl mx-auto px-4">
<div className="mb-8">
<h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}>
Surgical Success Stories
</h2>
<p className="text-sm" style={{ color: '#666' }}>
Remarkable journeys through complex surgical procedures and recovery
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{surgicalCases.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
</div>
</section>
)}
{/* Other Cases Section */}
{otherCases.length > 0 && (
<section className="py-4" style={{ backgroundColor: '#fff' }}>
<div className="max-w-7xl mx-auto px-4">
<div className="mb-8">
<h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}>
More Patient Stories
</h2>
<p className="text-sm" style={{ color: '#666' }}>
Additional testimonials from patients across various medical specialties
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{otherCases.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
</div>
</section>
)}
{/* Show message if no testimonials */}
{testimonials.length === 0 && !loading && (
<section className="py-16 text-center">
<div className="max-w-2xl mx-auto px-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}>
No Testimonials Available
</h2>
<p className="text-gray-600 mb-6">
Patient testimonials are currently being updated. Please check back later.
</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 text-white rounded-lg hover:opacity-90 transition-opacity"
style={{ backgroundColor: '#012068' }}
>
Refresh Page
</button>
</div>
</section>
)}
{/* Bottom CTA Section */}
<div className="border-t border-gray-100 bg-gray-50">
<div className="max-w-7xl mx-auto px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 py-12">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h3 className="text-xl font-semibold mb-2" style={{ color: '#012068' }}>
Support Emergency Trauma Care
</h3>
<p className="text-sm leading-relaxed" style={{ color: '#333' }}>
Your donation ensures trauma victims receive immediate, life-saving treatment.
</p>
</div>
<a
href="https://givecmc.org/trauma-centre/"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-3 text-sm font-medium text-white hover:opacity-90 transition-opacity whitespace-nowrap"
style={{ backgroundColor: '#e64838' }}
>
Donate Now
</a>
</div>
</div>
</div>
</div>
);
};
export default TestimonialsListing;

View File

@ -0,0 +1,32 @@
'use client'
import React from 'react';
export default function TraumaStats() {
const stats = [
{ label: "Road Accident Victims Treated Annually", value: "Over 2,500" },
{ label: "Trauma Bed Capacity", value: "112" },
{ label: "Operation Theatres (Trauma)", value: "6" },
{ label: "24x7 Consultant Availability", value: "Yes" },
];
return (
<div className="max-w-7xl mx-auto px-6 sm:px-8 md:px-6 lg:px-6 xl:px-6 py-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-2">
{stats.map((stat, idx) => (
<div
key={idx}
className="px-4 py-4 rounded-lg"
style={{ backgroundColor: '#f4f4f4' }}
>
<div className="text-2xl font-semibold mb-1" style={{ color: '#012068' }}>
{stat.value}
</div>
<div className="text-xs" style={{ color: '#333' }}>
{stat.label}
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -18,6 +18,7 @@ export interface Event {
phone: string; phone: string;
email: string; email: string;
isActive: boolean; isActive: boolean;
bookSeatLink?: string; // NEW FIELD
professors?: Professor[]; professors?: Professor[];
} }

View File

@ -0,0 +1,71 @@
// services/milestoneService.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
export interface Milestone {
id: number;
title: string;
description: string;
displayOrder: number;
isActive: boolean;
milestoneDate: string | null;
createdAt: string;
updatedAt: string;
}
/**
* Fetch all active milestones from the API
* Uses the public endpoint that doesn't require authentication
*/
export async function getActiveMilestones(): Promise<Milestone[]> {
try {
const response = await fetch(`${API_BASE_URL}/api/milestones/public`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store', // Disable caching for fresh data
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching milestones:', error);
// Return empty array as fallback
return [];
}
}
/**
* Fetch all milestones (requires authentication)
*/
export async function getAllMilestones(token?: string): Promise<Milestone[]> {
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}/api/milestones`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching all milestones:', error);
return [];
}
}

View File

@ -0,0 +1,174 @@
// src/services/testimonialService.ts
export interface Testimonial {
id: number;
name: string;
age: number;
title: string;
story: string;
outcome: string;
impact: string;
category: string;
isActive: boolean;
createdAt?: Date;
updatedAt?: Date;
}
class TestimonialService {
private baseUrl: string;
constructor() {
// Use environment variable or default to localhost
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
}
/**
* Get all active testimonials (for public website)
*/
async getActiveTestimonials(): Promise<Testimonial[]> {
try {
const response = await fetch(`${this.baseUrl}/api/testimonials/public`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store', // Always fetch fresh data
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching active testimonials:', error);
throw error;
}
}
/**
* Get all testimonials (for admin)
*/
async getAllTestimonials(): Promise<Testimonial[]> {
try {
const response = await fetch(`${this.baseUrl}/api/testimonials`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching all testimonials:', error);
throw error;
}
}
/**
* Get testimonial by ID
*/
async getTestimonialById(id: number): Promise<Testimonial> {
try {
const response = await fetch(`${this.baseUrl}/api/testimonials/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Error fetching testimonial ${id}:`, error);
throw error;
}
}
/**
* Create new testimonial (admin only)
*/
async createTestimonial(testimonial: Omit<Testimonial, 'id' | 'createdAt' | 'updatedAt'>): Promise<Testimonial> {
try {
const response = await fetch(`${this.baseUrl}/api/testimonials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(testimonial),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error creating testimonial:', error);
throw error;
}
}
/**
* Update testimonial (admin only)
*/
async updateTestimonial(id: number, testimonial: Omit<Testimonial, 'id' | 'createdAt' | 'updatedAt'>): Promise<Testimonial> {
try {
const response = await fetch(`${this.baseUrl}/api/testimonials/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(testimonial),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Error updating testimonial ${id}:`, error);
throw error;
}
}
/**
* Delete testimonial (admin only)
*/
async deleteTestimonial(id: number): Promise<void> {
try {
const response = await fetch(`${this.baseUrl}/api/testimonials/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error(`Error deleting testimonial ${id}:`, error);
throw error;
}
}
}
// Export singleton instance
const testimonialService = new TestimonialService();
export default testimonialService;