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 Introduction from "../../components/about/Introduction";
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() {
const breadcrumbItems = [
@ -24,9 +21,9 @@ export default function Home() {
<Introduction />
<MissionVision />
<Process />
<Services />
{/* <Services />
<StatisticsTiles />
<PatientCareCards />
<PatientCareCards /> */}
<Footer />
</>
);

View File

@ -1,13 +1,13 @@
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
import { Footer } from "../../components/Layouts/Footer"
import EducationTraining from "../../components/education/EducationTraining";
import AcademicResearch from "@/components/education/EducationTraining";
export default function contact() {
return (
<>
<Header />
<EducationTraining/>
<AcademicResearch/>
<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 HeroSection from "../components/home/HeroSection";
import EventsSection from "../components/home/EventSection";
import PatientTestimonialSlider from "../components/home/PatientTestimonialSlider";
export default function faculty() {
return (
@ -9,6 +10,7 @@ export default function faculty() {
<Header />
<HeroSection/>
<EventsSection/>
<PatientTestimonialSlider/>
<Footer />
</div>
);

View File

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

View File

@ -14,11 +14,11 @@ const Header = () => {
const menuItems = [
{ href: "/", label: "Home" },
{ href: "/about", label: "About" },
{ href: "/teamMember", label: "Faculty Team" },
{ href: "/education-training", label: "Academics" },
{ href: "/research", label: "Research" },
{ href: "/teamMember", label: "Our Team" },
{ href: "/education-training", label: "Academics & Research" },
{ href: "/services", label: "Services" },
{ href: "/events", label: "Events" },
{ href: "/publications", label: "Publications" },
{ href: "/publications", label: "Blogs" },
{ href: "/career", label: "Career" },
{ href: "/contact", label: "Contact Us" },
];
@ -32,7 +32,7 @@ const Header = () => {
<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">
<Image
src="/images/cmclogo1.svg"
src="/images/cmclogo2.svg"
alt="CMC Logo"
fill
className="object-fill"
@ -43,12 +43,12 @@ const Header = () => {
</div>
{/* 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) => (
<Link
key={idx}
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}
>
{item.label}
@ -69,8 +69,8 @@ const Header = () => {
priority
/>
</div>
<span className="text-xs sm:text-sm font-semibold text-blue-900 mt-1">
H - 2 0 2 4 - 1 4 3 1
<span className="text-xs sm:text-sm font-semibold text-blue-900 mt-1 whitespace-nowrap">
H-2024-1431
</span>
</div>

View File

@ -3,11 +3,11 @@ import { Building2, Home } from 'lucide-react';
const PatientCareCards = () => {
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="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
{/* 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="w-10 h-10 rounded-full flex items-center justify-center mr-3"
@ -26,7 +26,7 @@ const PatientCareCards = () => {
</div>
{/* 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="w-10 h-10 rounded-full flex items-center justify-center mr-3"

View File

@ -5,7 +5,7 @@ const Services = () => {
const services = [
{
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."
},
{
@ -26,7 +26,7 @@ const Services = () => {
];
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">
<h2 className="text-2xl sm:text-3xl font-semibold text-center mb-2" style={{ color: '#012068' }}>
Our Services
@ -38,7 +38,7 @@ const Services = () => {
{services.map((service, index) => (
<div
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

View File

@ -26,7 +26,7 @@ const StatisticsTiles = () => {
];
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">
<h2
className="text-2xl sm:text-3xl font-semibold text-center mb-8 sm:mb-12"
@ -39,7 +39,7 @@ const StatisticsTiles = () => {
<div
key={index}
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 items-center mb-3">

View File

@ -1,11 +1,74 @@
'use client'
import React, { useState, useEffect, useRef } from "react";
import { getActiveMilestones, type Milestone } from "../../services/milestoneService";
// Define CalendarIcon at the top level
const CalendarIcon = (
<svg
width={34}
height={34}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
);
export default function Process() {
const [activeStep, setActiveStep] = useState(-1);
const [scrollProgress, setScrollProgress] = useState(0);
const [activeStep, setActiveStep] = useState(0);
const [milestones, setMilestones] = useState<Milestone[]>([]);
const [loading, setLoading] = useState(true);
const containerRef = useRef<HTMLDivElement>(null);
const timelineRef = useRef<HTMLDivElement>(null);
const stepRefs = useRef<HTMLDivElement[]>([]);
// Fetch milestones from API
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 = () => {
@ -14,23 +77,45 @@ export default function Process() {
const container = containerRef.current;
const timeline = timelineRef.current;
const rect = container.getBoundingClientRect();
const timelineHeight = timeline.scrollHeight - timeline.clientHeight;
const windowHeight = window.innerHeight;
const containerHeight = container.offsetHeight;
// 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);
// 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
setScrollProgress(1);
// After sticky zone
const timelineHeight = timeline.scrollHeight - timeline.clientHeight;
timeline.scrollTop = timelineHeight;
setActiveStep(steps.length - 1);
}
};
@ -38,167 +123,47 @@ export default function Process() {
handleScroll(); // Initial check
return () => window.removeEventListener('scroll', handleScroll);
}, []);
}, [steps.length]);
const CalendarIcon = (
<svg
width={34}
height={34}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
);
// Loading state
if (loading) {
return (
<div className="bg-white py-8 sm:py-12 min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 mb-4" style={{ borderColor: "#012068" }}></div>
<p className="text-lg" style={{ color: "#012068" }}>Loading milestones...</p>
</div>
</div>
);
}
const steps = [
{
number: "1",
title: "2017",
description: "Formation of Trauma Services",
icon: CalendarIcon,
},
{
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,
},
];
// 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>
);
}
// Calculate extra height needed for scroll effect
const extraHeight = 2000;
// For small number of items (1-3), use static layout without sticky scroll
const isSmallList = steps.length <= 3;
return (
<div
ref={containerRef}
className="bg-white py-10 sm:py-10"
style={{ minHeight: `calc(100vh + ${extraHeight}px)` }}
>
<div className="sticky top-0 h-screen flex items-start pt-12 sm:pt-20">
<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">
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 */}
<div className="flex flex-col justify-start">
<div>
@ -211,123 +176,204 @@ export default function Process() {
>
Our Journey in Trauma Care
</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.
</p>
</div>
</div>
{/* Right Column - Auto-scrolling Timeline */}
<div className="relative h-[600px]">
<div className="relative h-[70vh] max-h-[600px]">
<div
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={{
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}}
>
{steps.map((step, index) => (
<div
key={index}
className={`relative group transition-all duration-500 ${
index !== 0 ? "mt-8 sm:mt-12" : ""
} ${index === 0 ? "pt-2" : ""}`}
onMouseEnter={() => setActiveStep(index)}
onMouseLeave={() => setActiveStep(-1)}
>
{/* Connecting Line */}
{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={`w-full transition-all duration-700 ease-out origin-bottom ${
activeStep >= index ||
(activeStep === -1 && index === 0)
? "h-full scale-y-100"
: "h-0 scale-y-0"
}`}
style={{ backgroundColor: "#012068" }}
/>
</div>
)}
<div className="py-4">
{steps.map((step, index) => (
<div
key={index}
ref={(el) => {
if (el) {
stepRefs.current[index] = el;
}
}}
className={`relative group transition-all duration-500 ${
index !== 0 ? "mt-10" : ""
}`}
>
{/* 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 transition-all duration-700 ease-out origin-top ${
activeStep >= index ? "h-full scale-y-100" : "h-0 scale-y-0"
}`}
style={{ backgroundColor: "#012068" }}
/>
</div>
)}
{/* Step Content */}
<div className="flex items-start gap-4 sm:gap-6">
{/* 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 ||
(activeStep === -1 && index === 0)
? "scale-110 shadow-lg ring-4"
: "scale-100 hover:scale-105"
}`}
style={{
backgroundColor:
activeStep === index ||
(activeStep === -1 && index === 0)
? "#012068"
: "#333",
"--tw-ring-color":
activeStep === index ||
(activeStep === -1 && index === 0)
? "#012068" + "33"
: "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 ${
activeStep === index ||
(activeStep === -1 && index === 0)
? "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 ||
(activeStep === -1 && index === 0)
? "#012068"
: "#f4f4f4",
color:
activeStep === index ||
(activeStep === -1 && index === 0)
? "white"
: "#333",
}}
>
{/* Icon */}
{/* Step Content */}
<div className="flex items-start gap-4 sm:gap-6">
{/* Number Circle */}
<div
className={`mb-4 transition-all duration-500 transform ${
activeStep === index ||
(activeStep === -1 && index === 0)
? "text-white"
: "scale-100 group-hover:scale-105"
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"
}`}
style={{
color:
activeStep === index ||
(activeStep === -1 && index === 0)
? "white"
: "black",
}}
backgroundColor: activeStep === index ? "#012068" : "#333",
"--tw-ring-color": activeStep === index ? "#01206833" : "transparent",
} as React.CSSProperties}
>
{step.icon}
{step.number}
</div>
{/* Text */}
<h3 className="text-lg sm:text-xl font-semibold mb-3 transition-all duration-300">
{step.title}
</h3>
<p className="text-sm sm:text-base leading-relaxed transition-all duration-500">
{step.description}
</p>
{/* 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"
}`}
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>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,6 @@ const EventDetail = () => {
const [eventData, setEventData] = useState<Event | null>(null);
const [isBookmarked, setIsBookmarked] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [bookingStatus, setBookingStatus] = useState<'idle' | 'booking' | 'success' | 'error'>('idle');
useEffect(() => {
const fetchEventData = async () => {
@ -25,7 +24,6 @@ const EventDetail = () => {
console.log('Fetching event with ID:', eventId);
setIsLoading(true);
try {
// Convert string ID to number for API call
const numericId = parseInt(eventId, 10);
if (isNaN(numericId)) {
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;
setBookingStatus('booking');
try {
// Replace with actual booking API call
await new Promise(resolve => setTimeout(resolve, 1500));
setBookingStatus('success');
setTimeout(() => setBookingStatus('idle'), 2000);
} catch (error) {
setBookingStatus('error');
setTimeout(() => setBookingStatus('idle'), 2000);
// Check if admin has provided a custom booking link
if (eventData.bookSeatLink && eventData.bookSeatLink.trim() !== '') {
// Validate if it's a proper URL
try {
// If the URL doesn't start with http:// or https://, add https://
let url = eventData.bookSeatLink;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
// Open in new tab
window.open(url, '_blank', 'noopener,noreferrer');
} catch (error) {
console.error('Invalid booking link:', error);
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();
};
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
const formatPrice = (event: Event) => {
if (event.fee && event.fee.length > 0) {
@ -171,7 +162,6 @@ const EventDetail = () => {
return fallbackImages;
}
// Ensure we have at least 2 images for the layout
while (validImages.length < 2) {
validImages.push(fallbackImages[validImages.length % fallbackImages.length]);
}
@ -179,7 +169,6 @@ const EventDetail = () => {
return validImages.slice(0, 2);
};
// Format description data for display
const getFormattedDescription = (event: Event) => {
return {
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) {
return (
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
{/* 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' }}
>
<Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
Home
</Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<a
href="/events"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
<a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
Medical Events
</a>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}>
Loading...
</span>
<span className="font-medium" style={{ color: '#e64838' }}>Loading...</span>
</nav>
</div>
</section>
@ -259,29 +237,18 @@ const EventDetail = () => {
if (!eventData) {
return (
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
{/* 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' }}
>
<Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
Home
</Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<a
href="/events"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
<a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
Medical Events
</a>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<span className="font-medium" style={{ color: '#e64838' }}>
Event Not Found
</span>
<span className="font-medium" style={{ color: '#e64838' }}>Event Not Found</span>
</nav>
</div>
</section>
@ -317,19 +284,11 @@ const EventDetail = () => {
<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' }}
>
<Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
Home
</Link>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
<a
href="/events"
className="hover:opacity-70 transition-opacity duration-200"
style={{ color: '#012068' }}
>
<a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
Medical Events
</a>
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
@ -400,14 +359,10 @@ const EventDetail = () => {
</div>
<button
onClick={handleBookSeat}
disabled={bookingStatus === 'booking'}
className={`w-full sm:w-auto px-6 py-2 text-sm rounded-lg transition-all duration-200 ${getBookingButtonStyle()}`}
style={{
backgroundColor: bookingStatus === 'idle' ? '#012068' : undefined,
color: '#f4f4f4'
}}
className="w-full sm:w-auto px-6 py-2 text-sm rounded-lg transition-all duration-200 hover:opacity-90"
style={{ backgroundColor: '#012068', color: '#f4f4f4' }}
>
{getBookingButtonText()}
Book Your Seat
</button>
</div>
</div>

View File

@ -76,77 +76,106 @@ const TeamListing: React.FC<TeamListingProps> = ({
}
const TeamMemberCard: React.FC<{ member: TeamMember }> = ({ member }) => {
const [imageError, setImageError] = useState(false);
const [imageLoading, setImageLoading] = useState(true);
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
const target = e.target as HTMLImageElement;
console.error('Image failed to load:', member.image);
console.error('Member:', member.name, 'Professor ID:', member.professorId);
const [imageError, setImageError] = useState(false);
const [imageLoading, setImageLoading] = useState(true);
const isTrainee = member.category === 'TRAINEE_FELLOW';
if (!imageError) {
// 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>) => {
console.error('Image failed to load:', member.image);
console.error('Member:', member.name, 'Professor ID:', member.professorId);
setImageError(true);
target.src = '/images/default-avatar.jpg';
}
};
setImageLoading(false);
};
const handleImageLoad = () => {
console.log('Image loaded successfully:', member.image);
setImageLoading(false);
};
const handleImageLoad = () => {
console.log('Image loaded successfully:', member.image);
setImageLoading(false);
};
return (
<div
className="group cursor-pointer bg-white rounded-lg border border-gray-300 overflow-hidden hover:shadow-lg transition-all duration-300"
onClick={() => handleMemberClick(member)}
>
<div className="relative aspect-square overflow-hidden">
{imageLoading && (
<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>
return (
<div
className={`bg-white rounded-lg border border-gray-300 overflow-hidden transition-all duration-300 ${
!isTrainee ? 'group cursor-pointer hover:shadow-lg' : 'cursor-default'
}`}
onClick={() => !isTrainee && handleMemberClick(member)}
>
{/* Only show image section for non-trainees */}
{!isTrainee && (
<div className="relative aspect-square overflow-hidden bg-gray-50">
{shouldShowImage && !imageError ? (
<>
{imageLoading && (
<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>
)}
<img
src={member.image}
alt={member.name}
className={`w-full h-full object-cover transition-transform duration-300 group-hover:scale-105 ${
imageLoading ? 'opacity-0' : 'opacity-100'
}`}
onError={handleImageError}
onLoad={handleImageLoad}
loading="lazy"
/>
</>
) : (
<div
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>
)}
<img
src={member.image}
alt={member.name}
className={`w-full h-full object-cover transition-transform duration-300 group-hover:scale-105 ${
imageLoading ? 'opacity-0' : 'opacity-100'
}`}
onError={handleImageError}
onLoad={handleImageLoad}
loading="lazy"
/>
{imageError && (
<div className="absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded">
Default
</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' }}>
{member.name}
</h3>
<p className="text-sm leading-relaxed" style={{ color: '#e64838' }}>
{member.position}
</p>
{member.department && (
<p className="text-xs mt-1" style={{ color: '#666' }}>
{member.department}
<div className="p-4 sm:p-6">
<h3 className="text-lg font-medium mb-2 group-hover:opacity-70 transition-opacity" style={{ color: '#012068' }}>
{member.name}
</h3>
<p className="text-sm leading-relaxed" style={{ color: '#e64838' }}>
{member.position}
</p>
)}
{member.specialty && (
<p className="text-xs mt-1 font-medium" style={{ color: '#333' }}>
{member.specialty}
</p>
)}
{!isTrainee && (
<>
{member.department && (
<p className="text-xs mt-1" style={{ color: '#666' }}>
{member.department}
</p>
)}
{member.specialty && (
<p className="text-xs mt-1 font-medium" style={{ color: '#333' }}>
{member.specialty}
</p>
)}
</>
)}
</div>
</div>
</div>
);
};
);
};
return (
<div className="min-h-screen">
@ -233,7 +262,7 @@ const TeamListing: React.FC<TeamListingProps> = ({
<h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}>
Trainees & Fellows
</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
</p>
</div>

View File

@ -21,6 +21,7 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
const [loading, setLoading] = useState(!memberData);
const [error, setError] = useState<string | null>(null);
const [showSocialShare, setShowSocialShare] = useState(false);
const [imageError, setImageError] = useState(false);
useEffect(() => {
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
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
@ -87,6 +103,11 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
: member.phone;
};
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
console.error('Image failed to load:', member.image);
setImageError(true);
};
return (
<div className="min-h-screen bg-white">
{/* Breadcrumb Section */}
@ -120,6 +141,11 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
{member.name}
</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>
<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
@ -134,17 +160,29 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
{/* Sidebar */}
<div className="xl:col-span-1">
<div className="bg-white rounded-md border border-gray-300 overflow-hidden sticky top-8">
{/* Profile Image */}
<div className="aspect-square relative overflow-hidden">
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-300"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = '/images/default-avatar.jpg';
}}
/>
{/* Profile Image or Avatar */}
<div className="aspect-square relative overflow-hidden bg-gray-50">
{shouldShowImage ? (
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-300"
onError={handleImageError}
/>
) : (
<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>
{/* Profile Info */}
@ -160,7 +198,7 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
</div>
<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>
<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;
email: string;
isActive: boolean;
bookSeatLink?: string; // NEW FIELD
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;