Further updates on 03-11-2025
This commit is contained in:
12
public/images/cmclogo2.svg
Normal file
12
public/images/cmclogo2.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 353 KiB |
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
29
src/app/services/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
src/app/testimonials/[id]/page.tsx
Normal file
18
src/app/testimonials/[id]/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/app/testimonials/page.tsx
Normal file
13
src/app/testimonials/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
164
src/components/home/PatientTestimonialSlider.tsx
Normal file
164
src/components/home/PatientTestimonialSlider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
268
src/components/home/TraumaTestimonials.tsx
Normal file
268
src/components/home/TraumaTestimonials.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
74
src/components/services/Breadcrumb.tsx
Normal file
74
src/components/services/Breadcrumb.tsx
Normal 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;
|
||||
276
src/components/testimonials/TestimonialsListing.tsx
Normal file
276
src/components/testimonials/TestimonialsListing.tsx
Normal 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;
|
||||
32
src/components/testimonials/TraumaStats.tsx
Normal file
32
src/components/testimonials/TraumaStats.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -18,6 +18,7 @@ export interface Event {
|
||||
phone: string;
|
||||
email: string;
|
||||
isActive: boolean;
|
||||
bookSeatLink?: string; // NEW FIELD
|
||||
professors?: Professor[];
|
||||
}
|
||||
|
||||
|
||||
71
src/services/milestoneService.ts
Normal file
71
src/services/milestoneService.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
174
src/services/testimonialService.ts
Normal file
174
src/services/testimonialService.ts
Normal 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;
|
||||
Reference in New Issue
Block a user