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 Breadcrumb from "../../components/about/AboutBreadcrumb";
|
||||||
import Introduction from "../../components/about/Introduction";
|
import Introduction from "../../components/about/Introduction";
|
||||||
import MissionVision from "../../components/about/MissionVision";
|
import MissionVision from "../../components/about/MissionVision";
|
||||||
import Services from "../../components/about/Services";
|
|
||||||
import StatisticsTiles from "../../components/about/StatisticsTiles";
|
|
||||||
import PatientCareCards from "../../components/about/PatientCareCards";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
@ -24,9 +21,9 @@ export default function Home() {
|
|||||||
<Introduction />
|
<Introduction />
|
||||||
<MissionVision />
|
<MissionVision />
|
||||||
<Process />
|
<Process />
|
||||||
<Services />
|
{/* <Services />
|
||||||
<StatisticsTiles />
|
<StatisticsTiles />
|
||||||
<PatientCareCards />
|
<PatientCareCards /> */}
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
|
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||||
import { Footer } from "../../components/Layouts/Footer"
|
import { Footer } from "../../components/Layouts/Footer"
|
||||||
import EducationTraining from "../../components/education/EducationTraining";
|
import AcademicResearch from "@/components/education/EducationTraining";
|
||||||
|
|
||||||
|
|
||||||
export default function contact() {
|
export default function contact() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<EducationTraining/>
|
<AcademicResearch/>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import Header from "../components/Layouts/Header"; // Adjust path based on your
|
|||||||
import { Footer } from "../components/Layouts/Footer";
|
import { Footer } from "../components/Layouts/Footer";
|
||||||
import HeroSection from "../components/home/HeroSection";
|
import HeroSection from "../components/home/HeroSection";
|
||||||
import EventsSection from "../components/home/EventSection";
|
import EventsSection from "../components/home/EventSection";
|
||||||
|
import PatientTestimonialSlider from "../components/home/PatientTestimonialSlider";
|
||||||
|
|
||||||
export default function faculty() {
|
export default function faculty() {
|
||||||
return (
|
return (
|
||||||
@ -9,6 +10,7 @@ export default function faculty() {
|
|||||||
<Header />
|
<Header />
|
||||||
<HeroSection/>
|
<HeroSection/>
|
||||||
<EventsSection/>
|
<EventsSection/>
|
||||||
|
<PatientTestimonialSlider/>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Footer } from "../../components/Layouts/Footer"
|
|||||||
import ResearchComponent from "../../components/research/ResearchComponent";
|
import ResearchComponent from "../../components/research/ResearchComponent";
|
||||||
|
|
||||||
|
|
||||||
export default function contact() {
|
export default function research() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
29
src/app/services/page.tsx
Normal file
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">
|
<ul className="space-y-3">
|
||||||
{[
|
{[
|
||||||
{ label: 'About CMC', href: '/about' },
|
{ label: 'About CMC', href: '/about' },
|
||||||
{ label: 'Faculty Team', href: '/teamMember' },
|
{ label: 'Our Team', href: '/teamMember' },
|
||||||
{ label: 'Academics', href: '/education-training' },
|
{ label: 'Academics & Research', href: '/education-training' },
|
||||||
{ label: 'Research', href: '/research'},
|
{ label: 'Services', href: '/services'},
|
||||||
].map((link) => (
|
].map((link) => (
|
||||||
<li key={link.label}>
|
<li key={link.label}>
|
||||||
<a
|
<a
|
||||||
@ -55,7 +55,7 @@ export function Footer() {
|
|||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{[
|
{[
|
||||||
{ label: 'Events', href: '/events' },
|
{ label: 'Events', href: '/events' },
|
||||||
{ label: 'Publications', href: '/publications'},
|
{ label: 'Blogs', href: '/publications'},
|
||||||
{ label: 'Career', href: '/career' },
|
{ label: 'Career', href: '/career' },
|
||||||
{ label: 'Contact us', href: '/contact' },
|
{ label: 'Contact us', href: '/contact' },
|
||||||
].map((link) => (
|
].map((link) => (
|
||||||
|
|||||||
@ -14,11 +14,11 @@ const Header = () => {
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ href: "/", label: "Home" },
|
{ href: "/", label: "Home" },
|
||||||
{ href: "/about", label: "About" },
|
{ href: "/about", label: "About" },
|
||||||
{ href: "/teamMember", label: "Faculty Team" },
|
{ href: "/teamMember", label: "Our Team" },
|
||||||
{ href: "/education-training", label: "Academics" },
|
{ href: "/education-training", label: "Academics & Research" },
|
||||||
{ href: "/research", label: "Research" },
|
{ href: "/services", label: "Services" },
|
||||||
{ href: "/events", label: "Events" },
|
{ href: "/events", label: "Events" },
|
||||||
{ href: "/publications", label: "Publications" },
|
{ href: "/publications", label: "Blogs" },
|
||||||
{ href: "/career", label: "Career" },
|
{ href: "/career", label: "Career" },
|
||||||
{ href: "/contact", label: "Contact Us" },
|
{ href: "/contact", label: "Contact Us" },
|
||||||
];
|
];
|
||||||
@ -32,7 +32,7 @@ const Header = () => {
|
|||||||
<Link href="/" onClick={closeAllMenus} className="flex items-center">
|
<Link href="/" onClick={closeAllMenus} className="flex items-center">
|
||||||
<div className="relative w-60 sm:w-80 h-14 sm:h-18 mr-3 rounded overflow-hidden">
|
<div className="relative w-60 sm:w-80 h-14 sm:h-18 mr-3 rounded overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src="/images/cmclogo1.svg"
|
src="/images/cmclogo2.svg"
|
||||||
alt="CMC Logo"
|
alt="CMC Logo"
|
||||||
fill
|
fill
|
||||||
className="object-fill"
|
className="object-fill"
|
||||||
@ -43,12 +43,12 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className="hidden lg:flex items-start space-x-8">
|
<nav className="hidden lg:flex items-center space-x-3 xl:space-x-5">
|
||||||
{menuItems.map((item, idx) => (
|
{menuItems.map((item, idx) => (
|
||||||
<Link
|
<Link
|
||||||
key={idx}
|
key={idx}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
className="text-blue-900 hover:text-red-600 transition-colors font-medium text-sm xl:text-base whitespace-nowrap"
|
||||||
onClick={closeAllMenus}
|
onClick={closeAllMenus}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
@ -69,8 +69,8 @@ const Header = () => {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs sm:text-sm font-semibold text-blue-900 mt-1">
|
<span className="text-xs sm:text-sm font-semibold text-blue-900 mt-1 whitespace-nowrap">
|
||||||
H - 2 0 2 4 - 1 4 3 1
|
H-2024-1431
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { Building2, Home } from 'lucide-react';
|
|||||||
|
|
||||||
const PatientCareCards = () => {
|
const PatientCareCards = () => {
|
||||||
return (
|
return (
|
||||||
<section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}>
|
<section className="py-8 sm:py-12" style={{ backgroundColor: '#ffffff' }}>
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
||||||
{/* Inpatient Services */}
|
{/* Inpatient Services */}
|
||||||
<div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' }}>
|
<div className="rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' , backgroundColor: '#f4f4f4' }}>
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<div
|
<div
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center mr-3"
|
className="w-10 h-10 rounded-full flex items-center justify-center mr-3"
|
||||||
@ -26,7 +26,7 @@ const PatientCareCards = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Outpatient Services */}
|
{/* Outpatient Services */}
|
||||||
<div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' }}>
|
<div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068', backgroundColor: '#f4f4f4' }}>
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<div
|
<div
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center mr-3"
|
className="w-10 h-10 rounded-full flex items-center justify-center mr-3"
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const Services = () => {
|
|||||||
const services = [
|
const services = [
|
||||||
{
|
{
|
||||||
icon: <Users className="w-6 h-6" />,
|
icon: <Users className="w-6 h-6" />,
|
||||||
title: "Injury Prevention – Outreach Activity",
|
title: "Injury Prevention - Outreach Activity",
|
||||||
description: "Community-based programs including first responder training for laypersons, schools and workplace groups. Education in helmet use, bleeding control, safe transport practices and initial life-saving care."
|
description: "Community-based programs including first responder training for laypersons, schools and workplace groups. Education in helmet use, bleeding control, safe transport practices and initial life-saving care."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -26,7 +26,7 @@ const Services = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}>
|
<section className="py-8 sm:py-12" style={{ backgroundColor: '#ffffff' }}>
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<h2 className="text-2xl sm:text-3xl font-semibold text-center mb-2" style={{ color: '#012068' }}>
|
<h2 className="text-2xl sm:text-3xl font-semibold text-center mb-2" style={{ color: '#012068' }}>
|
||||||
Our Services
|
Our Services
|
||||||
@ -38,7 +38,7 @@ const Services = () => {
|
|||||||
{services.map((service, index) => (
|
{services.map((service, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-white rounded-lg p-6 border border-gray-300 hover:shadow-lg transition-shadow duration-300"
|
className="rounded-lg p-6 border border-gray-300 hover:shadow-lg transition-shadow duration-300" style={{ backgroundColor: '#f4f4f4' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const StatisticsTiles = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-8 sm:py-12 bg-white">
|
<section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}>
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<h2
|
<h2
|
||||||
className="text-2xl sm:text-3xl font-semibold text-center mb-8 sm:mb-12"
|
className="text-2xl sm:text-3xl font-semibold text-center mb-8 sm:mb-12"
|
||||||
@ -39,7 +39,7 @@ const StatisticsTiles = () => {
|
|||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="border border-gray-300 rounded-lg p-5 h-full hover:shadow-lg transition-shadow duration-300"
|
className="border border-gray-300 rounded-lg p-5 h-full hover:shadow-lg transition-shadow duration-300"
|
||||||
style={{ backgroundColor: '#f4f4f4' }}
|
style={{ backgroundColor: '#ffffff' }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-center mb-3">
|
<div className="flex items-center mb-3">
|
||||||
|
|||||||
@ -1,11 +1,74 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { getActiveMilestones, type Milestone } from "../../services/milestoneService";
|
||||||
|
|
||||||
|
// 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() {
|
export default function Process() {
|
||||||
const [activeStep, setActiveStep] = useState(-1);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [scrollProgress, setScrollProgress] = useState(0);
|
const [milestones, setMilestones] = useState<Milestone[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const timelineRef = 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(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@ -14,23 +77,45 @@ export default function Process() {
|
|||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const timeline = timelineRef.current;
|
const timeline = timelineRef.current;
|
||||||
const rect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
const timelineHeight = timeline.scrollHeight - timeline.clientHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
const containerHeight = container.offsetHeight;
|
||||||
|
|
||||||
// Calculate when section enters viewport
|
// Calculate when to start the scroll effect (when "Milestones" text reaches near top)
|
||||||
if (rect.top <= 0 && rect.bottom >= window.innerHeight) {
|
const scrollStartOffset = 100; // Adjust this to control when scrolling starts
|
||||||
// Section is sticky, scroll the timeline
|
|
||||||
const progress = Math.abs(rect.top) / (rect.height - window.innerHeight);
|
if (rect.top <= scrollStartOffset && rect.bottom >= windowHeight) {
|
||||||
const clampedProgress = Math.max(0, Math.min(1, progress));
|
// Section is in the sticky zone
|
||||||
setScrollProgress(clampedProgress);
|
const scrollableHeight = containerHeight - windowHeight;
|
||||||
timeline.scrollTop = clampedProgress * timelineHeight;
|
const scrolled = Math.abs(rect.top - scrollStartOffset);
|
||||||
} else if (rect.top > 0) {
|
const progress = Math.max(0, Math.min(1, scrolled / scrollableHeight));
|
||||||
// Before sticky
|
|
||||||
setScrollProgress(0);
|
// 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;
|
timeline.scrollTop = 0;
|
||||||
|
setActiveStep(0);
|
||||||
} else {
|
} else {
|
||||||
// After sticky
|
// After sticky zone
|
||||||
setScrollProgress(1);
|
const timelineHeight = timeline.scrollHeight - timeline.clientHeight;
|
||||||
timeline.scrollTop = timelineHeight;
|
timeline.scrollTop = timelineHeight;
|
||||||
|
setActiveStep(steps.length - 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,167 +123,47 @@ export default function Process() {
|
|||||||
handleScroll(); // Initial check
|
handleScroll(); // Initial check
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
}, []);
|
}, [steps.length]);
|
||||||
|
|
||||||
const CalendarIcon = (
|
// Loading state
|
||||||
<svg
|
if (loading) {
|
||||||
width={34}
|
return (
|
||||||
height={34}
|
<div className="bg-white py-8 sm:py-12 min-h-screen flex items-center justify-center">
|
||||||
viewBox="0 0 24 24"
|
<div className="text-center">
|
||||||
fill="none"
|
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 mb-4" style={{ borderColor: "#012068" }}></div>
|
||||||
stroke="currentColor"
|
<p className="text-lg" style={{ color: "#012068" }}>Loading milestones...</p>
|
||||||
strokeWidth="2"
|
</div>
|
||||||
strokeLinecap="round"
|
</div>
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
const steps = [
|
// Empty state
|
||||||
{
|
if (steps.length === 0) {
|
||||||
number: "1",
|
return (
|
||||||
title: "2017",
|
<div className="bg-white py-8 sm:py-12">
|
||||||
description: "Formation of Trauma Services",
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
icon: CalendarIcon,
|
<div className="text-xl mb-4" style={{ color: "#e64838" }}>
|
||||||
},
|
Milestones
|
||||||
{
|
</div>
|
||||||
number: "2",
|
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold leading-tight mb-6" style={{ color: "#012068" }}>
|
||||||
title: "2020",
|
Our Journey in Trauma Care
|
||||||
description: "Establishment of the Department of Trauma Surgery under Dr. Sukria Nayak",
|
</h2>
|
||||||
icon: CalendarIcon,
|
<p className="text-base sm:text-lg leading-relaxed" style={{ color: '#333' }}>
|
||||||
},
|
No milestones available at the moment.
|
||||||
{
|
</p>
|
||||||
number: "3",
|
</div>
|
||||||
title: "May 2021",
|
</div>
|
||||||
description: "Development of Trauma Protocols (MHTP, TROB, TRIP, Airway, Obstetric Trauma)",
|
);
|
||||||
icon: CalendarIcon,
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "4",
|
|
||||||
title: "June 2022",
|
|
||||||
description: "Inauguration of CMC Ranipet Campus: Level 1 Trauma Centre",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "5",
|
|
||||||
title: "November 2022",
|
|
||||||
description: "Formation of T-ReCS (Trauma Registry – CMC – Pilot Study) under TCI-CMC Research Scholar Program",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "6",
|
|
||||||
title: "November 2022",
|
|
||||||
description: "Integration of Multidisciplinary Trauma Services",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "7",
|
|
||||||
title: "November 2022",
|
|
||||||
description: "Formation of Neurotrauma Unit",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "8",
|
|
||||||
title: "January 2023",
|
|
||||||
description: "Accreditation of Trauma Radiology Fellowship",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "9",
|
|
||||||
title: "August 2023",
|
|
||||||
description: "Inaugural ATLS® Course at CMC Vellore",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "10",
|
|
||||||
title: "September 2023",
|
|
||||||
description: "First Research Publication from the Department",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "11",
|
|
||||||
title: "October 2023",
|
|
||||||
description: "ACTraM Conference: Advances in Chest Trauma Management",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "12",
|
|
||||||
title: "October 2023",
|
|
||||||
description: "CME Cadaveric Workshop",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "13",
|
|
||||||
title: "December 2023",
|
|
||||||
description: "ICMR Trauma Quality Improvement Programme (TQIP) Initiated at CMC",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "14",
|
|
||||||
title: "February 2024",
|
|
||||||
description: "HOPE Grant & RCPSG Trauma First Responder Outreach Programme",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "15",
|
|
||||||
title: "April 2024",
|
|
||||||
description: "Launch of Masters of Trauma Online Lecture Series",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "16",
|
|
||||||
title: "April 2024",
|
|
||||||
description: "Inaugural ATCN® Course for Nurses",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "17",
|
|
||||||
title: "July 2024",
|
|
||||||
description: "MOU with GVK EMRI for Strengthening Pre-hospital Trauma Care",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "18",
|
|
||||||
title: "July 2024",
|
|
||||||
description: "Accreditation for FNB in Trauma and Acute Care Surgery",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "19",
|
|
||||||
title: "January 2025",
|
|
||||||
description: "Trauma Quality Workshop under ICMR-TQIP at CMC Vellore",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "20",
|
|
||||||
title: "March 2025",
|
|
||||||
description: "Formation of CMC Trauma Quality Improvement Committee",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: "21",
|
|
||||||
title: "July 2025",
|
|
||||||
description: "Establishment of Trauma Orthopaedics Unit",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Calculate extra height needed for scroll effect
|
// For small number of items (1-3), use static layout without sticky scroll
|
||||||
const extraHeight = 2000;
|
const isSmallList = steps.length <= 3;
|
||||||
|
|
||||||
return (
|
if (isSmallList) {
|
||||||
<div
|
return (
|
||||||
ref={containerRef}
|
<div className="bg-white py-8 sm:py-12">
|
||||||
className="bg-white py-10 sm:py-10"
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
style={{ minHeight: `calc(100vh + ${extraHeight}px)` }}
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
|
||||||
>
|
|
||||||
<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">
|
|
||||||
{/* Left Column */}
|
{/* Left Column */}
|
||||||
<div className="flex flex-col justify-start">
|
<div className="flex flex-col justify-start">
|
||||||
<div>
|
<div>
|
||||||
@ -211,123 +176,204 @@ export default function Process() {
|
|||||||
>
|
>
|
||||||
Our Journey in Trauma Care
|
Our Journey in Trauma Care
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base sm:text-lg leading-relaxed mb-8" style={{ color: '#333' }}>
|
<p className="text-base sm:text-lg leading-relaxed" style={{ color: '#333' }}>
|
||||||
|
From the formation of Trauma Services in 2017 to establishing a Level-1 Trauma Facility and comprehensive trauma care programs, we continue to expand our emergency care services with dedication and excellence.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - Static Timeline */}
|
||||||
|
<div className="relative">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`relative group transition-all duration-500 ${
|
||||||
|
index !== 0 ? "mt-10" : ""
|
||||||
|
}`}
|
||||||
|
onMouseEnter={() => setActiveStep(index)}
|
||||||
|
onMouseLeave={() => setActiveStep(-1)}
|
||||||
|
>
|
||||||
|
{/* Connecting Line */}
|
||||||
|
{index !== 0 && (
|
||||||
|
<div className="absolute left-6 -top-10 w-0.5 h-10 bg-gray-300 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="w-full h-full transition-all duration-700"
|
||||||
|
style={{ backgroundColor: "#012068" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step Content */}
|
||||||
|
<div className="flex items-start gap-4 sm:gap-6 pl-2">
|
||||||
|
{/* Number Circle */}
|
||||||
|
<div
|
||||||
|
className={`relative z-10 flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center text-white font-semibold text-lg transition-all duration-500 transform ${
|
||||||
|
activeStep === index ? "scale-110 shadow-lg ring-4" : "scale-100 hover:scale-105"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: activeStep === index ? "#012068" : "#333",
|
||||||
|
"--tw-ring-color": activeStep === index ? "#01206833" : "transparent",
|
||||||
|
} as React.CSSProperties}
|
||||||
|
>
|
||||||
|
{step.number}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Card */}
|
||||||
|
<div
|
||||||
|
className={`flex-1 p-4 sm:p-6 rounded-lg border transition-all duration-500 transform min-h-[180px] ${
|
||||||
|
activeStep === index
|
||||||
|
? "border-transparent text-white shadow-2xl scale-105 -translate-y-2"
|
||||||
|
: "border-gray-300 hover:shadow-lg hover:-translate-y-1"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: activeStep === index ? "#012068" : "#f4f4f4",
|
||||||
|
color: activeStep === index ? "white" : "#333",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className="mb-4 transition-all duration-500"
|
||||||
|
style={{
|
||||||
|
color: activeStep === index ? "white" : "black",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step.icon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text */}
|
||||||
|
<h3 className="text-lg sm:text-xl font-semibold mb-3 transition-all duration-300 break-words">
|
||||||
|
{step.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm sm:text-base leading-relaxed transition-all duration-500 break-words whitespace-normal">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For larger lists (4+), use sticky scroll behavior
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="bg-white relative mb-0"
|
||||||
|
style={{
|
||||||
|
minHeight: `calc(100vh + ${extraHeight}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="sticky top-0 h-screen flex items-center overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
|
||||||
|
{/* Left Column - Fixed Header */}
|
||||||
|
<div className="flex flex-col justify-start lg:sticky lg:top-1/4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xl mb-4" style={{ color: "#e64838" }}>
|
||||||
|
Milestones
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
className="text-3xl sm:text-4xl md:text-5xl font-bold leading-tight mb-6"
|
||||||
|
style={{ color: "#012068" }}
|
||||||
|
>
|
||||||
|
Our Journey in Trauma Care
|
||||||
|
</h2>
|
||||||
|
<p className="text-base sm:text-lg leading-relaxed" style={{ color: '#333' }}>
|
||||||
From the formation of Trauma Services in 2017 to establishing a Level-1 Trauma Facility and comprehensive trauma care programs, we continue to expand our emergency care services with dedication and excellence.
|
From the formation of Trauma Services in 2017 to establishing a Level-1 Trauma Facility and comprehensive trauma care programs, we continue to expand our emergency care services with dedication and excellence.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Auto-scrolling Timeline */}
|
{/* Right Column - Auto-scrolling Timeline */}
|
||||||
<div className="relative h-[600px]">
|
<div className="relative h-[70vh] max-h-[600px]">
|
||||||
<div
|
<div
|
||||||
ref={timelineRef}
|
ref={timelineRef}
|
||||||
className="h-full overflow-y-auto pr-2 pl-2 py-2 [&::-webkit-scrollbar]:hidden pointer-events-none"
|
className="h-full overflow-y-auto pr-4 pl-4 [&::-webkit-scrollbar]:hidden pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
msOverflowStyle: 'none'
|
msOverflowStyle: 'none'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{steps.map((step, index) => (
|
<div className="py-4">
|
||||||
<div
|
{steps.map((step, index) => (
|
||||||
key={index}
|
<div
|
||||||
className={`relative group transition-all duration-500 ${
|
key={index}
|
||||||
index !== 0 ? "mt-8 sm:mt-12" : ""
|
ref={(el) => {
|
||||||
} ${index === 0 ? "pt-2" : ""}`}
|
if (el) {
|
||||||
onMouseEnter={() => setActiveStep(index)}
|
stepRefs.current[index] = el;
|
||||||
onMouseLeave={() => setActiveStep(-1)}
|
}
|
||||||
>
|
}}
|
||||||
{/* Connecting Line */}
|
className={`relative group transition-all duration-500 ${
|
||||||
{index !== 0 && (
|
index !== 0 ? "mt-10" : ""
|
||||||
<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 ${
|
{/* Connecting Line */}
|
||||||
activeStep >= index ||
|
{index !== 0 && (
|
||||||
(activeStep === -1 && index === 0)
|
<div className="absolute left-6 -top-10 w-0.5 h-10 bg-gray-300 overflow-hidden">
|
||||||
? "h-full scale-y-100"
|
<div
|
||||||
: "h-0 scale-y-0"
|
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" }}
|
}`}
|
||||||
/>
|
style={{ backgroundColor: "#012068" }}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Step Content */}
|
{/* Step Content */}
|
||||||
<div className="flex items-start gap-4 sm:gap-6">
|
<div className="flex items-start gap-4 sm:gap-6">
|
||||||
{/* Number Circle */}
|
{/* 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 */}
|
|
||||||
<div
|
<div
|
||||||
className={`mb-4 transition-all duration-500 transform ${
|
className={`relative z-10 flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center text-white font-semibold text-lg transition-all duration-500 transform ${
|
||||||
activeStep === index ||
|
activeStep === index
|
||||||
(activeStep === -1 && index === 0)
|
? "scale-110 shadow-lg ring-4"
|
||||||
? "text-white"
|
: "scale-100"
|
||||||
: "scale-100 group-hover:scale-105"
|
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
color:
|
backgroundColor: activeStep === index ? "#012068" : "#333",
|
||||||
activeStep === index ||
|
"--tw-ring-color": activeStep === index ? "#01206833" : "transparent",
|
||||||
(activeStep === -1 && index === 0)
|
} as React.CSSProperties}
|
||||||
? "white"
|
|
||||||
: "black",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{step.icon}
|
{step.number}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text */}
|
{/* Step Card */}
|
||||||
<h3 className="text-lg sm:text-xl font-semibold mb-3 transition-all duration-300">
|
<div
|
||||||
{step.title}
|
className={`flex-1 p-4 sm:p-6 rounded-lg border transition-all duration-500 transform min-h-[180px] ${
|
||||||
</h3>
|
activeStep === index
|
||||||
<p className="text-sm sm:text-base leading-relaxed transition-all duration-500">
|
? "border-transparent text-white shadow-2xl scale-105 -translate-y-2"
|
||||||
{step.description}
|
: "border-gray-300"
|
||||||
</p>
|
}`}
|
||||||
|
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>
|
</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 [eventData, setEventData] = useState<Event | null>(null);
|
||||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [bookingStatus, setBookingStatus] = useState<'idle' | 'booking' | 'success' | 'error'>('idle');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchEventData = async () => {
|
const fetchEventData = async () => {
|
||||||
@ -25,7 +24,6 @@ const EventDetail = () => {
|
|||||||
console.log('Fetching event with ID:', eventId);
|
console.log('Fetching event with ID:', eventId);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// Convert string ID to number for API call
|
|
||||||
const numericId = parseInt(eventId, 10);
|
const numericId = parseInt(eventId, 10);
|
||||||
if (isNaN(numericId)) {
|
if (isNaN(numericId)) {
|
||||||
console.error('Invalid event ID:', eventId);
|
console.error('Invalid event ID:', eventId);
|
||||||
@ -77,17 +75,28 @@ const EventDetail = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBookSeat = async () => {
|
// NEW FUNCTION: Handle book seat click - redirects to admin-provided link
|
||||||
|
const handleBookSeat = () => {
|
||||||
if (!eventData) return;
|
if (!eventData) return;
|
||||||
setBookingStatus('booking');
|
|
||||||
try {
|
// Check if admin has provided a custom booking link
|
||||||
// Replace with actual booking API call
|
if (eventData.bookSeatLink && eventData.bookSeatLink.trim() !== '') {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
// Validate if it's a proper URL
|
||||||
setBookingStatus('success');
|
try {
|
||||||
setTimeout(() => setBookingStatus('idle'), 2000);
|
// If the URL doesn't start with http:// or https://, add https://
|
||||||
} catch (error) {
|
let url = eventData.bookSeatLink;
|
||||||
setBookingStatus('error');
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
setTimeout(() => setBookingStatus('idle'), 2000);
|
url = 'https://' + url;
|
||||||
|
}
|
||||||
|
// Open in new tab
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
} catch (error) {
|
||||||
|
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();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBookingButtonText = () => {
|
|
||||||
switch (bookingStatus) {
|
|
||||||
case 'booking': return 'Booking...';
|
|
||||||
case 'success': return 'Booked!';
|
|
||||||
case 'error': return 'Try Again';
|
|
||||||
default: return 'Book Your Seat';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBookingButtonStyle = () => {
|
|
||||||
switch (bookingStatus) {
|
|
||||||
case 'booking': return 'bg-gray-600 cursor-not-allowed';
|
|
||||||
case 'success': return 'bg-green-600 hover:bg-green-700';
|
|
||||||
case 'error': return 'bg-red-600 hover:bg-red-700';
|
|
||||||
default: return 'hover:opacity-90';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions to format API data for display
|
// Helper functions to format API data for display
|
||||||
const formatPrice = (event: Event) => {
|
const formatPrice = (event: Event) => {
|
||||||
if (event.fee && event.fee.length > 0) {
|
if (event.fee && event.fee.length > 0) {
|
||||||
@ -171,7 +162,6 @@ const EventDetail = () => {
|
|||||||
return fallbackImages;
|
return fallbackImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have at least 2 images for the layout
|
|
||||||
while (validImages.length < 2) {
|
while (validImages.length < 2) {
|
||||||
validImages.push(fallbackImages[validImages.length % fallbackImages.length]);
|
validImages.push(fallbackImages[validImages.length % fallbackImages.length]);
|
||||||
}
|
}
|
||||||
@ -179,7 +169,6 @@ const EventDetail = () => {
|
|||||||
return validImages.slice(0, 2);
|
return validImages.slice(0, 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format description data for display
|
|
||||||
const getFormattedDescription = (event: Event) => {
|
const getFormattedDescription = (event: Event) => {
|
||||||
return {
|
return {
|
||||||
overview: event.description || 'Join leading medical professionals for this comprehensive event focusing on the latest developments in healthcare.',
|
overview: event.description || 'Join leading medical professionals for this comprehensive event focusing on the latest developments in healthcare.',
|
||||||
@ -207,29 +196,18 @@ const EventDetail = () => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
|
||||||
{/* Breadcrumb Section */}
|
|
||||||
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<nav className="flex items-center space-x-2 text-sm">
|
<nav className="flex items-center space-x-2 text-sm">
|
||||||
<Link
|
<Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
|
||||||
href="/"
|
|
||||||
className="hover:opacity-70 transition-opacity duration-200"
|
|
||||||
style={{ color: '#012068' }}
|
|
||||||
>
|
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||||
<a
|
<a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
|
||||||
href="/events"
|
|
||||||
className="hover:opacity-70 transition-opacity duration-200"
|
|
||||||
style={{ color: '#012068' }}
|
|
||||||
>
|
|
||||||
Medical Events
|
Medical Events
|
||||||
</a>
|
</a>
|
||||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
<span className="font-medium" style={{ color: '#e64838' }}>Loading...</span>
|
||||||
Loading...
|
|
||||||
</span>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -259,29 +237,18 @@ const EventDetail = () => {
|
|||||||
if (!eventData) {
|
if (!eventData) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
|
||||||
{/* Breadcrumb Section */}
|
|
||||||
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<nav className="flex items-center space-x-2 text-sm">
|
<nav className="flex items-center space-x-2 text-sm">
|
||||||
<Link
|
<Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
|
||||||
href="/"
|
|
||||||
className="hover:opacity-70 transition-opacity duration-200"
|
|
||||||
style={{ color: '#012068' }}
|
|
||||||
>
|
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||||
<a
|
<a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
|
||||||
href="/events"
|
|
||||||
className="hover:opacity-70 transition-opacity duration-200"
|
|
||||||
style={{ color: '#012068' }}
|
|
||||||
>
|
|
||||||
Medical Events
|
Medical Events
|
||||||
</a>
|
</a>
|
||||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
<span className="font-medium" style={{ color: '#e64838' }}>Event Not Found</span>
|
||||||
Event Not Found
|
|
||||||
</span>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -317,19 +284,11 @@ const EventDetail = () => {
|
|||||||
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<nav className="flex items-center space-x-2 text-sm">
|
<nav className="flex items-center space-x-2 text-sm">
|
||||||
<Link
|
<Link href="/" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
|
||||||
href="/"
|
|
||||||
className="hover:opacity-70 transition-opacity duration-200"
|
|
||||||
style={{ color: '#012068' }}
|
|
||||||
>
|
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||||
<a
|
<a href="/events" className="hover:opacity-70 transition-opacity duration-200" style={{ color: '#012068' }}>
|
||||||
href="/events"
|
|
||||||
className="hover:opacity-70 transition-opacity duration-200"
|
|
||||||
style={{ color: '#012068' }}
|
|
||||||
>
|
|
||||||
Medical Events
|
Medical Events
|
||||||
</a>
|
</a>
|
||||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||||
@ -400,14 +359,10 @@ const EventDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleBookSeat}
|
onClick={handleBookSeat}
|
||||||
disabled={bookingStatus === 'booking'}
|
className="w-full sm:w-auto px-6 py-2 text-sm rounded-lg transition-all duration-200 hover:opacity-90"
|
||||||
className={`w-full sm:w-auto px-6 py-2 text-sm rounded-lg transition-all duration-200 ${getBookingButtonStyle()}`}
|
style={{ backgroundColor: '#012068', color: '#f4f4f4' }}
|
||||||
style={{
|
|
||||||
backgroundColor: bookingStatus === 'idle' ? '#012068' : undefined,
|
|
||||||
color: '#f4f4f4'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{getBookingButtonText()}
|
Book Your Seat
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -76,77 +76,106 @@ const TeamListing: React.FC<TeamListingProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TeamMemberCard: React.FC<{ member: TeamMember }> = ({ member }) => {
|
const TeamMemberCard: React.FC<{ member: TeamMember }> = ({ member }) => {
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
const [imageLoading, setImageLoading] = useState(true);
|
const [imageLoading, setImageLoading] = useState(true);
|
||||||
|
const isTrainee = member.category === 'TRAINEE_FELLOW';
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
setImageError(true);
|
||||||
target.src = '/images/default-avatar.jpg';
|
setImageLoading(false);
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageLoad = () => {
|
const handleImageLoad = () => {
|
||||||
console.log('Image loaded successfully:', member.image);
|
console.log('Image loaded successfully:', member.image);
|
||||||
setImageLoading(false);
|
setImageLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group cursor-pointer bg-white rounded-lg border border-gray-300 overflow-hidden hover:shadow-lg transition-all duration-300"
|
className={`bg-white rounded-lg border border-gray-300 overflow-hidden transition-all duration-300 ${
|
||||||
onClick={() => handleMemberClick(member)}
|
!isTrainee ? 'group cursor-pointer hover:shadow-lg' : 'cursor-default'
|
||||||
>
|
}`}
|
||||||
<div className="relative aspect-square overflow-hidden">
|
onClick={() => !isTrainee && handleMemberClick(member)}
|
||||||
{imageLoading && (
|
>
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
|
{/* Only show image section for non-trainees */}
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
{!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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<img
|
<div className="p-4 sm:p-6">
|
||||||
src={member.image}
|
<h3 className="text-lg font-medium mb-2 group-hover:opacity-70 transition-opacity" style={{ color: '#012068' }}>
|
||||||
alt={member.name}
|
{member.name}
|
||||||
className={`w-full h-full object-cover transition-transform duration-300 group-hover:scale-105 ${
|
</h3>
|
||||||
imageLoading ? 'opacity-0' : 'opacity-100'
|
<p className="text-sm leading-relaxed" style={{ color: '#e64838' }}>
|
||||||
}`}
|
{member.position}
|
||||||
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}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
{!isTrainee && (
|
||||||
{member.specialty && (
|
<>
|
||||||
<p className="text-xs mt-1 font-medium" style={{ color: '#333' }}>
|
{member.department && (
|
||||||
{member.specialty}
|
<p className="text-xs mt-1" style={{ color: '#666' }}>
|
||||||
</p>
|
{member.department}
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
|
{member.specialty && (
|
||||||
|
<p className="text-xs mt-1 font-medium" style={{ color: '#333' }}>
|
||||||
|
{member.specialty}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<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' }}>
|
<h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}>
|
||||||
Trainees & Fellows
|
Trainees & Fellows
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm" style={{ color: '#666' }}>
|
<p className="text-sm mb-2" style={{ color: '#666' }}>
|
||||||
Medical trainees, residents, and fellows advancing their skills and contributing to patient care
|
Medical trainees, residents, and fellows advancing their skills and contributing to patient care
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
|
|||||||
const [loading, setLoading] = useState(!memberData);
|
const [loading, setLoading] = useState(!memberData);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showSocialShare, setShowSocialShare] = useState(false);
|
const [showSocialShare, setShowSocialShare] = useState(false);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadMemberData = async () => {
|
const loadMemberData = async () => {
|
||||||
@ -72,9 +73,24 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTrainee = member.category === 'TRAINEE_FELLOW';
|
||||||
|
|
||||||
|
// Check if image URL is valid and not a default placeholder
|
||||||
|
const hasValidImage = member.image &&
|
||||||
|
member.image.trim() !== '' &&
|
||||||
|
!member.image.includes('default-avatar') &&
|
||||||
|
!member.image.includes('placeholder.') &&
|
||||||
|
!member.image.includes('robot') &&
|
||||||
|
// Only filter if URL ENDS with /profile-image (backend default endpoint)
|
||||||
|
!(member.image.endsWith('/profile-image') ||
|
||||||
|
member.image.includes('/profile-image?') ||
|
||||||
|
member.image === 'https://via.placeholder.com/400');
|
||||||
|
|
||||||
|
const shouldShowImage = !isTrainee && hasValidImage && !imageError;
|
||||||
|
|
||||||
// Create a comprehensive description from the member's details
|
// Create a comprehensive description from the member's details
|
||||||
const getFullDescription = () => {
|
const getFullDescription = () => {
|
||||||
return member.description || `${member.name} is a dedicated member of our faculty with expertise in ${member.specialty?.toLowerCase() || 'medical practice'}. They bring valuable experience in surgical education, patient care, and clinical research to Christian Medical College, Vellore.`;
|
return member.description || `${member.name} is a dedicated member of our ${isTrainee ? 'trainee program' : 'faculty'} with expertise in ${member.specialty?.toLowerCase() || 'medical practice'}. They bring valuable experience in ${isTrainee ? 'clinical training' : 'surgical education'}, patient care, and clinical research to Christian Medical College, Vellore.`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse phone numbers if multiple are provided
|
// Parse phone numbers if multiple are provided
|
||||||
@ -87,6 +103,11 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
|
|||||||
: member.phone;
|
: member.phone;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
console.error('Image failed to load:', member.image);
|
||||||
|
setImageError(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{/* Breadcrumb Section */}
|
{/* Breadcrumb Section */}
|
||||||
@ -120,6 +141,11 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
|
|||||||
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
|
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
|
||||||
{member.name}
|
{member.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
{isTrainee && (
|
||||||
|
<span className="ml-4 px-3 py-1 text-xs font-medium rounded-full bg-gray-200 text-gray-700">
|
||||||
|
Trainee/Fellow
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
|
<p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
|
||||||
{member.designation || member.position} {member.experience && `with ${member.experience} of experience`} at CMC Vellore
|
{member.designation || member.position} {member.experience && `with ${member.experience} of experience`} at CMC Vellore
|
||||||
@ -134,17 +160,29 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
|
|||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="xl:col-span-1">
|
<div className="xl:col-span-1">
|
||||||
<div className="bg-white rounded-md border border-gray-300 overflow-hidden sticky top-8">
|
<div className="bg-white rounded-md border border-gray-300 overflow-hidden sticky top-8">
|
||||||
{/* Profile Image */}
|
{/* Profile Image or Avatar */}
|
||||||
<div className="aspect-square relative overflow-hidden">
|
<div className="aspect-square relative overflow-hidden bg-gray-50">
|
||||||
<img
|
{shouldShowImage ? (
|
||||||
src={member.image}
|
<img
|
||||||
alt={member.name}
|
src={member.image}
|
||||||
className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-300"
|
alt={member.name}
|
||||||
onError={(e) => {
|
className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-300"
|
||||||
const target = e.target as HTMLImageElement;
|
onError={handleImageError}
|
||||||
target.src = '/images/default-avatar.jpg';
|
/>
|
||||||
}}
|
) : (
|
||||||
/>
|
<div
|
||||||
|
className="w-full h-full flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: '#e0e5eb' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-48 h-48 text-gray-400"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Info */}
|
{/* Profile Info */}
|
||||||
@ -160,7 +198,7 @@ const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberDat
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm mb-6 leading-relaxed" style={{ color: '#333' }}>
|
<p className="text-sm mb-6 leading-relaxed" style={{ color: '#333' }}>
|
||||||
{member.description || `${member.name} is a dedicated faculty member at CMC Vellore.`}
|
{member.description || `${member.name} is a dedicated ${isTrainee ? 'trainee' : 'faculty member'} at CMC Vellore.`}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
|
|||||||
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;
|
phone: string;
|
||||||
email: string;
|
email: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
bookSeatLink?: string; // NEW FIELD
|
||||||
professors?: Professor[];
|
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