first commit
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
37
DockerFile
Normal file
@ -0,0 +1,37 @@
|
||||
# Use Node.js LTS Alpine for smaller image
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install --frozen-lockfile || npm install
|
||||
|
||||
# Build the app
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy only necessary files
|
||||
FROM base AS runner
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy build output and node_modules
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
nextjs:
|
||||
build: .
|
||||
container_name: cmc_nextjs_pro
|
||||
ports:
|
||||
- "9012:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
25
eslint.config.mjs
Normal file
@ -0,0 +1,25 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
29
next.config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '8080',
|
||||
pathname: '/api/files/images/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'yourproductiondomain.com', // Replace with your production domain
|
||||
pathname: '/api/files/images/**',
|
||||
}
|
||||
],
|
||||
domains: ['localhost'],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6149
package-lock.json
generated
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "cmc_nextjs_pro",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.3",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
postcss.config.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
public/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/Meta.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/award-icon.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/default-avatar.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
public/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/footer.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/images/hero.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
public/images/logo-2.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/images/logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
33
src/app/about/page.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../../components/Layouts/Footer"
|
||||
import Process from "../../components/about/process";
|
||||
import Breadcrumb from "../../components/about/AboutBreadcrumb";
|
||||
import Introduction from "../../components/about/Introduction";
|
||||
import MissionVision from "../../components/about/MissionVision";
|
||||
import Services from "../../components/about/Services";
|
||||
import StatisticsTiles from "../../components/about/StatisticsTiles";
|
||||
import PatientCareCards from "../../components/about/PatientCareCards";
|
||||
|
||||
export default function Home() {
|
||||
const breadcrumbItems = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "About Us", isActive: true }
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Breadcrumb
|
||||
items={breadcrumbItems}
|
||||
title="About Us"
|
||||
description="Dedicated to excellence in trauma care at CMC Hospital, Ranipet campus—saving lives with compassion and expertise."
|
||||
/>
|
||||
<Introduction />
|
||||
<MissionVision />
|
||||
<Process />
|
||||
<Services />
|
||||
<StatisticsTiles />
|
||||
<PatientCareCards />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
src/app/blog-detail/[id]/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import Header from "../../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../../../components/Layouts/Footer"
|
||||
import BlogDetail from "../../../components/blogs/BlogDetail";
|
||||
|
||||
|
||||
export default function contact() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<BlogDetail/>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
src/app/blogs/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../../components/Layouts/Footer"
|
||||
import BlogListing from '../../components/blogs/BlogListing';
|
||||
|
||||
|
||||
export default function contact() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<BlogListing/>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
src/app/career/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../../components/Layouts/Footer"
|
||||
import CareersComponent from "@/components/career/careerscomponent";
|
||||
|
||||
|
||||
export default function contact() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<CareersComponent/>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/app/contact/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 ContactPage from '../../components/contact-us/ContactForm';
|
||||
|
||||
export default function contact() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<ContactPage />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
src/app/education-training/course-detail/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import Header from "../../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../../../components/Layouts/Footer"
|
||||
import CourseDetail from "../../../components/education/CourseDetail";
|
||||
|
||||
|
||||
export default function contact() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<CourseDetail/>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
src/app/education-training/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../../components/Layouts/Footer"
|
||||
import EducationTraining from "../../components/education/EducationTraining";
|
||||
|
||||
|
||||
export default function contact() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<EducationTraining/>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/app/event-detail/[id]/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 EventDetail from "../../../components/events/EventDetail";
|
||||
export default function faculty() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<EventDetail/>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/app/events/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 MedicalEventsComponent from "../../components/events/MedicalEventsComponent";
|
||||
export default function faculty() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<MedicalEventsComponent/>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
88
src/app/faculty/[id]/page.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
// app/faculty/[id]/page.tsx
|
||||
import Header from "../../../components/Layouts/Header";
|
||||
import { Footer } from "../../../components/Layouts/Footer";
|
||||
import TeamMemberDetail from "../../../components/faculty/TeamMemberDetail";
|
||||
import { notFound } from 'next/navigation';
|
||||
import { FacultyService } from '../../../lib/facultyData';
|
||||
|
||||
interface FacultyPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function FacultyPage({ params }: FacultyPageProps) {
|
||||
const memberId = parseInt(params.id);
|
||||
|
||||
// Check if ID is valid number
|
||||
if (isNaN(memberId)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Fetch member data from API
|
||||
const memberData = await FacultyService.getFacultyById(memberId);
|
||||
|
||||
// If member not found, show 404
|
||||
if (!memberData) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<TeamMemberDetail memberId={memberId} memberData={memberData} />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Generate static params for all team members
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const teamMembers = await FacultyService.getAllFaculty();
|
||||
|
||||
return teamMembers.map((member) => ({
|
||||
id: member.id.toString(),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error generating static params:', error);
|
||||
// Return empty array if data fetching fails during build
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Generate metadata for each team member page
|
||||
export async function generateMetadata({ params }: FacultyPageProps) {
|
||||
const memberId = parseInt(params.id);
|
||||
|
||||
if (isNaN(memberId)) {
|
||||
return {
|
||||
title: 'Faculty Member Not Found - CMC Vellore'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const memberData = await FacultyService.getFacultyById(memberId);
|
||||
|
||||
if (!memberData) {
|
||||
return {
|
||||
title: 'Faculty Member Not Found - CMC Vellore'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${memberData.name} - ${memberData.designation} | CMC Vellore`,
|
||||
description: memberData.description,
|
||||
openGraph: {
|
||||
title: `${memberData.name} - ${memberData.designation}`,
|
||||
description: memberData.description,
|
||||
images: [memberData.image],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating metadata:', error);
|
||||
return {
|
||||
title: 'Faculty Member - CMC Vellore'
|
||||
};
|
||||
}
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
26
src/app/globals.css
Normal file
@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
34
src/app/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CMC - Department of Trauma Surgery",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
15
src/app/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import Header from "../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../components/Layouts/Footer";
|
||||
import HeroSection from "../components/home/HeroSection";
|
||||
import EventsSection from "../components/home/EventSection";
|
||||
|
||||
export default function faculty() {
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<Header />
|
||||
<HeroSection/>
|
||||
<EventsSection/>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
src/app/research/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import Header from "../../components/Layouts/Header"; // Adjust path based on your project structure
|
||||
import { Footer } from "../../components/Layouts/Footer"
|
||||
import ResearchComponent from "../../components/research/ResearchComponent";
|
||||
|
||||
|
||||
export default function contact() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<ResearchComponent/>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/app/teamMember/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 TeamListing from "../../components/faculty/TeamListing";
|
||||
export default function faculty() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<TeamListing/>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
136
src/components/Layouts/Footer.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="text-gray-100" style={{ backgroundColor: '#012068'}}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{/* Company Info */}
|
||||
<div className="lg:col-span-1">
|
||||
<Link href="/" className="inline-block mb-4">
|
||||
<div className="flex items-center">
|
||||
|
||||
<span className="text-2xl font-bold text-gray-100">Department of <br></br>Trauma Surgery</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="space-y-3 mb-6">
|
||||
<a
|
||||
href="mailto:traumasurg@cmcvellore.ac.in"
|
||||
className="block transition-colors duration-200 text-gray-100 hover:text-red-600"
|
||||
>
|
||||
traumasurg@cmcvellore.ac.in
|
||||
</a>
|
||||
<p className="text-sm leading-relaxed text-gray-300">
|
||||
Department of Trauma Surgery<br />
|
||||
Room A601, 6th Floor, A Block<br />
|
||||
CMC Vellore Ranipet Campus<br />
|
||||
Kilminnal Village, Ranipet – 632517,<br />
|
||||
Tamil Nadu
|
||||
</p>
|
||||
<p className="text-gray-100">0417-2224626</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Company Links */}
|
||||
<div className="md:col-span-1">
|
||||
<h3 className="font-semibold text-lg mb-4 text-gray-100">Company</h3>
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
{ label: 'About CMC', href: '/about' },
|
||||
{ label: 'Contact us', href: '/contact' },
|
||||
{ label: 'Events', href: '/events' },
|
||||
{ label: 'Education', href: '/education-training' },
|
||||
{ label: 'Career', href: '/career' },
|
||||
{ label: 'Team Member', href: '/teamMember' },
|
||||
].map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-gray-300 hover:text-red-600 transition-all duration-200 text-sm"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Additional Links */}
|
||||
<div className="md:col-span-1">
|
||||
<h3 className="font-semibold text-lg mb-4 text-gray-100">Links</h3>
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
{ label: 'Help Center', href: '/contact' },
|
||||
{ label: 'Privacy Policy', href: '#' },
|
||||
{ label: 'Terms & Conditions', href: '#' },
|
||||
{ label: 'Blogs', href: '/blogs' },
|
||||
].map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-gray-300 hover:text-red-600 transition-all duration-200 text-sm"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Newsletter */}
|
||||
<div className="md:col-span-2 lg:col-span-1">
|
||||
<h3 className="font-semibold text-lg mb-4 text-gray-100">Stay Updated</h3>
|
||||
<p className="text-sm mb-4 text-gray-300">
|
||||
Follow us on social media for the latest updates and news.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
className="flex-1 px-4 py-2 rounded-md bg-blue-800 bg-opacity-20 border border-blue-600 text-white placeholder-white transition-all"
|
||||
/>
|
||||
<button className="px-4 py-2 rounded-md bg-red-600 text-gray-100 hover:bg-red-700 transition-all duration-200 whitespace-nowrap">
|
||||
Subscribe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Bottom */}
|
||||
<div className="border-t border-gray-600 border-opacity-30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||
<p className="text-sm text-gray-300">
|
||||
© {new Date().getFullYear()} Copyright by{" "}
|
||||
<a
|
||||
href="#"
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
CMC
|
||||
</a>
|
||||
. All Rights Reserved.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center md:justify-end space-x-6">
|
||||
{[
|
||||
{ label: 'Privacy Policy', href: '#' },
|
||||
{ label: 'Terms of Service', href: '#' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
].map((link) => (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className="text-gray-300 hover:text-red-600 transition-all duration-200 text-sm"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
238
src/components/Layouts/Header.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
const Header = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||
|
||||
const toggleDropdown = (dropdownName: string) => {
|
||||
setOpenDropdown(current => current === dropdownName ? null : dropdownName);
|
||||
};
|
||||
|
||||
const closeAllMenus = () => {
|
||||
setIsMenuOpen(false);
|
||||
setOpenDropdown(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky bg-white top-0 z-50 shadow-sm">
|
||||
<div className="container max-w-7xl mx-auto px-4 sm:px-6">
|
||||
<div className="flex justify-between items-center py-3 sm:py-4">
|
||||
{/* Logo */}
|
||||
<div className="flex-shrink-0">
|
||||
<Link href="/" onClick={closeAllMenus} className="flex items-center">
|
||||
<div className="relative w-80 h-18 mr-3 rounded overflow-hidden">
|
||||
<Image
|
||||
src="/images/logo.png" // Replace with your logo path
|
||||
alt="CMC Logo"
|
||||
fill
|
||||
className="object-fill"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden lg:flex items-start space-x-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/about"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/events"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Events
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/education-training"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Education
|
||||
</Link>
|
||||
<Link
|
||||
href="/research"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Research
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/blogs"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Blogs
|
||||
</Link>
|
||||
<Link
|
||||
href="/teamMember"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Team Member
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/career"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Career
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-blue-900 hover:text-red-600 transition-colors font-medium"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
className="lg:hidden p-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{isMenuOpen ? (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{isMenuOpen && (
|
||||
<div className="lg:hidden bg-gray-100 border-t border-blue-900">
|
||||
<nav className="px-4 py-4">
|
||||
<ul className="space-y-4">
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="block font-medium py-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
href="/about"
|
||||
className="block font-medium py-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
href="/events"
|
||||
className="block font-medium py-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Events
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
href="/blogs"
|
||||
className="block font-medium py-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Blogs
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
href="/career"
|
||||
className="block font-medium py-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Career
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="block font-medium py-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link
|
||||
href="/teamMember"
|
||||
className="block font-medium py-2 text-blue-900 hover:text-red-600 transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Team Member
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
{/* Mobile Support Info */}
|
||||
<li className="pt-4 border-t border-blue-900">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex justify-center px-4 py-2 text-sm font-medium text-gray-100 bg-red-600 hover:bg-blue-900 rounded transition-colors"
|
||||
onClick={closeAllMenus}
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
74
src/components/about/AboutBreadcrumb.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;
|
||||
22
src/components/about/Introduction.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
const Introduction = () => {
|
||||
return (
|
||||
<section className="py-8 sm:py-12 bg-[#012068]">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<p className="text-base sm:text-lg leading-relaxed text-white ">
|
||||
Learn about our mission, vision, and the dedicated team behind the Trauma Care Center
|
||||
at CMC Hospital, Ranipet campus. We are committed to delivering world-class emergency
|
||||
and trauma services that focus on saving lives, reducing recovery time, and restoring
|
||||
hope for patients and their families. Our center integrates advanced medical technology,
|
||||
skilled professionals, and compassionate care to respond swiftly and effectively to
|
||||
critical injuries and emergencies. With a holistic approach that spans emergency response,
|
||||
surgical intervention, and rehabilitation, we strive to set new benchmarks in trauma
|
||||
management and patient-centered care.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Introduction;
|
||||
40
src/components/about/MissionVision.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Target, Eye } from 'lucide-react';
|
||||
|
||||
const MissionVision = () => {
|
||||
return (
|
||||
<section className="py-8 sm:py-12 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
||||
{/* Vision */}
|
||||
<div className="rounded-lg p-6 border-l-4" style={{ backgroundColor: '#f4f4f4', borderColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center mr-3" style={{ backgroundColor: '#012068' }}>
|
||||
<Eye className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold" style={{ color: '#012068' }}>Our Vision</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed text-base"style={{ color: '#333' }}>
|
||||
To stand as a centre of excellence for trauma care in South India—combining world-class clinical service with teaching and outreach grounded in faith.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mission */}
|
||||
<div className="rounded-lg p-6 border-l-4" style={{ backgroundColor: '#f4f4f4', borderColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center mr-3" style={{ backgroundColor: '#012068' }}>
|
||||
<Target className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold" style={{ color: '#012068' }}>Our Mission</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed text-base"style={{ color: '#333' }}>
|
||||
To reduce trauma-related deaths and lifelong disabilities by providing integrated, compassionate, evidence-based care.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MissionVision;
|
||||
64
src/components/about/PatientCareCards.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { Building2, Home } from 'lucide-react';
|
||||
|
||||
const PatientCareCards = () => {
|
||||
return (
|
||||
<section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
||||
{/* Inpatient Services */}
|
||||
<div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center mr-3"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<Building2 className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold" style={{ color: '#012068' }}>
|
||||
INPATIENT
|
||||
</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed mb-3 text-base" style={{ color: '#333' }}>
|
||||
Different facilities available include Trauma Intensive Care Unit, General ward, Semiprivate accommodation
|
||||
(with or without AC) and Private - single (with or without AC) & Deluxe rooms.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Outpatient Services */}
|
||||
<div className="bg-white rounded-lg p-6 border-l-4" style={{ borderColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center mr-3"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<Home className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold" style={{ color: '#012068' }}>
|
||||
OUTPATIENT
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="text-sm space-y-2">
|
||||
<li className="flex items-start" style={{ color: '#333' }}>
|
||||
<span
|
||||
className="w-2 h-2 rounded-full mr-3 mt-2"
|
||||
style={{ backgroundColor: '#012068'}}
|
||||
></span>
|
||||
Trauma Surgery - Every Monday and Friday are OP days.
|
||||
</li>
|
||||
<li className="flex items-start" style={{ color: '#333' }}>
|
||||
<span
|
||||
className="w-2 h-2 rounded-full mr-3 mt-2"
|
||||
style={{ backgroundColor: '#012068'}}
|
||||
></span>
|
||||
Acute Care Surgery Follow-up Clinic - Every Monday and Friday are OP days.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PatientCareCards;
|
||||
69
src/components/about/Services.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Leaf, Car, Activity, Globe } from 'lucide-react';
|
||||
|
||||
const Services = () => {
|
||||
const services = [
|
||||
{
|
||||
icon: <Leaf className="w-6 h-6" />,
|
||||
title: "Multi-system Polytrauma",
|
||||
description: "Comprehensive care for patients with multiple severe injuries requiring urgent intervention."
|
||||
},
|
||||
{
|
||||
icon: <Car className="w-6 h-6" />,
|
||||
title: "Road Traffic Injuries",
|
||||
description: "Expert trauma management for accidents involving motorbikes, cars, and other vehicles."
|
||||
},
|
||||
{
|
||||
icon: <Activity className="w-6 h-6" />,
|
||||
title: "Falls & Accidents",
|
||||
description: "Specialized treatment for injuries from falls, industrial incidents, and agricultural accidents."
|
||||
},
|
||||
{
|
||||
icon: <Globe className="w-6 h-6" />,
|
||||
title: "Referrals",
|
||||
description: "Providing trauma care support for referrals from Tamil Nadu, Andhra Pradesh, Karnataka, and overseas."
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-8 sm:py-12" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h2 className="text-2xl sm:text-3xl font-semibold text-center mb-2" style={{ color: '#012068' }}>
|
||||
Our Services
|
||||
</h2>
|
||||
<h2 className="text-md sm:text-xl font-normal text-center mb-8 sm:mb-12">
|
||||
We provide urgent care for
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{services.map((service, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-lg p-6 border border-gray-300 hover:shadow-lg transition-shadow duration-300"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div
|
||||
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<div className="text-white">
|
||||
{service.icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium mb-2" style={{ color: '#012068' }}>
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-base leading-relaxed"style={{ color: '#333' }}>
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Services;
|
||||
70
src/components/about/StatisticsTiles.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { ShieldPlus, UserCheck, Hospital, BookOpen } from 'lucide-react';
|
||||
|
||||
const StatisticsTiles = () => {
|
||||
const tiles = [
|
||||
{
|
||||
icon: <ShieldPlus className="w-6 h-6" style={{ color: '#012068' }} />,
|
||||
title: "Primary Trauma Care",
|
||||
description: "Specialized care for Priority One trauma patients in close coordination with the Emergency Department team."
|
||||
},
|
||||
{
|
||||
icon: <UserCheck className="w-6 h-6" style={{ color: '#012068' }} />,
|
||||
title: "24×7 Trauma Surgeon",
|
||||
description: "Round-the-clock availability of trauma surgeons ensures immediate surgical intervention when needed."
|
||||
},
|
||||
{
|
||||
icon: <Hospital className="w-6 h-6" style={{ color: '#012068' }} />,
|
||||
title: "Trauma Intensive & Ward Care",
|
||||
description: "Comprehensive trauma intensive care and dedicated trauma ward services for critical and recovering patients."
|
||||
},
|
||||
{
|
||||
icon: <BookOpen className="w-6 h-6" style={{ color: '#012068' }} />,
|
||||
title: "Trauma Education",
|
||||
description: "Focused education and counseling for patients and families to enhance recovery and awareness."
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-8 sm:py-12 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h2
|
||||
className="text-2xl sm:text-3xl font-semibold text-center mb-8 sm:mb-12"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Our Trauma Care Services
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
|
||||
{tiles.map((tile, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-300 rounded-lg p-5 h-full hover:shadow-lg transition-shadow duration-300"
|
||||
style={{ backgroundColor: '#f4f4f4' }}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center mb-3">
|
||||
<div className="flex-shrink-0 mr-3">
|
||||
{tile.icon}
|
||||
</div>
|
||||
<h3
|
||||
className="text-base sm:text-lg font-medium"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
{tile.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p
|
||||
className="text-sm sm:text-base leading-relaxed flex-grow" style={{ color: '#333' }}
|
||||
>
|
||||
{tile.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatisticsTiles;
|
||||
178
src/components/about/process.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
'use client'
|
||||
import React, { useState } from "react";
|
||||
|
||||
export default function Process() {
|
||||
const [activeStep, setActiveStep] = useState(-1);
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
number: "1",
|
||||
title: "2020",
|
||||
description: "Department of Trauma Surgery inaugurated at Town Campus",
|
||||
icon: CalendarIcon,
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
title: "2022",
|
||||
description: "Ranipet Campus opens as Level-1 Trauma Facility",
|
||||
icon: CalendarIcon,
|
||||
},
|
||||
{
|
||||
number: "3",
|
||||
title: "Nov 2022",
|
||||
description: "Trauma centre begins operations with full emergency suite",
|
||||
icon: CalendarIcon,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white py-12 sm:py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
|
||||
{/* Left Column */}
|
||||
<div className="flex flex-col justify-start">
|
||||
<div className="mb-8 lg:mb-16">
|
||||
<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 mb-8" style={{ color: '#333' }}>
|
||||
From the inauguration of our Department of Trauma Surgery to
|
||||
establishing a Level-1 Trauma Facility, we continue to expand our
|
||||
emergency care services with dedication and excellence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Steps */}
|
||||
<div className="relative">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative group cursor-pointer transition-all duration-500 ${
|
||||
index !== 0 ? "mt-8 sm:mt-12" : ""
|
||||
}`}
|
||||
onMouseEnter={() => setActiveStep(index)}
|
||||
onMouseLeave={() => setActiveStep(-1)}
|
||||
>
|
||||
{/* Connecting Line */}
|
||||
{index !== 0 && (
|
||||
<div className="absolute left-6 -top-8 sm:-top-12 w-0.5 h-8 sm:h-12 bg-gray-300 overflow-hidden">
|
||||
<div
|
||||
className={`w-full transition-all duration-700 ease-out origin-bottom ${
|
||||
activeStep >= index ||
|
||||
(activeStep === -1 && index === 0)
|
||||
? "h-full scale-y-100"
|
||||
: "h-0 scale-y-0"
|
||||
}`}
|
||||
style={{ backgroundColor: "#012068" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="flex items-start gap-4 sm:gap-6">
|
||||
{/* Number Circle */}
|
||||
<div
|
||||
className={`relative z-10 flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center text-white font-semibold text-lg transition-all duration-500 transform ${
|
||||
activeStep === index ||
|
||||
(activeStep === -1 && index === 0)
|
||||
? "scale-110 shadow-lg ring-4"
|
||||
: "scale-100 hover:scale-105"
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
activeStep === index ||
|
||||
(activeStep === -1 && index === 0)
|
||||
? "#012068"
|
||||
: "#333", // Black when inactive
|
||||
"--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", // Black text when inactive
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`mb-4 transition-all duration-500 transform ${
|
||||
activeStep === index ||
|
||||
(activeStep === -1 && index === 0)
|
||||
? "text-white"
|
||||
: "scale-100 group-hover:scale-105"
|
||||
}`}
|
||||
style={{
|
||||
color:
|
||||
activeStep === index ||
|
||||
(activeStep === -1 && index === 0)
|
||||
? "white"
|
||||
: "black", // Black icon when inactive
|
||||
}}
|
||||
>
|
||||
{step.icon}
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<h3 className="text-lg sm:text-xl font-semibold mb-3 transition-all duration-300">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm sm:text-base leading-relaxed transition-all duration-500">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
421
src/components/blogs/BlogDetail.tsx
Normal file
@ -0,0 +1,421 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ChevronRight, Clock, Calendar, Share2, ArrowLeft, Facebook, Twitter, Linkedin } from 'lucide-react';
|
||||
import { blogService, Blog } from '../../services/blogService'; // Adjust path as needed
|
||||
|
||||
const BlogDetail: React.FC = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const blogId = params.id as string;
|
||||
|
||||
const [blogData, setBlogData] = useState<Blog | null>(null);
|
||||
const [relatedPosts, setRelatedPosts] = useState<Blog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBlogData = async () => {
|
||||
if (!blogId) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Convert string ID to number for API call
|
||||
const numericId = parseInt(blogId, 10);
|
||||
if (isNaN(numericId)) {
|
||||
throw new Error('Invalid blog ID');
|
||||
}
|
||||
|
||||
console.log('Fetching blog with ID:', numericId);
|
||||
|
||||
// Fetch the specific blog and related posts
|
||||
const [blog, allBlogs] = await Promise.all([
|
||||
blogService.getBlogById(numericId),
|
||||
blogService.getPostedBlogs()
|
||||
]);
|
||||
|
||||
if (!blog) {
|
||||
setError('Blog not found');
|
||||
return;
|
||||
}
|
||||
|
||||
setBlogData(blog);
|
||||
|
||||
// Get related posts (same tags, exclude current blog)
|
||||
const related = allBlogs
|
||||
.filter(b => b.id !== blog.id && b.tags.some(tag => blog.tags.includes(tag)))
|
||||
.slice(0, 3);
|
||||
|
||||
// If not enough related posts with same tags, fill with other recent posts
|
||||
if (related.length < 3) {
|
||||
const otherPosts = allBlogs
|
||||
.filter(b => b.id !== blog.id && !related.some(r => r.id === b.id))
|
||||
.slice(0, 3 - related.length);
|
||||
related.push(...otherPosts);
|
||||
}
|
||||
|
||||
setRelatedPosts(related);
|
||||
} catch (err) {
|
||||
console.error('Error fetching blog:', err);
|
||||
setError('Failed to load blog. Please try again later.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (mounted) {
|
||||
fetchBlogData();
|
||||
}
|
||||
}, [blogId, mounted]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleGoBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const handleShare = async (platform?: string) => {
|
||||
if (!blogData || typeof window === 'undefined') return;
|
||||
|
||||
const url = window.location.href;
|
||||
const title = blogData.title;
|
||||
|
||||
try {
|
||||
if (platform === 'facebook') {
|
||||
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, '_blank');
|
||||
} else if (platform === 'twitter') {
|
||||
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`, '_blank');
|
||||
} else if (platform === 'linkedin') {
|
||||
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`, '_blank');
|
||||
} else {
|
||||
// Generic share or copy link
|
||||
if (navigator.share) {
|
||||
await navigator.share({
|
||||
title: title,
|
||||
url: url,
|
||||
});
|
||||
} else {
|
||||
await navigator.clipboard.writeText(url);
|
||||
alert('Blog link copied to clipboard!');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Share failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageError = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const target = event.target as HTMLImageElement;
|
||||
target.src = '/images/default-blog-image.jpg';
|
||||
};
|
||||
|
||||
const getAuthorName = (blog: Blog) => {
|
||||
if (blog.professors && blog.professors.length > 0) {
|
||||
return blog.professors.map(prof => prof.firstName || prof.name).join(', ');
|
||||
}
|
||||
return 'Medical Team';
|
||||
};
|
||||
|
||||
const getAuthorBio = (blog: Blog) => {
|
||||
if (blog.professors && blog.professors.length > 0) {
|
||||
return `Medical professional${blog.professors.length > 1 ? 's' : ''} specializing in trauma care and mental health support.`;
|
||||
}
|
||||
return 'Our medical team consists of experienced professionals dedicated to trauma care and mental health support.';
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading blog...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !blogData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* 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 mb-4">
|
||||
<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="/blogs"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Trauma Care Resources
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
||||
Blog Not Found
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="inline-flex items-center space-x-2 text-sm hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Resources</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="py-6">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="bg-white shadow-lg rounded-lg p-4 md:p-8 text-center">
|
||||
<h1 className="text-xl md:text-2xl font-medium mb-4" style={{ color: '#012068' }}>
|
||||
{error || 'Blog Not Found'}
|
||||
</h1>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="px-6 py-2 text-sm rounded-lg hover:opacity-90 transition-opacity"
|
||||
style={{ backgroundColor: '#012068', color: '#f4f4f4' }}
|
||||
>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* 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 mb-4">
|
||||
<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="/blogs"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Trauma Care Resources
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium truncate" style={{ color: '#e64838' }}>
|
||||
{blogData.title}
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="inline-flex items-center space-x-2 text-sm hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Resources</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Article Content */}
|
||||
<article className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Article Header */}
|
||||
<header className="mb-8">
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{blogData.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 text-sm font-medium rounded"
|
||||
style={{
|
||||
backgroundColor: '#f4f4f4',
|
||||
color: '#e64838'
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1
|
||||
className="text-3xl md:text-4xl font-bold mb-4 leading-tight"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
{blogData.title}
|
||||
</h1>
|
||||
|
||||
{/* Meta Information */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm" style={{ color: '#666' }}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{new Date(blogData.publishDate).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>{blogData.readTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Author Info */}
|
||||
<div className="flex items-start space-x-4 mt-6 p-4 rounded-lg" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="w-15 h-15 bg-gray-300 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-2xl font-medium" style={{ color: '#012068' }}>
|
||||
{getAuthorName(blogData).charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium" style={{ color: '#012068' }}>
|
||||
{getAuthorName(blogData)}
|
||||
</h3>
|
||||
<p className="text-sm mt-1" style={{ color: '#666' }}>
|
||||
{getAuthorBio(blogData)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Share Buttons */}
|
||||
<div className="flex items-center space-x-4 mb-8 pb-6 border-b border-gray-200">
|
||||
<span className="text-sm font-medium" style={{ color: '#012068' }}>Share:</span>
|
||||
<button
|
||||
onClick={() => handleShare('facebook')}
|
||||
className="p-2 rounded hover:bg-gray-100 transition-colors duration-200"
|
||||
>
|
||||
<Facebook className="w-5 h-5" style={{ color: '#1877f2' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleShare('twitter')}
|
||||
className="p-2 rounded hover:bg-gray-100 transition-colors duration-200"
|
||||
>
|
||||
<Twitter className="w-5 h-5" style={{ color: '#1da1f2' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleShare('linkedin')}
|
||||
className="p-2 rounded hover:bg-gray-100 transition-colors duration-200"
|
||||
>
|
||||
<Linkedin className="w-5 h-5" style={{ color: '#0077b5' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleShare()}
|
||||
className="p-2 rounded hover:bg-gray-100 transition-colors duration-200"
|
||||
>
|
||||
<Share2 className="w-5 h-5" style={{ color: '#666' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Article Content */}
|
||||
<div
|
||||
className="prose prose-lg max-w-none mb-12"
|
||||
style={{
|
||||
'--tw-prose-headings': '#012068',
|
||||
'--tw-prose-body': '#333',
|
||||
'--tw-prose-links': '#e64838',
|
||||
color:'#333'
|
||||
} as React.CSSProperties}
|
||||
dangerouslySetInnerHTML={{ __html: blogData.content || blogData.excerpt }}
|
||||
/>
|
||||
</article>
|
||||
|
||||
{/* Related Posts */}
|
||||
{relatedPosts.length > 0 && (
|
||||
<section className="py-12" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h2 className="text-2xl font-bold mb-8" style={{ color: '#012068' }}>
|
||||
Related Articles
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{relatedPosts.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="group bg-white rounded-lg overflow-hidden border border-gray-300 hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<Link href={`/blog-detail/${post.id}`} className="block">
|
||||
<div className="relative h-40 overflow-hidden">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
onError={handleImageError}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3
|
||||
className="font-medium text-sm line-clamp-2 mb-2 group-hover:opacity-70 transition-opacity duration-300"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2 text-xs" style={{ color: '#666' }}>
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{post.readTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<div className="p-8 rounded-lg" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<h2 className="text-2xl font-bold mb-4" style={{ color: '#012068' }}>
|
||||
Need Professional Support?
|
||||
</h2>
|
||||
<p className="text-base mb-6 max-w-2xl mx-auto" style={{ color: '#666' }}>
|
||||
If you or someone you know is struggling with trauma, don't hesitate to reach out for professional help. Our team is here to support you on your healing journey.
|
||||
</p>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-block px-6 py-3 text-sm font-medium rounded hover:opacity-90 transition-opacity duration-300"
|
||||
style={{
|
||||
backgroundColor: '#012068',
|
||||
color: '#f4f4f4'
|
||||
}}
|
||||
>
|
||||
Get Support Today
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogDetail;
|
||||
372
src/components/blogs/BlogListing.tsx
Normal file
@ -0,0 +1,372 @@
|
||||
// components/BlogListing.tsx
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { blogService, Blog } from '../../services/blogService'; // Adjust path as needed
|
||||
|
||||
const BlogListing: React.FC = () => {
|
||||
const [blogs, setBlogs] = useState<Blog[]>([]);
|
||||
const [filteredBlogs, setFilteredBlogs] = useState<Blog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [displayCount, setDisplayCount] = useState(6);
|
||||
const [tagCounts, setTagCounts] = useState<{ [key: string]: number }>({});
|
||||
|
||||
// Get unique categories from blogs with their counts
|
||||
const getCategories = () => {
|
||||
const allTags = blogs.flatMap(blog => blog.tags);
|
||||
const uniqueTags = Array.from(new Set(allTags));
|
||||
return ['All Categories', ...uniqueTags];
|
||||
};
|
||||
|
||||
// Filter blogs based on category and search query
|
||||
const filterBlogs = () => {
|
||||
let filtered = blogs;
|
||||
|
||||
// Filter by category
|
||||
if (selectedCategory !== 'All Categories') {
|
||||
filtered = filtered.filter(blog =>
|
||||
blog.tags.some(tag =>
|
||||
tag.toLowerCase().includes(selectedCategory.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
filtered = filtered.filter(blog =>
|
||||
blog.title.toLowerCase().includes(query) ||
|
||||
blog.excerpt.toLowerCase().includes(query) ||
|
||||
blog.tags.some(tag => tag.toLowerCase().includes(query)) ||
|
||||
(blog.professors && blog.professors.some(prof =>
|
||||
prof.firstName?.toLowerCase().includes(query) ||
|
||||
prof.name?.toLowerCase().includes(query)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
setFilteredBlogs(filtered);
|
||||
};
|
||||
|
||||
// Load blogs from API
|
||||
useEffect(() => {
|
||||
const loadBlogs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Load both posted blogs and tag counts
|
||||
const [fetchedBlogs, fetchedTagCounts] = await Promise.all([
|
||||
blogService.getPostedBlogs(),
|
||||
blogService.getTagsWithCount()
|
||||
]);
|
||||
|
||||
setBlogs(fetchedBlogs);
|
||||
setFilteredBlogs(fetchedBlogs);
|
||||
setTagCounts(fetchedTagCounts);
|
||||
} catch (err) {
|
||||
setError('Failed to load blogs. Please try again later.');
|
||||
console.error('Error loading blogs:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (mounted) {
|
||||
loadBlogs();
|
||||
}
|
||||
}, [mounted]);
|
||||
|
||||
// Filter blogs when category or search changes
|
||||
useEffect(() => {
|
||||
if (mounted && blogs.length > 0) {
|
||||
filterBlogs();
|
||||
}
|
||||
}, [selectedCategory, searchQuery, blogs, mounted]);
|
||||
|
||||
// Handle mount
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedCategory(e.target.value);
|
||||
setDisplayCount(6);
|
||||
};
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setDisplayCount(6);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
setDisplayCount(prev => prev + 6);
|
||||
};
|
||||
|
||||
const handleImageError = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
// Fallback to default image if the uploaded image fails to load
|
||||
const target = event.target as HTMLImageElement;
|
||||
target.src = '/images/default-blog-image.jpg';
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading blogs...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center text-red-600">
|
||||
<p className="text-xl mb-4">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-blue-900 text-white rounded hover:bg-blue-800"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = getCategories();
|
||||
const blogsToShow = filteredBlogs.slice(0, displayCount);
|
||||
const hasMoreBlogs = filteredBlogs.length > displayCount;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* 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' }}>
|
||||
Trauma Care Resources
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
|
||||
Trauma Care Resources
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
|
||||
Expert insights, healing strategies, and support resources for trauma recovery
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Filters Section */}
|
||||
<div className="flex justify-end items-center gap-4 max-w-7xl mx-auto px-4 py-8" style={{ backgroundColor: '#fff' }}>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={handleCategoryChange}
|
||||
className="appearance-none bg-gray-100 text-sm border border-blue-900 rounded-lg px-4 py-2 pr-8 focus:outline-none focus:border-blue-900"
|
||||
style={{ color: '#333' }}
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<option key={index} value={category}>
|
||||
{category}
|
||||
{category !== 'All Categories' && tagCounts[category] ? ` (${tagCounts[category]})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
placeholder="Search blogs..."
|
||||
className="border border-blue-900 rounded-lg px-4 py-2 pl-4 pr-10 text-sm focus:outline-none focus:border-blue-900 w-64 text-gray-700"
|
||||
/>
|
||||
<button className="absolute inset-y-0 right-0 flex items-center px-3 bg-blue-900 rounded-r-lg">
|
||||
<svg className="w-4 h-4 text-gray-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Info */}
|
||||
{(selectedCategory !== 'All Categories' || searchQuery.trim()) && (
|
||||
<div className="max-w-7xl mx-auto px-4 pb-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
{filteredBlogs.length === 0
|
||||
? 'No blogs found matching your criteria.'
|
||||
: `Showing ${blogsToShow.length} of ${filteredBlogs.length} blog${filteredBlogs.length !== 1 ? 's' : ''}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Blog Grid Section */}
|
||||
<section className="py-4" style={{ backgroundColor: '#fff' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{blogsToShow.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 text-lg mb-4">
|
||||
{searchQuery.trim() || selectedCategory !== 'All Categories'
|
||||
? 'No blogs match your search criteria.'
|
||||
: 'No blogs available at the moment.'
|
||||
}
|
||||
</p>
|
||||
{(searchQuery.trim() || selectedCategory !== 'All Categories') && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setSelectedCategory('All Categories');
|
||||
}}
|
||||
className="px-4 py-2 text-sm border border-blue-900 text-blue-900 rounded hover:bg-blue-50"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{blogsToShow.map((blog) => (
|
||||
<div
|
||||
key={blog.id}
|
||||
className="group relative bg-white border border-gray-300 rounded-lg overflow-hidden hover:shadow-lg transition-all duration-300 flex flex-col"
|
||||
>
|
||||
{/* All cards redirect to blog detail page with ID */}
|
||||
<Link
|
||||
href={`/blog-detail/${blog.id}`}
|
||||
className="absolute top-0 left-0 h-full w-full z-10"
|
||||
aria-label={`Read article: ${blog.title}`}
|
||||
/>
|
||||
|
||||
{/* Blog Image */}
|
||||
<div className="relative h-48 w-full overflow-hidden">
|
||||
<Image
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
onError={handleImageError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Blog Content */}
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{blog.tags.slice(0, 3).map((tag, tagIndex) => (
|
||||
<span
|
||||
key={tagIndex}
|
||||
className="px-2 py-1 text-xs font-medium rounded"
|
||||
style={{
|
||||
backgroundColor: '#f4f4f4',
|
||||
color: '#e64838'
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{blog.tags.length > 3 && (
|
||||
<span
|
||||
className="px-2 py-1 text-xs font-medium rounded"
|
||||
style={{
|
||||
backgroundColor: '#f4f4f4',
|
||||
color: '#666'
|
||||
}}
|
||||
>
|
||||
+{blog.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3
|
||||
className="text-lg font-medium mb-2 line-clamp-2 group-hover:opacity-70 transition-opacity duration-300"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
{blog.title}
|
||||
</h3>
|
||||
|
||||
{/* Excerpt */}
|
||||
<p
|
||||
className="text-xs leading-relaxed line-clamp-3 mb-4 flex-1"
|
||||
style={{ opacity: 0.8, color: "#333" }}
|
||||
>
|
||||
{blog.excerpt}
|
||||
</p>
|
||||
|
||||
{/* Authors (if available) */}
|
||||
{blog.professors && blog.professors.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<p className="text-xs text-gray-600">
|
||||
By: {blog.professors.map(prof => prof.firstName || prof.name).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Meta Information */}
|
||||
<div className="flex items-center justify-between text-xs mt-auto" style={{ color: '#333' }}>
|
||||
<span>{new Date(blog.publishDate).toLocaleDateString()}</span>
|
||||
<span>{blog.readTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Load More Button */}
|
||||
{hasMoreBlogs && (
|
||||
<div className="text-center mt-12">
|
||||
<button
|
||||
onClick={handleLoadMore}
|
||||
className="px-6 py-2 text-sm font-medium rounded hover:opacity-90 transition-opacity duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
style={{
|
||||
backgroundColor: '#012068',
|
||||
color: '#f4f4f4',
|
||||
'--tw-ring-color': '#012068'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
Load More Articles ({filteredBlogs.length - displayCount} remaining)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogListing;
|
||||
482
src/components/career/careerscomponent.tsx
Normal file
@ -0,0 +1,482 @@
|
||||
// components/Career.tsx
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { MapPin, Clock, DollarSign, Building, Users, ChevronRight } from 'lucide-react';
|
||||
import { careerService, Job, JobApplicationData } from '../../services/careerService';
|
||||
import { fileUploadService } from '../../services/fileUploadService';
|
||||
|
||||
const Career: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
experience: '',
|
||||
coverLetter: '',
|
||||
resume: null as File | null
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
loadJobs();
|
||||
}, []);
|
||||
|
||||
const loadJobs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const fetchedJobs = await careerService.getActiveJobs();
|
||||
setJobs(fetchedJobs);
|
||||
|
||||
// Set first job as default selection if available
|
||||
if (fetchedJobs.length > 0) {
|
||||
setSelectedJob(fetchedJobs[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load job listings. Please try again later.');
|
||||
console.error('Error loading jobs:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleJobSelect = (job: Job) => {
|
||||
setSelectedJob(job);
|
||||
setSubmitSuccess(false);
|
||||
// Reset form when job changes
|
||||
setFormData({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
experience: '',
|
||||
coverLetter: '',
|
||||
resume: null
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
resume: e.target.files![0]
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!selectedJob) {
|
||||
alert('Please select a job position.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
let resumeUrl = '';
|
||||
|
||||
// Upload resume if provided
|
||||
if (formData.resume) {
|
||||
try {
|
||||
const uploadResponse = await fileUploadService.uploadFile(formData.resume);
|
||||
resumeUrl = uploadResponse.url;
|
||||
} catch (uploadError) {
|
||||
console.error('Resume upload failed:', uploadError);
|
||||
setError('Failed to upload resume. Please try again.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare application data
|
||||
const applicationData: JobApplicationData = {
|
||||
jobId: parseInt(selectedJob.id),
|
||||
fullName: formData.fullName,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
experience: formData.experience,
|
||||
coverLetter: formData.coverLetter || undefined,
|
||||
resumeUrl: resumeUrl || undefined
|
||||
};
|
||||
|
||||
// Submit application
|
||||
const success = await careerService.submitApplication(applicationData);
|
||||
|
||||
if (success) {
|
||||
setSubmitSuccess(true);
|
||||
// Reset form
|
||||
setFormData({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
experience: '',
|
||||
coverLetter: '',
|
||||
resume: null
|
||||
});
|
||||
|
||||
// Clear file input
|
||||
const fileInput = document.getElementById('resume') as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
} else {
|
||||
setError('Failed to submit application. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error submitting application:', err);
|
||||
setError('Failed to submit application. Please try again.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getJobTitle = (): string => {
|
||||
if (!selectedJob) return 'Job Application';
|
||||
return `${selectedJob.title} - Application`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading career opportunities...</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' }}>
|
||||
Careers
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
|
||||
Join Our Team
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
|
||||
Explore career opportunities and be part of our mission to advance trauma care and medical excellence
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
<button
|
||||
onClick={loadJobs}
|
||||
className="text-red-600 underline text-sm mt-2"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<section className="py-6" style={{ backgroundColor: '#fff' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{jobs.length === 0 && !error ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 text-lg mb-4">No job openings available at the moment.</p>
|
||||
<p className="text-gray-400">Please check back later for new opportunities.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid lg:grid-cols-2 gap-6 lg:gap-8">
|
||||
|
||||
{/* Left Side - Job Listings */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-medium mb-4" style={{ color: '#012068' }}>
|
||||
Available Positions ({jobs.length})
|
||||
</h3>
|
||||
|
||||
{jobs.map((job) => (
|
||||
<div
|
||||
key={job.id}
|
||||
className={`bg-white rounded-lg border cursor-pointer transition-all duration-300 ${
|
||||
selectedJob?.id === job.id
|
||||
? 'shadow-lg border-blue-200'
|
||||
: 'border-gray-100 hover:shadow-md'
|
||||
}`}
|
||||
onClick={() => handleJobSelect(job)}
|
||||
>
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between mb-3 gap-2">
|
||||
<h4 className="text-base sm:text-lg font-medium" style={{ color: '#012068' }}>
|
||||
{job.title}
|
||||
</h4>
|
||||
<span
|
||||
className="px-2 py-1 text-xs font-medium rounded self-start"
|
||||
style={{ backgroundColor: '#012068', color: '#f4f4f4' }}
|
||||
>
|
||||
{job.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs mb-3" style={{ color: '#012068', opacity: 0.7 }}>
|
||||
<div className="flex items-center">
|
||||
<Building className="w-3 h-3 mr-1" />
|
||||
<span className="hidden sm:inline">{job.department}</span>
|
||||
<span className="sm:hidden">{job.department.split(' ')[0]}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<MapPin className="w-3 h-3 mr-1" />
|
||||
<span className="hidden sm:inline">{job.location}</span>
|
||||
<span className="sm:hidden">
|
||||
{job.location.includes('Vellore') ? 'Vellore' : job.location.split(',')[0]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
{job.experience}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs leading-relaxed mb-3 line-clamp-2" style={{ color: '#333' }}>
|
||||
{job.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between">
|
||||
<div className="flex items-center text-xs" style={{ color: '#012068', opacity: 0.7 }}>
|
||||
{job.salary}
|
||||
</div>
|
||||
<button className="text-xs font-medium hover:opacity-70 transition-opacity text-left sm:text-right" style={{ color: '#e64838' }}>
|
||||
{selectedJob?.id === job.id ? 'Selected' : 'Select & Apply'} →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right Side - Application Form */}
|
||||
<div className="lg:sticky lg:top-8 h-fit">
|
||||
<div className="bg-white rounded-lg border border-gray-300 p-4 sm:p-6">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-base sm:text-lg font-medium mb-2" style={{ color: '#012068' }}>
|
||||
{getJobTitle()}
|
||||
</h3>
|
||||
{selectedJob && (
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs" style={{ color: '#012068', opacity: 0.7 }}>
|
||||
<div className="flex items-center">
|
||||
<Building className="w-3 h-3 mr-1" />
|
||||
<span className="hidden sm:inline">{selectedJob.department}</span>
|
||||
<span className="sm:hidden">{selectedJob.department.split(' ')[0]}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<MapPin className="w-3 h-3 mr-1" />
|
||||
<span className="hidden sm:inline">{selectedJob.location}</span>
|
||||
<span className="sm:hidden">
|
||||
{selectedJob.location.includes('Vellore') ? 'Vellore' : selectedJob.location.split(',')[0]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{submitSuccess && (
|
||||
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-green-800 text-sm font-medium">Application Submitted Successfully!</p>
|
||||
<p className="text-green-700 text-xs mt-1">We'll review your application and get back to you soon.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="fullName" className="block text-xs font-medium mb-2" style={{ color: '#012068' }}>
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
value={formData.fullName}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
disabled={submitting}
|
||||
className="w-full px-3 py-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-2 focus:border-transparent disabled:bg-gray-100"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
placeholder="Enter your full name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-xs font-medium mb-2" style={{ color: '#012068' }}>
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
disabled={submitting}
|
||||
className="w-full px-3 py-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-2 focus:border-transparent disabled:bg-gray-100"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
placeholder="Enter your email address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-xs font-medium mb-2" style={{ color: '#012068' }}>
|
||||
Phone Number *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
disabled={submitting}
|
||||
className="w-full px-3 py-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-2 focus:border-transparent disabled:bg-gray-100"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
placeholder="Enter your phone number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="experience" className="block text-xs font-medium mb-2" style={{ color: '#012068' }}>
|
||||
Years of Experience *
|
||||
</label>
|
||||
<select
|
||||
id="experience"
|
||||
name="experience"
|
||||
value={formData.experience}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
disabled={submitting}
|
||||
className="w-full px-3 py-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-2 focus:border-transparent disabled:bg-gray-100"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
>
|
||||
<option value="">Select experience</option>
|
||||
<option value="0-1">0-1 years</option>
|
||||
<option value="1-3">1-3 years</option>
|
||||
<option value="3-5">3-5 years</option>
|
||||
<option value="5-10">5-10 years</option>
|
||||
<option value="10+">10+ years</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="resume" className="block text-xs font-medium mb-2" style={{ color: '#012068' }}>
|
||||
Resume/CV
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="resume"
|
||||
name="resume"
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.doc,.docx"
|
||||
disabled={submitting}
|
||||
className="w-full px-3 py-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-2 focus:border-transparent disabled:bg-gray-100"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
/>
|
||||
<p className="text-xs mt-1" style={{ color: '#012068', opacity: 0.7 }}>
|
||||
Accepted formats: PDF, DOC, DOCX (Max 5MB)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="coverLetter" className="block text-xs font-medium mb-2" style={{ color: '#012068' }}>
|
||||
Cover Letter
|
||||
</label>
|
||||
<textarea
|
||||
id="coverLetter"
|
||||
name="coverLetter"
|
||||
value={formData.coverLetter}
|
||||
onChange={handleInputChange}
|
||||
rows={4}
|
||||
disabled={submitting}
|
||||
className="w-full px-3 py-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-2 focus:border-transparent disabled:bg-gray-100"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
placeholder="Tell us why you're interested in this position..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !selectedJob}
|
||||
className="w-full px-6 py-3 text-sm font-medium rounded hover:opacity-90 transition-opacity duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
backgroundColor: '#012068',
|
||||
color: '#f4f4f4',
|
||||
'--tw-ring-color': '#012068'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{submitting ? 'Submitting...' : 'Submit Application'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Job Details */}
|
||||
{selectedJob && (
|
||||
<div className="mt-6 pt-6 border-t border-gray-300">
|
||||
<h4 className="text-base font-medium mb-3" style={{ color: '#012068' }}>Job Requirements</h4>
|
||||
<ul className="space-y-1">
|
||||
{selectedJob.requirements.slice(0, 3).map((req, index) => (
|
||||
<li key={index} className="text-sm flex items-start" style={{ color: '#333' }}>
|
||||
<span className="mr-2" style={{ color: '#e64838' }}>•</span>
|
||||
{req}
|
||||
</li>
|
||||
))}
|
||||
{selectedJob.requirements.length > 3 && (
|
||||
<li className="text-sm text-gray-500">
|
||||
+{selectedJob.requirements.length - 3} more requirements
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Career;
|
||||
383
src/components/contact-us/ContactForm.tsx
Normal file
@ -0,0 +1,383 @@
|
||||
'use client'
|
||||
import React, { useState, ChangeEvent, MouseEvent } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
phone: string;
|
||||
service: string;
|
||||
email: string;
|
||||
organization: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ContactForm: React.FC = () => {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
phone: '',
|
||||
service: '',
|
||||
email: '',
|
||||
organization: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: MouseEvent<HTMLButtonElement>): Promise<void> => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||
console.log('Form submitted:', formData);
|
||||
|
||||
// Reset form after successful submission
|
||||
setFormData({
|
||||
name: '',
|
||||
phone: '',
|
||||
service: '',
|
||||
email: '',
|
||||
organization: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
alert('Message sent successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
alert('Error sending message. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
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' }}>
|
||||
Contact Us
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
|
||||
Get in Touch
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-base max-w-2xl leading-relaxed"style={{ color: '#333' }}>
|
||||
We're here to help with your research and innovation needs. Reach out to our team for personalized support.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="py-8 px-4"style={{ backgroundColor: '#fff' }}>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
|
||||
|
||||
{/* Left side - Content */}
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-5">
|
||||
<h2 className="text-2xl lg:text-3xl font-bold leading-tight" style={{ color: '#012068' }}>
|
||||
Ready to collaborate on your next breakthrough?
|
||||
</h2>
|
||||
<p className="text-base leading-relaxed" style={{ color: '#333' }}>
|
||||
Whether you need genomics analysis, research consultation, or laboratory services, our expert team is ready to support your scientific endeavors.
|
||||
</p>
|
||||
|
||||
{/* Contact Features */}
|
||||
<div className="space-y-3 mt-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-md"style={{ color: '#333' }}>Quick response within 24 hours</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-md" style={{ color: '#333' }}>Expert consultation available</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-md" style={{ color: '#333' }}>Secure and confidential</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="rounded-lg border border-gray-300 p-6"style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<h3 className="text-lg font-bold mb-1" style={{ color: '#012068' }}>
|
||||
Contact Information
|
||||
</h3>
|
||||
<div className="w-9 h-0.5 rounded-lg mb-4" style={{ backgroundColor: '#e64838' }}></div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-1" style={{ color: '#012068' }}>Address</h4>
|
||||
<p className="text-xs leading-relaxed" style={{ color: '#333' }}>
|
||||
Department of Trauma Surgery<br />
|
||||
Room A601, 6th Floor, A Block<br />
|
||||
CMC Vellore Ranipet Campus<br />
|
||||
Kilminnal Village, Ranipet - 632517<br />
|
||||
Tamil Nadu, India
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-1" style={{ color: '#012068' }}>Phone</h4>
|
||||
<p className="text-xs" style={{ color: '#333' }}>
|
||||
0417-2224626
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-1" style={{ color: '#012068' }}>Email</h4>
|
||||
<p className="text-xs" style={{ color: '#333' }}>
|
||||
traumasurg@cmcvellore.ac.in
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Contact form */}
|
||||
<div className="bg-white rounded-lg border border-gray-300 p-6 lg:p-8">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl font-bold mb-3" style={{ color: '#012068' }}>
|
||||
Send us a message
|
||||
</h3>
|
||||
<div className="w-12 h-1 rounded-lg" style={{ backgroundColor: '#e64838' }}></div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Name and Phone row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Your Name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all duration-200 text-base"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
placeholder="Your Phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all duration-200 text-base"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service field */}
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="service"
|
||||
placeholder="Service and Product of Interest"
|
||||
value={formData.service}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all duration-200 text-base"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email and Organization row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Your Email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all duration-200 text-base"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="organization"
|
||||
placeholder="Organization"
|
||||
value={formData.organization}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all duration-200 text-base"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project description */}
|
||||
<div>
|
||||
<textarea
|
||||
name="description"
|
||||
placeholder="Project description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all duration-200 text-base resize-none"
|
||||
style={{ '--tw-ring-color': '#012068', color:'#333' } as React.CSSProperties}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit button */}
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
className={`w-full font-semibold py-3 px-6 rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 text-base ${
|
||||
isSubmitting
|
||||
? 'cursor-not-allowed opacity-60'
|
||||
: 'hover:opacity-90'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isSubmitting ? '#6b7280' : '#012068',
|
||||
color: '#f4f4f4',
|
||||
'--tw-ring-color': '#012068'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>Sending...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<span>Send Message</span>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact info */}
|
||||
<div className="mt-6 pt-6 border-t border-gray-300">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: '#e64838' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span style={{ color: '#012068' }}>Quick response guaranteed</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: '#e64838' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span style={{ color: '#012068' }}>Your data is secure</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map Section */}
|
||||
<div className="mt-12">
|
||||
<div className="bg-white rounded-lg border border-gray-300 overflow-hidden">
|
||||
<div className="h-96 relative">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3888.1234567890123!2d79.2376943!3d12.9384508!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3bad374304ae1249:0x77617f161cfad670!2sChristian+Medical+College+and+Hospital,+Ranipet+Campus+-+Vellore!5e0!3m2!1sen!2sin!4v1724612345678!5m2!1sen!2sin"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title="Location Map"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactForm;
|
||||
402
src/components/education/CourseDetail.tsx
Normal file
@ -0,0 +1,402 @@
|
||||
// components/CourseDetail.tsx
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ChevronRight,
|
||||
Clock,
|
||||
Users,
|
||||
Calendar,
|
||||
User,
|
||||
ExternalLink
|
||||
} from 'lucide-react';
|
||||
import { educationService, CourseApplicationData } from '../../services/educationService';
|
||||
import { fileUploadService } from '../../services/fileUploadService';
|
||||
|
||||
interface CourseDetailProps {
|
||||
courseId?: string;
|
||||
}
|
||||
|
||||
const CourseDetail: React.FC<CourseDetailProps> = ({ courseId }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [course, setCourse] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
|
||||
// Application form data
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
qualification: '',
|
||||
experience: '',
|
||||
coverLetter: '',
|
||||
resume: null as File | null
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
// Get course ID from URL params if not provided as prop
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const id = courseId || urlParams.get('id');
|
||||
|
||||
if (id) {
|
||||
loadCourseDetail(parseInt(id));
|
||||
} else {
|
||||
setError('Course ID not provided');
|
||||
setLoading(false);
|
||||
}
|
||||
}, [courseId]);
|
||||
|
||||
const loadCourseDetail = async (id: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const courseData = await educationService.getCourseById(id);
|
||||
if (courseData) {
|
||||
setCourse(courseData);
|
||||
} else {
|
||||
setError('Course not found');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load course details');
|
||||
console.error('Error loading course:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
resume: e.target.files![0]
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!course) {
|
||||
alert('Course not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
let resumeUrl = '';
|
||||
|
||||
// Upload resume if provided
|
||||
if (formData.resume) {
|
||||
try {
|
||||
const uploadResponse = await fileUploadService.uploadFile(formData.resume);
|
||||
resumeUrl = uploadResponse.url;
|
||||
} catch (uploadError) {
|
||||
console.warn('Resume upload failed, submitting application without resume:', uploadError);
|
||||
resumeUrl = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare application data
|
||||
const applicationData: CourseApplicationData = {
|
||||
courseId: parseInt(course.id),
|
||||
fullName: formData.fullName,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
qualification: formData.qualification,
|
||||
experience: formData.experience || undefined,
|
||||
coverLetter: formData.coverLetter || undefined,
|
||||
resumeUrl: resumeUrl || undefined
|
||||
};
|
||||
|
||||
// Submit application
|
||||
const success = await educationService.submitApplication(applicationData);
|
||||
|
||||
if (success) {
|
||||
setSubmitSuccess(true);
|
||||
// Reset form
|
||||
setFormData({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
qualification: '',
|
||||
experience: '',
|
||||
coverLetter: '',
|
||||
resume: null
|
||||
});
|
||||
|
||||
// Clear file input
|
||||
const fileInput = document.getElementById('resume') as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
} else {
|
||||
setError('Failed to submit application. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error submitting application:', err);
|
||||
setError('Failed to submit application. Please try again.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading course details...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !course) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 text-xl mb-4">⚠️ {error || 'Course not found'}</p>
|
||||
<Link href="/education-training" className="text-blue-600 hover:underline">
|
||||
← Back to Education & Training
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Header Section with Background Pattern */}
|
||||
<section
|
||||
className="py-8 relative overflow-hidden"
|
||||
style={{ backgroundColor: '#f4f4f4' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 relative z-10">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center space-x-2 text-sm mb-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<Link
|
||||
href="/education-training"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Education & Training
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
||||
Course Details
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Course Title and Meta */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center mb-4">
|
||||
<h1 className="text-4xl font-bold" style={{ color: '#012068' }}>
|
||||
{course.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-6 text-lg mb-6" style={{ color: '#012068' }}>
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-5 h-5 mr-2" />
|
||||
<span>Duration: {course.duration}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Users className="w-5 h-5 mr-2" />
|
||||
<span>No of seats: {course.seats}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Course Info Bar */}
|
||||
<section className="py-6 border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<User className="w-4 h-4 mr-2" style={{ color: '#012068' }} />
|
||||
<span style={{ color: '#666' }}>Instructor: </span>
|
||||
<span className="font-medium" style={{ color: '#012068' }}>
|
||||
{course.instructor}
|
||||
</span>
|
||||
</div>
|
||||
{course.startDate && (
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-2" style={{ color: '#012068' }} />
|
||||
<span style={{ color: '#666' }}>Start Date: </span>
|
||||
<span className="font-medium" style={{ color: '#012068' }}>
|
||||
{new Date(course.startDate).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
className="px-6 py-2 text-sm font-medium rounded border-2 hover:opacity-70 transition-opacity duration-300"
|
||||
style={{
|
||||
borderColor: '#012068',
|
||||
color: '#012068'
|
||||
}}
|
||||
>
|
||||
Download Brochure
|
||||
</button>
|
||||
<button
|
||||
className="px-6 py-2 mr-2 text-sm font-medium rounded hover:opacity-90 transition-opacity duration-300"
|
||||
style={{
|
||||
backgroundColor: '#012068',
|
||||
color: 'white'
|
||||
}}
|
||||
onClick={() => {
|
||||
document.getElementById('applicationForm')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
Apply Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content - Full Width */}
|
||||
<section className="py-8">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
|
||||
{/* Left Column - Course Information */}
|
||||
<div>
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-8 space-y-12">
|
||||
|
||||
{/* Overview Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-6" style={{ color: '#012068' }}>
|
||||
Overview
|
||||
</h2>
|
||||
<div className="prose max-w-none">
|
||||
<p className="text-base leading-relaxed mb-6" style={{ color: '#666' }}>
|
||||
{course.description}
|
||||
</p>
|
||||
|
||||
{course.objectives && course.objectives.length > 0 && (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold mb-4" style={{ color: '#012068' }}>
|
||||
Learning Objectives
|
||||
</h3>
|
||||
<p className="text-sm mb-4" style={{ color: '#666' }}>
|
||||
The main objectives of the program can be summarised as follows:
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{course.objectives.map((objective: string, index: number) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="text-sm font-medium mr-3" style={{ color: '#012068' }}>
|
||||
•
|
||||
</span>
|
||||
<span className="text-sm leading-relaxed" style={{ color: '#666' }}>
|
||||
{objective}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Eligibility Criteria Section */}
|
||||
{course.eligibility && course.eligibility.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-6" style={{ color: '#012068' }}>
|
||||
Eligibility Criteria
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border border-gray-200 rounded-lg">
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#f8f9fa' }}>
|
||||
<th
|
||||
className="px-6 py-4 text-left text-sm font-semibold border-b border-gray-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Sr. No.
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-4 text-left text-sm font-semibold border-b border-gray-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Eligibility Requirements
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{course.eligibility.map((criteria: string, index: number) => (
|
||||
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-6 py-4 text-sm border-b border-gray-200" style={{ color: '#012068' }}>
|
||||
{index + 1}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm border-b border-gray-200" style={{ color: '#666' }}>
|
||||
{criteria}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* How to Apply Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-6" style={{ color: '#012068' }}>
|
||||
How to Apply
|
||||
</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-sm mb-2" style={{ color: '#666' }}>
|
||||
Use the application form on the right to apply for this course, or visit our admissions website:
|
||||
</p>
|
||||
<a
|
||||
href="https://admissions.cmcvellore.ac.in/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center text-sm font-medium hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#e64838' }}
|
||||
>
|
||||
https://admissions.cmcvellore.ac.in/
|
||||
<ExternalLink className="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseDetail;
|
||||
417
src/components/education/EducationTraining.tsx
Normal file
@ -0,0 +1,417 @@
|
||||
// components/EducationTraining.tsx
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ChevronRight, Clock, Users, Award, Calendar, Globe, GraduationCap } from 'lucide-react';
|
||||
import { educationService, Course } from '../../services/educationService';
|
||||
import { upcomingEventsService, UpcomingEvent } from '../../services/upcomingEventsService';
|
||||
|
||||
const EducationTraining: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [upcomingEvents, setUpcomingEvents] = useState<UpcomingEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
const categories = ['All', 'Certification', 'Training', 'Workshop', 'Fellowship'];
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Load both courses and upcoming events concurrently
|
||||
const [fetchedCourses, fetchedEvents] = await Promise.all([
|
||||
educationService.getActiveCourses(),
|
||||
upcomingEventsService.getActiveUpcomingEvents()
|
||||
]);
|
||||
|
||||
setCourses(fetchedCourses);
|
||||
setUpcomingEvents(fetchedEvents);
|
||||
} catch (err) {
|
||||
setError('Failed to load courses and events. Please try again later.');
|
||||
console.error('Error loading data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter courses based on category and search
|
||||
const filteredCourses = courses.filter(course => {
|
||||
const matchesCategory = selectedCategory === 'All' || course.category === selectedCategory;
|
||||
const matchesSearch = !searchQuery.trim() ||
|
||||
course.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
course.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
course.instructor.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchQuery(e.target.value);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading courses...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Header Section */}
|
||||
<section
|
||||
className="py-4 relative overflow-hidden"
|
||||
style={{ backgroundColor: '#f4f4f4' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 relative z-10">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center space-x-2 text-sm mb-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
||||
Education & Training
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mb-2">
|
||||
<h1 className="text-4xl font-bold mb-4" style={{ color: '#012068' }}>
|
||||
Education & Training
|
||||
</h1>
|
||||
<p className="text-lg max-w-3xl leading-relaxed" style={{ color: '#333' }}>
|
||||
Advance your trauma care expertise with structured training programs for doctors, nurses, students, and community partners.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="text-red-600 underline text-sm mt-2"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upcoming Training Section with Dynamic Cards */}
|
||||
<section className="py-8" style={{ backgroundColor: '#fff' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<h2 className="text-3xl font-bold mb-8 text-center" style={{ color: '#012068' }}>
|
||||
Upcoming Training Programs
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{upcomingEvents.length > 0 ? (
|
||||
upcomingEvents.map((event) => (
|
||||
<div key={event.id} className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
|
||||
{event.title}
|
||||
</h3>
|
||||
<div className="flex items-center text-sm">
|
||||
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
|
||||
<span style={{ color: '#666' }}>{event.schedule}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
// Fallback to static cards if no events are loaded
|
||||
<>
|
||||
<div className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
|
||||
Simulation-based Team Drills
|
||||
</h3>
|
||||
<div className="flex items-center text-sm">
|
||||
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
|
||||
<span>Q3 2025</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
|
||||
Hands-on simulation training designed to improve team coordination and emergency response in high-pressure trauma situations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
|
||||
Online Webinar Series
|
||||
</h3>
|
||||
<div className="flex items-center text-sm">
|
||||
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
|
||||
<span>Monthly Sessions</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
|
||||
Monthly online sessions covering trauma ethics, young doctor support, and professional development in emergency medicine.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow duration-300 border-l-4" style={{ borderLeftColor: '#012068' }}>
|
||||
<div className="flex items-center mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold" style={{ color: '#012068' }}>
|
||||
Community Education
|
||||
</h3>
|
||||
<div className="flex items-center text-sm">
|
||||
<Calendar className="w-4 h-4 mr-1" style={{ color: '#e64838' }} />
|
||||
<span>Ongoing</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#666' }}>
|
||||
Road safety fairs and school education sessions to promote trauma prevention and basic first aid awareness in the community.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Filter & Search */}
|
||||
<section className="py-8" style={{ backgroundColor: '#fff' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
|
||||
{/* Category Filter */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${selectedCategory === category
|
||||
? 'text-white'
|
||||
: 'border-2'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: selectedCategory === category ? '#012068' : 'transparent',
|
||||
borderColor: '#012068',
|
||||
color: selectedCategory === category ? 'white' : '#012068'
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search programs..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
className="border-2 rounded-lg px-4 py-2 pl-4 pr-10 text-sm focus:outline-none w-64"
|
||||
style={{ borderColor: '#012068', color:'#333' }}
|
||||
/>
|
||||
<button className="absolute inset-y-0 right-0 flex items-center px-3 rounded-lg"
|
||||
style={{ backgroundColor: '#012068' }}>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Info */}
|
||||
{(selectedCategory !== 'All' || searchQuery.trim()) && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
{filteredCourses.length === 0
|
||||
? 'No courses found matching your criteria.'
|
||||
: `Showing ${filteredCourses.length} of ${courses.length} course${courses.length !== 1 ? 's' : ''}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Courses Grid */}
|
||||
<section className="pb-12" style={{ backgroundColor: '#fff' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{filteredCourses.length === 0 && !error ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 text-lg mb-4">
|
||||
{searchQuery.trim() || selectedCategory !== 'All'
|
||||
? 'No courses match your search criteria.'
|
||||
: 'No courses available at the moment.'
|
||||
}
|
||||
</p>
|
||||
{(searchQuery.trim() || selectedCategory !== 'All') && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setSelectedCategory('All');
|
||||
}}
|
||||
className="px-4 py-2 text-sm border border-blue-900 text-blue-900 rounded hover:bg-blue-50"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 auto-rows-fr">
|
||||
{filteredCourses.map((course) => (
|
||||
<Link
|
||||
key={course.id}
|
||||
href={`/education-training/course-detail?id=${course.id}`}
|
||||
className="group bg-white rounded-lg overflow-hidden border border-gray-300 hover:shadow-xl transition-all duration-300 flex flex-col h-full cursor-pointer"
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="relative h-48 overflow-hidden flex-shrink-0">
|
||||
<Image
|
||||
src={course.image}
|
||||
alt={course.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="p-6 flex-grow flex flex-col">
|
||||
<h3
|
||||
className="text-lg font-semibold mb-3 group-hover:opacity-70 transition-opacity duration-300 h-14 overflow-hidden"
|
||||
style={{
|
||||
color: '#012068',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical'
|
||||
}}
|
||||
>
|
||||
{course.title}
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm leading-relaxed mb-4 flex-grow overflow-hidden"
|
||||
style={{
|
||||
color: '#666',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 4,
|
||||
WebkitBoxOrient: 'vertical'
|
||||
}}
|
||||
>
|
||||
{course.description}
|
||||
</p>
|
||||
<div className="mt-auto">
|
||||
<span
|
||||
className="inline-block px-3 py-1 text-xs font-medium rounded-full border-2"
|
||||
style={{
|
||||
color: '#e64838',
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
{course.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pb-6 pt-2 border-t border-gray-100 flex-shrink-0">
|
||||
<div className="flex items-end mb-3">
|
||||
<span className="text-sm font-medium truncate" style={{ color: '#012068' }}>
|
||||
{course.instructor}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-3 text-sm" style={{ color: '#666' }}>
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1 flex-shrink-0" />
|
||||
<span className="truncate">{course.duration}</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-2">
|
||||
<Users className="w-4 h-4 mr-1 flex-shrink-0" />
|
||||
<span>{course.seats}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm" style={{ color: '#666' }}>
|
||||
{course.startDate ? `Starts: ${new Date(course.startDate).toLocaleDateString()}` : 'Contact for dates'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-16" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<Award className="w-12 h-12 mx-auto mb-6" style={{ color: '#e64838' }} />
|
||||
<h2 className="text-3xl font-bold mb-4" style={{ color: '#012068' }}>
|
||||
Ready to Advance Your Trauma Care Expertise?
|
||||
</h2>
|
||||
<p className="text-lg mb-8 max-w-2xl mx-auto" style={{ color: '#666' }}>
|
||||
Join our structured training programs designed to empower healthcare professionals, nurses, and community responders with critical trauma care skills.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-block px-6 py-3 text-sm font-medium rounded hover:opacity-90 transition-opacity duration-300"
|
||||
style={{
|
||||
backgroundColor: '#012068',
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
Contact Admissions
|
||||
</Link>
|
||||
<Link
|
||||
href="/brochure"
|
||||
className="inline-block px-6 py-3 text-sm font-medium rounded border-2 hover:opacity-70 transition-opacity duration-300"
|
||||
style={{
|
||||
borderColor: '#012068',
|
||||
color: '#012068'
|
||||
}}
|
||||
>
|
||||
Download Brochure
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationTraining;
|
||||
513
src/components/events/EventDetail.tsx
Normal file
@ -0,0 +1,513 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Calendar, MapPin, Clock, Users, Star, Share2, ArrowLeft, ChevronRight } from 'lucide-react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import Link from "next/link";
|
||||
import { eventAPI, Event } from '../../lib/api'; // Adjust path as needed
|
||||
|
||||
const EventDetail = () => {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const eventId = params.id as string;
|
||||
|
||||
const [eventData, setEventData] = useState<Event | null>(null);
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [bookingStatus, setBookingStatus] = useState<'idle' | 'booking' | 'success' | 'error'>('idle');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEventData = async () => {
|
||||
if (!eventId) {
|
||||
console.log('No event ID provided');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Fetching event with ID:', eventId);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Convert string ID to number for API call
|
||||
const numericId = parseInt(eventId, 10);
|
||||
if (isNaN(numericId)) {
|
||||
console.error('Invalid event ID:', eventId);
|
||||
throw new Error('Invalid event ID');
|
||||
}
|
||||
console.log('Converted to numeric ID:', numericId);
|
||||
|
||||
const event = await eventAPI.getEventById(numericId);
|
||||
console.log('Fetched event:', event);
|
||||
setEventData(event);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch event data:', error);
|
||||
setEventData(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEventData();
|
||||
}, [eventId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventData && typeof window !== 'undefined') {
|
||||
try {
|
||||
const bookmarkedEvents = JSON.parse(localStorage.getItem('bookmarkedEvents') || '[]');
|
||||
setIsBookmarked(bookmarkedEvents.includes(eventData.id.toString()));
|
||||
} catch (error) {
|
||||
console.error('Error accessing localStorage:', error);
|
||||
}
|
||||
}
|
||||
}, [eventData]);
|
||||
|
||||
const handleBookmark = () => {
|
||||
if (!eventData || typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const bookmarkedEvents = JSON.parse(localStorage.getItem('bookmarkedEvents') || '[]');
|
||||
const eventIdStr = eventData.id.toString();
|
||||
let updatedBookmarks;
|
||||
if (isBookmarked) {
|
||||
updatedBookmarks = bookmarkedEvents.filter((id: string) => id !== eventIdStr);
|
||||
} else {
|
||||
updatedBookmarks = [...bookmarkedEvents, eventIdStr];
|
||||
}
|
||||
localStorage.setItem('bookmarkedEvents', JSON.stringify(updatedBookmarks));
|
||||
setIsBookmarked(!isBookmarked);
|
||||
} catch (error) {
|
||||
console.error('Error updating bookmarks:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBookSeat = async () => {
|
||||
if (!eventData) return;
|
||||
setBookingStatus('booking');
|
||||
try {
|
||||
// Replace with actual booking API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
setBookingStatus('success');
|
||||
setTimeout(() => setBookingStatus('idle'), 2000);
|
||||
} catch (error) {
|
||||
setBookingStatus('error');
|
||||
setTimeout(() => setBookingStatus('idle'), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (!eventData || typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
if (navigator.share) {
|
||||
await navigator.share({
|
||||
title: eventData.title,
|
||||
url: window.location.href,
|
||||
});
|
||||
} else {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
alert('Event link copied to clipboard!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Share failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const getBookingButtonText = () => {
|
||||
switch (bookingStatus) {
|
||||
case 'booking': return 'Booking...';
|
||||
case 'success': return 'Booked!';
|
||||
case 'error': return 'Try Again';
|
||||
default: return 'Book Your Seat';
|
||||
}
|
||||
};
|
||||
|
||||
const getBookingButtonStyle = () => {
|
||||
switch (bookingStatus) {
|
||||
case 'booking': return 'bg-gray-600 cursor-not-allowed';
|
||||
case 'success': return 'bg-green-600 hover:bg-green-700';
|
||||
case 'error': return 'bg-red-600 hover:bg-red-700';
|
||||
default: return 'hover:opacity-90';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper functions to format API data for display
|
||||
const formatPrice = (event: Event) => {
|
||||
if (event.fee && event.fee.length > 0) {
|
||||
return `₹${event.fee[0].cost} per seat`;
|
||||
}
|
||||
return '₹1,800 per seat';
|
||||
};
|
||||
|
||||
const getPrimaryVenue = (event: Event) => {
|
||||
if (event.venue && event.venue.length > 0) {
|
||||
return {
|
||||
name: event.venue[0].title || 'Medical College Auditorium',
|
||||
address: event.venue[0].address || 'Chennai, Tamil Nadu'
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'Medical College Auditorium',
|
||||
address: 'Chennai, Tamil Nadu'
|
||||
};
|
||||
};
|
||||
|
||||
const getSafeImageUrl = (imageUrl: string | undefined, fallback: string) => {
|
||||
return imageUrl && imageUrl.trim() !== '' ? imageUrl : fallback;
|
||||
};
|
||||
|
||||
const getGalleryImages = (galleryImages: string[] | undefined) => {
|
||||
const fallbackImages = [
|
||||
'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=400&h=200&fit=crop'
|
||||
];
|
||||
|
||||
if (!galleryImages || galleryImages.length === 0) {
|
||||
return fallbackImages;
|
||||
}
|
||||
|
||||
const validImages = galleryImages.filter(img => img && img.trim() !== '');
|
||||
if (validImages.length === 0) {
|
||||
return fallbackImages;
|
||||
}
|
||||
|
||||
// Ensure we have at least 2 images for the layout
|
||||
while (validImages.length < 2) {
|
||||
validImages.push(fallbackImages[validImages.length % fallbackImages.length]);
|
||||
}
|
||||
|
||||
return validImages.slice(0, 2);
|
||||
};
|
||||
|
||||
// Format description data for display
|
||||
const getFormattedDescription = (event: Event) => {
|
||||
return {
|
||||
overview: event.description || 'Join leading medical professionals for this comprehensive event focusing on the latest developments in healthcare.',
|
||||
highlights: event.highlights && event.highlights.length > 0 ? event.highlights : [
|
||||
'Expert presentations and case studies',
|
||||
'Latest medical innovations and techniques',
|
||||
'Hands-on workshops and demonstrations',
|
||||
'Networking sessions with industry leaders'
|
||||
],
|
||||
includes: [
|
||||
'CME Credits',
|
||||
'Course materials and resources',
|
||||
'Networking breaks and lunch',
|
||||
'Certificate of attendance'
|
||||
],
|
||||
targetAudience: event.subject ? [event.subject] : [
|
||||
'Medical professionals',
|
||||
'Healthcare practitioners',
|
||||
'Medical students and residents',
|
||||
'Healthcare administrators'
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
{/* Breadcrumb Section */}
|
||||
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<nav className="flex items-center space-x-2 text-sm">
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<a
|
||||
href="/events"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Medical Events
|
||||
</a>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
||||
Loading...
|
||||
</span>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="py-6">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="bg-white shadow-lg rounded-lg overflow-hidden">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-64 md:h-80 bg-gray-200"></div>
|
||||
<div className="p-4 md:p-8">
|
||||
<div className="h-4 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-6 md:h-8 bg-gray-200 rounded mb-6"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-4 bg-gray-200 rounded"></div>
|
||||
<div className="h-4 bg-gray-200 rounded"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventData) {
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
{/* Breadcrumb Section */}
|
||||
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<nav className="flex items-center space-x-2 text-sm">
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<a
|
||||
href="/events"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Medical Events
|
||||
</a>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
||||
Event Not Found
|
||||
</span>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="py-6">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="bg-white shadow-lg rounded-lg p-4 md:p-8 text-center">
|
||||
<h1 className="text-xl md:text-2xl font-medium mb-4" style={{ color: '#012068' }}>
|
||||
Event Not Found
|
||||
</h1>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="px-6 py-2 text-sm rounded-lg hover:opacity-90 transition-opacity"
|
||||
style={{ backgroundColor: '#012068', color: '#f4f4f4' }}
|
||||
>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const venue = getPrimaryVenue(eventData);
|
||||
const description = getFormattedDescription(eventData);
|
||||
const galleryImages = getGalleryImages(eventData.galleryImages);
|
||||
const mainImage = getSafeImageUrl(eventData.mainImage, 'https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=800&h=400&fit=crop');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
{/* Breadcrumb Section */}
|
||||
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<nav className="flex items-center space-x-2 text-sm">
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<a
|
||||
href="/events"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Medical Events
|
||||
</a>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium truncate" style={{ color: '#e64838' }}>
|
||||
{eventData.title}
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Back Button */}
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="flex items-center hover:opacity-70 text-sm transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="py-6">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{/* Image Section */}
|
||||
<div className="bg-white shadow-lg rounded-md overflow-hidden mb-6" style={{ borderColor: '#012068' }}>
|
||||
<div className="relative">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-1 h-64 md:h-80">
|
||||
<div className="md:col-span-2 relative overflow-hidden">
|
||||
<img
|
||||
src={mainImage}
|
||||
alt={eventData.title}
|
||||
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden md:block space-y-1">
|
||||
{galleryImages.map((image, index) => (
|
||||
<div key={index} className="h-1/2 relative overflow-hidden">
|
||||
<img
|
||||
src={image}
|
||||
alt={`Gallery ${index + 1}`}
|
||||
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Details Section */}
|
||||
<div className="bg-white shadow-lg rounded-lg overflow-hidden" style={{ borderColor: '#012068' }}>
|
||||
<div className="p-4 md:p-8">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start gap-4 mb-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium mb-2" style={{ color: '#e64838' }}>
|
||||
{eventData.date}
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-3xl font-medium leading-tight" style={{ color: '#012068' }}>
|
||||
{eventData.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-left sm:text-right w-full sm:w-auto">
|
||||
<div className="text-xs mb-1" style={{ color: '#012068', opacity: 0.8 }}>From</div>
|
||||
<div className="text-lg font-medium mb-3" style={{ color: '#e64838' }}>
|
||||
{formatPrice(eventData)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleBookSeat}
|
||||
disabled={bookingStatus === 'booking'}
|
||||
className={`w-full sm:w-auto px-6 py-2 text-sm rounded-lg transition-all duration-200 ${getBookingButtonStyle()}`}
|
||||
style={{
|
||||
backgroundColor: bookingStatus === 'idle' ? '#012068' : undefined,
|
||||
color: '#f4f4f4'
|
||||
}}
|
||||
>
|
||||
{getBookingButtonText()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Info Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<div className="flex items-center" style={{ color: '#012068' }}>
|
||||
<Calendar className="w-4 h-4 mr-3 flex-shrink-0" style={{ color: '#e64838' }} />
|
||||
<div>
|
||||
<div className="text-sm font-medium">{eventData.date}</div>
|
||||
<div className="text-xs opacity-80">Full Day Event</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ color: '#012068' }}>
|
||||
<Clock className="w-4 h-4 mr-3 flex-shrink-0" style={{ color: '#e64838' }} />
|
||||
<div>
|
||||
<div className="text-sm font-medium">9:00 AM - 5:00 PM</div>
|
||||
<div className="text-xs opacity-80">8 hours</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center sm:col-span-2 lg:col-span-1" style={{ color: '#012068' }}>
|
||||
<MapPin className="w-4 h-4 mr-3 flex-shrink-0" style={{ color: '#e64838' }} />
|
||||
<div>
|
||||
<div className="text-sm font-medium">{venue.name}</div>
|
||||
<div className="text-xs opacity-80">{venue.address}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* About Section */}
|
||||
<div className="space-y-4 mb-8">
|
||||
<h3 className="text-lg font-medium" style={{ color: '#012068' }}>About This Event</h3>
|
||||
<p className="text-md leading-relaxed" style={{ color: '#333' }}>{description.overview}</p>
|
||||
|
||||
{description.highlights.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-md font-medium mb-2" style={{ color: '#012068' }}>Event Highlights</h4>
|
||||
<ul className="text-sm space-y-1" style={{ color: '#333' }}>
|
||||
{description.highlights.map((highlight, index) => (
|
||||
<li key={index}>• {highlight}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Attendees Info */}
|
||||
<div className="mb-8 p-4 rounded-lg" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div className="flex items-center" style={{ color: '#012068' }}>
|
||||
<Users className="w-4 h-4 mr-2 flex-shrink-0" style={{ color: '#e64838' }} />
|
||||
<span className="text-xs">Medical professionals attending</span>
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#012068', opacity: 0.8 }}>
|
||||
Event ID: #{eventData.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Cards */}
|
||||
<div className="mb-8 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 border rounded-lg" style={{ borderColor: '#012068' }}>
|
||||
<h4 className="text-sm font-medium mb-2" style={{ color: '#012068' }}>What's Included</h4>
|
||||
<ul className="text-xs space-y-1" style={{ color: '#333' }}>
|
||||
{description.includes.map((item, index) => (
|
||||
<li key={index}>• {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg" style={{ borderColor: '#012068' }}>
|
||||
<h4 className="text-sm font-medium mb-2" style={{ color: '#012068' }}>Target Audience</h4>
|
||||
<ul className="text-xs space-y-1" style={{ color: '#333' }}>
|
||||
{description.targetAudience.map((audience, index) => (
|
||||
<li key={index}>• {audience}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="flex items-center text-xs hover:underline"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
<Share2 className="w-3 h-3 mr-1" />
|
||||
Share Event
|
||||
</button>
|
||||
<span className="text-gray-400 hidden sm:inline">|</span>
|
||||
<span className="text-xs" style={{ color: '#012068', opacity: 0.8 }}>
|
||||
📍 {venue.name}, {venue.address}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDetail;
|
||||
415
src/components/events/MedicalEventsComponent.tsx
Normal file
@ -0,0 +1,415 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import Link from "next/link";
|
||||
import { eventAPI, Event } from '../../lib/api'; // Adjust path as needed
|
||||
|
||||
const MedicalEventsComponent = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('Upcoming Events');
|
||||
const [activeTab, setActiveTab] = useState('Upcoming Events');
|
||||
const [upcomingEvents, setUpcomingEvents] = useState<Event[]>([]);
|
||||
const [pastEvents, setPastEvents] = useState<Event[]>([]);
|
||||
const [nextEvent, setNextEvent] = useState<Event | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
loadEvents();
|
||||
}, []);
|
||||
|
||||
const loadEvents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [upcoming, past] = await Promise.all([
|
||||
eventAPI.getUpcomingEvents(),
|
||||
eventAPI.getPastEvents()
|
||||
]);
|
||||
|
||||
setUpcomingEvents(upcoming);
|
||||
setPastEvents(past);
|
||||
|
||||
// Set the next event (first upcoming event)
|
||||
if (upcoming.length > 0) {
|
||||
setNextEvent(upcoming[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading events:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter events based on search term
|
||||
const filteredEvents = () => {
|
||||
const events = activeTab === 'Upcoming Events' ? upcomingEvents : pastEvents;
|
||||
if (!searchTerm) return events;
|
||||
|
||||
return events.filter(event =>
|
||||
event.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
event.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
event.subject.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
// Navigation function for App Router
|
||||
const navigateToEventDetail = (eventId: string | number) => {
|
||||
router.push(`/event-detail/${eventId}`);
|
||||
};
|
||||
|
||||
// Format price display from fees
|
||||
const formatPrice = (event: Event) => {
|
||||
if (event.fee && event.fee.length > 0) {
|
||||
return `₹${event.fee[0].cost} per seat`;
|
||||
}
|
||||
return '₹1,800 per seat'; // fallback price
|
||||
};
|
||||
|
||||
// Get safe image URL with fallback
|
||||
const getSafeImageUrl = (imageUrl: string | undefined, fallback: string) => {
|
||||
return imageUrl && imageUrl.trim() !== '' ? imageUrl : fallback;
|
||||
};
|
||||
|
||||
// Get gallery images with fallbacks
|
||||
const getGalleryImages = (galleryImages: string[] | undefined) => {
|
||||
const fallbackImages = [
|
||||
'https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1638202993928-7267aad84c31?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=200&h=100&fit=crop'
|
||||
];
|
||||
|
||||
if (!galleryImages || galleryImages.length === 0) {
|
||||
return fallbackImages;
|
||||
}
|
||||
|
||||
// Fill missing images with fallbacks
|
||||
const validImages = galleryImages.filter(img => img && img.trim() !== '');
|
||||
while (validImages.length < 4 && validImages.length < fallbackImages.length) {
|
||||
validImages.push(fallbackImages[validImages.length]);
|
||||
}
|
||||
|
||||
return validImages.slice(0, 4);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-lg" style={{ color: '#012068' }}>Loading events...</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' }}>
|
||||
Medical Events
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<h1 className="text-2xl md:text-3xl font-bold" style={{ color: '#012068' }}>
|
||||
Medical Events
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-sm md:text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
|
||||
Discover upcoming medical conferences, workshops, and professional development events
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="py-6" style={{ backgroundColor: '#fff' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{/* Header with dropdown and search */}
|
||||
<div className="flex flex-col sm:flex-row justify-end items-start sm:items-center gap-4 mb-6">
|
||||
<div className="relative w-full sm:w-auto">
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
className="appearance-none bg-white text-sm border rounded-lg px-4 py-2 pr-8 focus:outline-none w-full sm:w-auto"
|
||||
style={{
|
||||
borderColor: '#012068',
|
||||
color: '#333'
|
||||
}}
|
||||
>
|
||||
<option>Upcoming Events</option>
|
||||
<option>This Week</option>
|
||||
<option>This Month</option>
|
||||
<option>This Year</option>
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg className="w-4 h-4" style={{ color: '#012068' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full sm:w-64">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search an event..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border rounded-lg px-4 py-2 pl-4 pr-10 text-sm focus:outline-none w-full"
|
||||
style={{
|
||||
borderColor: '#012068',
|
||||
color: '#012068'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="absolute inset-y-0 right-0 flex items-center px-3 rounded-r-lg"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Next Event Hero Section */}
|
||||
{nextEvent && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium mb-4" style={{ color: '#012068' }}>Next Event</h2>
|
||||
<div
|
||||
className="bg-white border border-gray-100 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
|
||||
onClick={() => navigateToEventDetail(nextEvent.id)}
|
||||
>
|
||||
<div className="relative h-48 md:h-64">
|
||||
<img
|
||||
src={getSafeImageUrl(nextEvent.mainImage, "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=800&h=300&fit=crop")}
|
||||
alt={nextEvent.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2">
|
||||
<div className="w-2 h-2 bg-white rounded-full shadow"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium mb-3" style={{ color: '#e64838' }}>{nextEvent.date}</div>
|
||||
<div className="text-lg md:text-xl font-medium mb-2" style={{ color: '#012068' }}>
|
||||
{nextEvent.title}
|
||||
</div>
|
||||
<div className="text-xs leading-relaxed mb-1" style={{ color: '#333' }}>
|
||||
{nextEvent.description}
|
||||
</div>
|
||||
<div className="text-xs leading-relaxed mb-4" style={{ color: '#333' }}>
|
||||
{nextEvent.detail}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<div
|
||||
className="text-xs cursor-pointer hover:underline"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Share Event
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#012068' }}>
|
||||
📍 {nextEvent.venue?.[0]?.address || 'Convention Center, Medical District'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left lg:text-right">
|
||||
<button
|
||||
className="w-full lg:w-auto px-6 py-2 text-sm rounded-lg mb-2 transition-colors hover:opacity-90"
|
||||
style={{
|
||||
backgroundColor: '#012068',
|
||||
color: '#f4f4f4'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Book seat clicked');
|
||||
}}
|
||||
>
|
||||
Book Your Seat
|
||||
</button>
|
||||
<div className="text-sm font-medium" style={{ color: '#e64838' }}>{formatPrice(nextEvent)}</div>
|
||||
<div className="text-xs mt-1" style={{ color: '#333' }}>
|
||||
Early bird discount available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
{/* Main Content */}
|
||||
<div className="flex-1">
|
||||
{/* Tabs */}
|
||||
<div className="border-b mb-6" style={{ borderColor: '#666666ff' }}>
|
||||
<div className="flex space-x-4 md:space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('Upcoming Events')}
|
||||
className={`pb-3 text-sm ${
|
||||
activeTab === 'Upcoming Events'
|
||||
? 'border-b-2 font-medium'
|
||||
: 'hover:opacity-70'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: activeTab === 'Upcoming Events' ? '#e64838' : 'transparent',
|
||||
color: activeTab === 'Upcoming Events' ? '#012068' : '#012068'
|
||||
}}
|
||||
>
|
||||
Upcoming Events ({upcomingEvents.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('Past Events')}
|
||||
className={`pb-3 text-sm ${
|
||||
activeTab === 'Past Events'
|
||||
? 'border-b-2 font-medium'
|
||||
: 'hover:opacity-70'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: activeTab === 'Past Events' ? '#e64838' : 'transparent',
|
||||
color: activeTab === 'Past Events' ? '#012068' : '#012068'
|
||||
}}
|
||||
>
|
||||
Past Events ({pastEvents.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Rows */}
|
||||
<div className="space-y-6">
|
||||
{filteredEvents().length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">No events found.</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredEvents().map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="bg-white border border-gray-100 rounded-lg p-4 md:p-6 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
onClick={() => navigateToEventDetail(event.id)}
|
||||
>
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Images Section */}
|
||||
<div className="flex flex-col sm:flex-row gap-1 md:w-auto">
|
||||
{/* Main image */}
|
||||
<div className="w-full sm:w-48 h-32 md:h-30 flex-shrink-0 rounded-xs overflow-hidden">
|
||||
<img
|
||||
src={getSafeImageUrl(event.mainImage, "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=200&fit=crop")}
|
||||
alt={event.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gallery grid */}
|
||||
<div className="grid grid-cols-2 gap-1 w-full sm:w-60 h-32 md:h-30">
|
||||
{getGalleryImages(event.galleryImages).map((img, index) => (
|
||||
<div key={index} className="rounded-xs overflow-hidden">
|
||||
<img
|
||||
src={img}
|
||||
alt={`${event.title} gallery ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event details */}
|
||||
<div className="flex-1 text-sm">
|
||||
<div className="mb-1 font-medium text-xs" style={{ color: '#e64838' }}>
|
||||
{event.date}
|
||||
</div>
|
||||
<div className="font-medium mb-2 text-lg md:text-xl" style={{ color: '#012068' }}>
|
||||
{event.title}
|
||||
</div>
|
||||
<div className="text-xs leading-relaxed mb-1" style={{ color: '#333' }}>
|
||||
{event.description}
|
||||
</div>
|
||||
<div className="text-xs leading-relaxed mb-4" style={{ color: '#333' }}>
|
||||
{event.detail}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<button
|
||||
className="text-xs hover:underline text-left"
|
||||
style={{ color: '#012068' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateToEventDetail(event.id);
|
||||
}}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
<span className="text-gray-400 hidden sm:inline">|</span>
|
||||
<span className="text-xs font-medium" style={{ color: '#e64838' }}>
|
||||
{formatPrice(event)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar */}
|
||||
<div className="w-full lg:w-64 lg:border-l lg:pl-4" style={{ borderColor: '#666666ff' }}>
|
||||
<h3 className="text-lg font-medium mb-6" style={{ color: '#012068' }}>
|
||||
Recent Past Events
|
||||
</h3>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-1 gap-6">
|
||||
{pastEvents.slice(0, 2).map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="cursor-pointer hover:bg-gray-50 p-2 rounded-lg transition-colors"
|
||||
onClick={() => navigateToEventDetail(event.id)}
|
||||
>
|
||||
<div className="w-full h-32 lg:h-28 mb-3 overflow-hidden rounded-xs">
|
||||
<img
|
||||
src={getSafeImageUrl(event.mainImage, "https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=300&h=200&fit=crop")}
|
||||
alt={event.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="mb-1 font-medium text-xs" style={{ color: '#e64838' }}>
|
||||
{event.date}
|
||||
</div>
|
||||
<div className="font-medium mb-1" style={{ color: '#012068' }}>
|
||||
{event.title}
|
||||
</div>
|
||||
<div className="text-xs leading-relaxed mb-1" style={{ color: '#333' }}>
|
||||
{event.description}
|
||||
</div>
|
||||
<div className="text-xs leading-relaxed" style={{ color: '#333' }}>
|
||||
{event.detail}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MedicalEventsComponent;
|
||||
286
src/components/faculty/TeamListing.tsx
Normal file
@ -0,0 +1,286 @@
|
||||
'use client'
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { FacultyService, TeamMember } from '../../lib/facultyData';
|
||||
|
||||
interface TeamListingProps {
|
||||
title?: string;
|
||||
onMemberClick?: (member: TeamMember) => void;
|
||||
}
|
||||
|
||||
const TeamListing: React.FC<TeamListingProps> = ({
|
||||
title = "Our Faculty",
|
||||
onMemberClick
|
||||
}) => {
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadFacultyData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const faculty = await FacultyService.getAllFaculty();
|
||||
setTeamMembers(faculty);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Failed to load faculty data:', err);
|
||||
setError('Failed to load faculty data. Please try again later.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadFacultyData();
|
||||
}, []);
|
||||
|
||||
// Filter members by category
|
||||
const facultyMembers = teamMembers.filter(member => member.category === 'FACULTY');
|
||||
const supportTeam = teamMembers.filter(member => member.category === 'SUPPORT_TEAM');
|
||||
const traineesAndFellows = teamMembers.filter(member => member.category === 'TRAINEE_FELLOW');
|
||||
|
||||
const handleMemberClick = (member: TeamMember) => {
|
||||
if (onMemberClick) {
|
||||
onMemberClick(member);
|
||||
} else {
|
||||
window.location.href = `/faculty/${member.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 faculty data...</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 TeamMemberCard: React.FC<{ member: TeamMember }> = ({ member }) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
|
||||
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
console.error('Image failed to load:', member.image);
|
||||
console.error('Member:', member.name, 'Professor ID:', member.professorId);
|
||||
|
||||
if (!imageError) {
|
||||
setImageError(true);
|
||||
target.src = '/images/default-avatar.jpg';
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageLoad = () => {
|
||||
console.log('Image loaded successfully:', member.image);
|
||||
setImageLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group cursor-pointer bg-white rounded-lg border border-gray-300 overflow-hidden hover:shadow-lg transition-all duration-300"
|
||||
onClick={() => handleMemberClick(member)}
|
||||
>
|
||||
<div className="relative aspect-square overflow-hidden">
|
||||
{imageLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
className={`w-full h-full object-cover transition-transform duration-300 group-hover:scale-105 ${
|
||||
imageLoading ? 'opacity-0' : 'opacity-100'
|
||||
}`}
|
||||
onError={handleImageError}
|
||||
onLoad={handleImageLoad}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{imageError && (
|
||||
<div className="absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded">
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 sm:p-6">
|
||||
<h3 className="text-lg font-medium mb-2 group-hover:opacity-70 transition-opacity" style={{ color: '#012068' }}>
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#333' }}>
|
||||
{member.position}
|
||||
</p>
|
||||
{member.department && (
|
||||
<p className="text-xs mt-1" style={{ color: '#666' }}>
|
||||
{member.department}
|
||||
</p>
|
||||
)}
|
||||
{member.specialty && (
|
||||
<p className="text-xs mt-1 font-medium" style={{ color: '#e64838' }}>
|
||||
{member.specialty}
|
||||
</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' }}>
|
||||
Faculty
|
||||
</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' }}>
|
||||
Meet our dedicated team of medical professionals, researchers, and support staff committed to advancing healthcare, education, and patient outcomes at Christian Medical College, Vellore
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Faculty Section */}
|
||||
{facultyMembers.length > 0 && (
|
||||
<section className="py-8" 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' }}>
|
||||
Faculty Members
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: '#666' }}>
|
||||
Our experienced faculty members leading medical education and research
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{facultyMembers.map((member) => (
|
||||
<TeamMemberCard key={member.id} member={member} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Support Team Section */}
|
||||
{supportTeam.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' }}>
|
||||
Support Team
|
||||
</h2>
|
||||
<p className="text-base leading-relaxed mb-2" style={{ color: '#012068' }}>
|
||||
<strong>Clinical Support Staff</strong> - Essential team members providing specialized support services
|
||||
</p>
|
||||
<p className="text-sm" style={{ color: '#012068' }}>
|
||||
<strong>Administrative & Technical Support</strong> - Dedicated professionals ensuring smooth operations
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{supportTeam.map((member) => (
|
||||
<TeamMemberCard key={member.id} member={member} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Trainees & Fellows Section */}
|
||||
{traineesAndFellows.length > 0 && (
|
||||
<section className="py-8" 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' }}>
|
||||
Trainees & Fellows
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: '#666' }}>
|
||||
Medical trainees, residents, and fellows advancing their skills and contributing to patient care
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{traineesAndFellows.map((member) => (
|
||||
<TeamMemberCard key={member.id} member={member} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Show message if no faculty data */}
|
||||
{teamMembers.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 Faculty Data Available
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Faculty information is currently being updated. Please check back later or contact administration.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Refresh Page
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Show message if all categories are empty but we have data */}
|
||||
{teamMembers.length > 0 && facultyMembers.length === 0 && supportTeam.length === 0 && traineesAndFellows.length === 0 && (
|
||||
<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 Categorized Faculty Available
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Faculty members need to be assigned to categories. Please contact administration.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamListing;
|
||||
391
src/components/faculty/TeamMemberDetail.tsx
Normal file
@ -0,0 +1,391 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Phone,
|
||||
Mail,
|
||||
Share,
|
||||
ChevronRight,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { TeamMember, FacultyService } from '../../lib/facultyData';
|
||||
|
||||
interface TeamMemberDetailProps {
|
||||
memberId: number;
|
||||
memberData?: TeamMember;
|
||||
}
|
||||
|
||||
const TeamMemberDetail: React.FC<TeamMemberDetailProps> = ({ memberId, memberData }) => {
|
||||
const [member, setMember] = useState<TeamMember | null>(memberData || null);
|
||||
const [loading, setLoading] = useState(!memberData);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showSocialShare, setShowSocialShare] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadMemberData = async () => {
|
||||
if (memberData) return; // Already have data
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const fetchedMember = await FacultyService.getFacultyById(memberId);
|
||||
if (fetchedMember) {
|
||||
setMember(fetchedMember);
|
||||
} else {
|
||||
setError('Faculty member not found');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load faculty member:', err);
|
||||
setError('Failed to load faculty member data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadMemberData();
|
||||
}, [memberId, memberData]);
|
||||
|
||||
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 faculty member details...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !member) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 mb-4">{error || 'Faculty member not found'}</p>
|
||||
<Link
|
||||
href="/teamMember"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Back to Faculty
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Create a comprehensive description from the member's details
|
||||
const getFullDescription = () => {
|
||||
return member.description || `${member.name} is a dedicated member of our faculty with expertise in ${member.specialty?.toLowerCase() || 'medical practice'}. They bring valuable experience in surgical education, patient care, and clinical research to Christian Medical College, Vellore.`;
|
||||
};
|
||||
|
||||
// Parse phone numbers if multiple are provided
|
||||
const getFormattedPhone = () => {
|
||||
if (!member.phone || member.phone === 'Not available') {
|
||||
return 'Contact through main office';
|
||||
}
|
||||
return member.phone.includes(',')
|
||||
? member.phone.split(',').map(p => p.trim()).join(' / ')
|
||||
: member.phone;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* 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' }} />
|
||||
<Link
|
||||
href="/teamMember"
|
||||
className="hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Faculty
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#012068' }} />
|
||||
<span className="font-medium" style={{ color: '#e64838' }}>
|
||||
{member.name}
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
|
||||
{member.name}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-base max-w-2xl leading-relaxed" style={{ color: '#333' }}>
|
||||
{member.designation || member.position} {member.experience && `with ${member.experience} of experience`} at CMC Vellore
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-7xl mx-auto px-4 py-8 bg-white">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-4 gap-6 lg:gap-8">
|
||||
{/* Sidebar */}
|
||||
<div className="xl:col-span-1">
|
||||
<div className="bg-white rounded-md border border-gray-300 overflow-hidden sticky top-8">
|
||||
{/* Profile Image */}
|
||||
<div className="aspect-square relative overflow-hidden">
|
||||
<img
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-300"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = '/images/default-avatar.jpg';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profile Info */}
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="mb-6">
|
||||
<div
|
||||
className="inline-flex items-center py-1 text-sm font-medium mb-3"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
{member.designation || member.position}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: '#012068' }}>{member.name}</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mb-6 leading-relaxed" style={{ color: '#333' }}>
|
||||
{member.description || `${member.name} is a dedicated faculty member at CMC Vellore.`}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="flex items-start">
|
||||
<Phone className="w-4 h-4 mr-3 mt-0.5 flex-shrink-0" style={{ color: '#e64838' }} />
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1" style={{ color: '#012068' }}>Phone</div>
|
||||
<div className="text-sm" style={{ color: '#333' }}>{getFormattedPhone()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<Mail className="w-4 h-4 mr-3 mt-0.5 flex-shrink-0" style={{ color: '#e64838' }} />
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1" style={{ color: '#012068' }}>Email</div>
|
||||
<a href={`mailto:${member.email}`} className="text-sm hover:underline transition-colors" style={{ color: '#e64838' }}>
|
||||
{member.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{member.experience && (
|
||||
<div className="flex items-start">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full mr-3 mt-0.5 flex-shrink-0 flex items-center justify-center"
|
||||
style={{ backgroundColor: '#e64838' }}
|
||||
>
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1" style={{ color: '#012068' }}>Experience</div>
|
||||
<div className="text-sm" style={{ color: '#333' }}>{member.experience}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{member.department && (
|
||||
<div className="flex items-start">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full mr-3 mt-0.5 flex-shrink-0 flex items-center justify-center"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1" style={{ color: '#012068' }}>Department</div>
|
||||
<div className="text-sm" style={{ color: '#333' }}>{member.department}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{member.officeLocation && (
|
||||
<div className="flex items-start">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full mr-3 mt-0.5 flex-shrink-0 flex items-center justify-center"
|
||||
style={{ backgroundColor: '#012068' }}
|
||||
>
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1" style={{ color: '#012068' }}>Office Location</div>
|
||||
<div className="text-sm" style={{ color: '#333' }}>{member.officeLocation}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Social Share */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowSocialShare(!showSocialShare)}
|
||||
className="flex items-center justify-center w-full px-4 py-3 rounded-lg transition-opacity font-medium text-sm hover:opacity-90"
|
||||
style={{ backgroundColor: '#f4f4f4', color: '#012068' }}
|
||||
>
|
||||
<Share className="w-4 h-4 mr-2" />
|
||||
Share Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="xl:col-span-3">
|
||||
<div className="bg-white rounded-md border border-gray-300 overflow-hidden">
|
||||
{/* Personal Info */}
|
||||
<div className="p-6 sm:p-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center mb-6">
|
||||
<h3 className="text-xl font-semibold" style={{ color: '#012068' }}>About</h3>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-lg max-w-none mb-8 leading-relaxed">
|
||||
<p className="first-letter:text-4xl first-letter:font-semibold first-letter:float-left first-letter:mr-2 first-letter:leading-none first-letter:mt-1 text-base first-letter:text-blue-900 text-gray-700">
|
||||
{getFullDescription()}
|
||||
</p>
|
||||
|
||||
<p className="mt-6 text-base text-gray-700">
|
||||
As a dedicated member of the Department of {member.department || 'Medicine'} at Christian Medical College, Vellore,
|
||||
{member.name.split(' ')[1] || member.name} contributes to advancing medical education, patient care, and
|
||||
clinical research. Their commitment to excellence in healthcare and medical education makes
|
||||
them an invaluable asset to the medical community and the patients they serve.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Details Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg p-4 border border-gray-300" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="font-medium mb-2 text-sm" style={{ color: '#012068' }}>Clinical Focus & Specialty</div>
|
||||
<div className="text-sm leading-relaxed" style={{ color: '#333' }}>{member.specialty || 'General Medicine'}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg p-4 border border-gray-300" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="font-medium mb-2 text-sm" style={{ color: '#012068' }}>Education & Certification</div>
|
||||
<div className="text-sm leading-relaxed" style={{ color: '#333' }}>{member.certification || 'Medical certification details available upon request'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg p-4 border border-gray-300" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="font-medium mb-2 text-sm" style={{ color: '#012068' }}>Training & Professional Development</div>
|
||||
<div className="text-sm leading-relaxed" style={{ color: '#333' }}>{member.training || 'Professional training details available upon request'}</div>
|
||||
</div>
|
||||
|
||||
{member.workDays && member.workDays.length > 0 && (
|
||||
<div className="rounded-lg p-4 border border-gray-300" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="font-medium mb-3 text-sm" style={{ color: '#012068' }}>Clinical Days</div>
|
||||
<div className="space-y-2">
|
||||
{member.workDays.map((day, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<div
|
||||
className="w-4 h-4 rounded flex items-center justify-center mr-3"
|
||||
style={{ backgroundColor: '#e64838' }}
|
||||
>
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-medium" style={{ color: '#012068' }}>{day}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills and Awards */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8 lg:gap-12">
|
||||
{/* Research Interests & Skills */}
|
||||
{member.skills && member.skills.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center mb-6">
|
||||
<h3 className="text-xl font-semibold" style={{ color: '#012068' }}>Research Interests & Expertise</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mb-6 leading-relaxed" style={{ color: '#333' }}>
|
||||
Areas of clinical expertise, research focus, and professional competencies that contribute to advancing medical practice and education at CMC Vellore.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{member.skills.map((skill, index) => (
|
||||
<div key={index} className="rounded-lg p-4 border border-gray-300" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-sm" style={{ color: '#012068' }}>{skill.name}</span>
|
||||
{skill.level && (
|
||||
<span className="text-xs" style={{ color: '#666' }}>
|
||||
{skill.level}% proficiency
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Awards */}
|
||||
{member.awards && member.awards.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center mb-6">
|
||||
<h3 className="text-xl font-semibold" style={{ color: '#012068' }}>Awards and Recognition</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mb-6 leading-relaxed" style={{ color: '#333' }}>
|
||||
Professional achievements, awards, and recognition received for excellence in medical practice, research, and education.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
{member.awards.map((award, index) => (
|
||||
<div key={index} className="rounded-lg p-4 border border-gray-300" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="flex items-start space-x-3">
|
||||
<img
|
||||
src={award.image}
|
||||
alt={award.title}
|
||||
className="w-12 h-12 rounded object-cover border border-gray-300 flex-shrink-0"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = '/images/award-icon.png';
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div
|
||||
className="inline-flex items-center px-2 py-1 rounded text-xs font-medium mb-2"
|
||||
style={{ backgroundColor: '#e64838', color: 'white' }}
|
||||
>
|
||||
{award.year}
|
||||
</div>
|
||||
<h4 className="text-base font-medium mb-2" style={{ color: '#012068' }}>{award.title}</h4>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#333' }}>{award.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamMemberDetail;
|
||||
258
src/components/home/EventSection.tsx
Normal file
@ -0,0 +1,258 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { eventAPI, Event } from '../../lib/api';
|
||||
|
||||
const EventsSection = () => {
|
||||
const router = useRouter();
|
||||
const [upcomingEvents, setUpcomingEvents] = useState<Event[]>([]);
|
||||
const [pastEvents, setPastEvents] = useState<Event[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadEvents();
|
||||
}, []);
|
||||
|
||||
const loadEvents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [upcoming, past] = await Promise.all([
|
||||
eventAPI.getUpcomingEvents(),
|
||||
eventAPI.getPastEvents()
|
||||
]);
|
||||
setUpcomingEvents(upcoming);
|
||||
setPastEvents(past);
|
||||
} catch (error) {
|
||||
console.error('Error loading events:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Navigation function for App Router
|
||||
const navigateToEventDetail = (eventId: string | number) => {
|
||||
router.push(`/event-detail/${eventId}`);
|
||||
};
|
||||
|
||||
const navigateToAllEvents = () => {
|
||||
router.push('/events');
|
||||
};
|
||||
|
||||
// Format price display from fees
|
||||
const formatPrice = (event: Event) => {
|
||||
if (event.fee && event.fee.length > 0) {
|
||||
return `₹${event.fee[0].cost} per ticket`;
|
||||
}
|
||||
return '₹1,800 per ticket';
|
||||
};
|
||||
|
||||
// Top section events (first 4 events for the grid)
|
||||
const topEvents = upcomingEvents.slice(0, 4);
|
||||
|
||||
// Featured event (first event)
|
||||
const featuredEvent = upcomingEvents.length > 0 ? upcomingEvents[0] : null;
|
||||
|
||||
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" style={{ color: '#012068' }}>Loading events...</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' }}>
|
||||
Our Events
|
||||
</h2>
|
||||
<button
|
||||
className="text-sm font-medium hover:opacity-70 transition-opacity duration-200"
|
||||
style={{ color: '#e64838' }}
|
||||
onClick={navigateToAllEvents}
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Events Grid - Top Section with Event Details */}
|
||||
{topEvents.length === 0 ? (
|
||||
<div className="text-center py-8 mb-12">
|
||||
<p className="text-gray-500">No upcoming events.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
{topEvents.map((event, index) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`relative rounded-lg h-48 overflow-hidden cursor-pointer hover:shadow-lg transition-shadow ${
|
||||
index === 2 ? 'lg:hidden xl:block' : ''
|
||||
} ${index === 3 ? 'md:col-span-2 lg:col-span-1' : ''}`}
|
||||
onClick={() => navigateToEventDetail(event.id)}
|
||||
>
|
||||
<img
|
||||
src={event.mainImage}
|
||||
alt={event.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* Gradient overlay for better text readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent"></div>
|
||||
|
||||
{/* Event details overlay */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 text-white">
|
||||
<div className="text-xs font-medium mb-1" style={{ color: '#e64838' }}>
|
||||
{event.date}
|
||||
</div>
|
||||
<h4 className="font-medium mb-2 text-sm leading-tight">
|
||||
{event.title}
|
||||
</h4>
|
||||
<div className="text-xs font-medium">
|
||||
{formatPrice(event)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Featured Events Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left - Large Featured Event */}
|
||||
<div className="lg:col-span-2 px-4 py-4 rounded-lg" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<h3 className="text-xl font-semibold mb-4" style={{ color: '#012068' }}>Upcoming Events</h3>
|
||||
{!featuredEvent ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">No upcoming events.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="bg-white border border-gray-100 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
|
||||
onClick={() => navigateToEventDetail(featuredEvent.id)}
|
||||
>
|
||||
<div className="relative h-48 md:h-64">
|
||||
<img
|
||||
src={featuredEvent.mainImage}
|
||||
alt={featuredEvent.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* Pagination dots */}
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2">
|
||||
<div className="w-2 h-2 bg-white rounded-full shadow"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium mb-3" style={{ color: '#e64838' }}>
|
||||
{featuredEvent.date}
|
||||
</div>
|
||||
<h4 className="text-lg md:text-xl font-medium mb-2" style={{ color: '#012068' }}>
|
||||
{featuredEvent.title}
|
||||
</h4>
|
||||
<div className="text-xs leading-relaxed mb-1" style={{ color: '#333' }}>
|
||||
{featuredEvent.description}
|
||||
</div>
|
||||
<div className="text-xs leading-relaxed mb-4" style={{ color: '#333' }}>
|
||||
{featuredEvent.detail}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<div
|
||||
className="text-xs cursor-pointer hover:underline"
|
||||
style={{ color: '#012068' }}
|
||||
>
|
||||
Share Event
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#012068' }}>
|
||||
📍 {featuredEvent.venue?.[0]?.address || 'Convention Center, Medical District'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left lg:text-right">
|
||||
<button
|
||||
className="w-full lg:w-auto px-6 py-2 text-sm rounded-lg mb-2 transition-colors hover:opacity-90"
|
||||
style={{
|
||||
backgroundColor: '#e64838',
|
||||
color: 'white'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Book seat clicked');
|
||||
}}
|
||||
>
|
||||
Book Your Seat
|
||||
</button>
|
||||
<div className="text-sm font-medium" style={{ color: '#e64838' }}>
|
||||
{formatPrice(featuredEvent)}
|
||||
</div>
|
||||
<div className="text-xs mt-1" style={{ color: '#012068', opacity: 0.8 }}>
|
||||
Early bird discount available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right - Event List with Scroller */}
|
||||
<div className="lg:col-span-1">
|
||||
<h3 className="text-xl font-semibold mb-4" style={{ color: '#012068' }}>Past Events</h3>
|
||||
{pastEvents.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">No past events.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-96 overflow-y-auto pr-2 space-y-4 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
|
||||
{pastEvents.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="bg-white border border-gray-100 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow flex-shrink-0"
|
||||
onClick={() => navigateToEventDetail(event.id)}
|
||||
>
|
||||
<div className="h-24">
|
||||
<img
|
||||
src={event.mainImage}
|
||||
alt={event.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="text-xs font-medium mb-1" style={{ color: '#e64838' }}>
|
||||
{event.date}
|
||||
</div>
|
||||
<h4 className="font-medium mb-1 text-sm" style={{ color: '#012068' }}>
|
||||
{event.title}
|
||||
</h4>
|
||||
<p className="text-xs leading-relaxed" style={{ color: '#333' }}>
|
||||
{event.description}
|
||||
</p>
|
||||
<div className="mt-2 flex justify-between items-center">
|
||||
<button
|
||||
className="text-xs hover:underline"
|
||||
style={{ color: '#012068' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateToEventDetail(event.id);
|
||||
}}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
<span className="text-xs font-medium" style={{ color: '#e64838' }}>
|
||||
{formatPrice(event)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventsSection;
|
||||
44
src/components/home/HeroSection.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
const HeroSection = () => {
|
||||
return (
|
||||
<div className="relative h-80 md:h-96 lg:h-[500px] min-h-80 md:min-h-96 lg:min-h-[450px] overflow-hidden">
|
||||
{/* Background Image using Next.js Image with fill */}
|
||||
<Image
|
||||
src="/images/hero.png"
|
||||
alt="CMC Vellore 125 years celebration"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="relative z-10 flex items-center justify-center text-left h-full px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 2xl:px-20 py-8 sm:py-12 md:py-16">
|
||||
<div className="w-full max-w-5xl">
|
||||
{/* Main Heading */}
|
||||
<h1 className="text-white mb-6 sm:mb-8 md:mb-10 text-left">
|
||||
<div className="text-lg sm:text-xl md:text-2xl lg:text-3xl xl:text-4xl 2xl:text-5xl font-bold leading-tight mb-1 sm:mb-2 md:mb-3 lg:mb-4">
|
||||
This year, we celebrate
|
||||
</div>
|
||||
<div className="text-lg sm:text-xl md:text-2xl lg:text-3xl xl:text-4xl 2xl:text-5xl font-bold leading-tight mb-1 sm:mb-2 md:mb-3 lg:mb-4">
|
||||
125 years of CMC Vellore
|
||||
</div>
|
||||
<div className="text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl 2xl:text-4xl font-semibold text-gray-200">
|
||||
1900 - 2025
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
{/* CTA Button */}
|
||||
<button
|
||||
className="bg-red-600 hover:bg-red-700 text-white px-6 py-3 sm:px-8 sm:py-4 md:px-10 md:py-4 text-sm sm:text-base md:text-lg font-semibold rounded-sm transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-red-300"
|
||||
style={{ backgroundColor: '#e64838' }}
|
||||
>
|
||||
Discover
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSection;
|
||||
198
src/components/research/ResearchComponent.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import { Calendar, Users, BookOpen, MapPin, Award, ChevronRight } from "lucide-react";
|
||||
import Link from 'next/link';
|
||||
|
||||
const ResearchComponent = () => {
|
||||
const ongoingProjects = [
|
||||
{
|
||||
icon: <BookOpen className="w-6 h-6" />,
|
||||
title: "Chest Trauma Outcomes",
|
||||
description: "Comprehensive analysis of mortality rates, ventilation requirements, and rib fixation effectiveness in chest trauma patients."
|
||||
},
|
||||
{
|
||||
icon: <Users className="w-6 h-6" />,
|
||||
title: "Rib Fixation Study",
|
||||
description: "Comparative study analyzing recovery outcomes between operative and non-operative treatment approaches for rib fractures."
|
||||
},
|
||||
{
|
||||
icon: <Calendar className="w-6 h-6" />,
|
||||
title: "Pre-hospital Time Study",
|
||||
description: "Investigating the impact of Golden Hour protocols versus standard transport timelines on patient outcomes."
|
||||
}
|
||||
];
|
||||
|
||||
const milestones = [
|
||||
{
|
||||
year: "2023",
|
||||
event: "ACTraM 2023",
|
||||
description: "Presented 3 comprehensive audits covering surgical intervention times, ICU stay duration, and patient outcome metrics",
|
||||
position: "left"
|
||||
},
|
||||
{
|
||||
year: "2022",
|
||||
event: "Q4 Resource Audit",
|
||||
description: "Completed comprehensive audit on trauma patient overstay patterns and healthcare resource utilization optimization",
|
||||
position: "right"
|
||||
}
|
||||
];
|
||||
|
||||
const collaborators = [
|
||||
{ name: "Royal College of Physicians & Surgeons", location: "Glasgow", short: "RCPS" },
|
||||
{ name: "All India Institute of Medical Sciences", location: "New Delhi", short: "AIIMS" },
|
||||
{ name: "Post Graduate Institute", location: "Chandigarh", short: "PGI" },
|
||||
{ name: "All India Institute of Medical Sciences", location: "Jodhpur", short: "AIIMS" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white min-h-screen">
|
||||
{/* Breadcrumb Section */}
|
||||
<section className="py-4" style={{ backgroundColor: '#f4f4f4' }}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<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' }}>
|
||||
Research
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#012068' }}>
|
||||
Research
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-base max-w-2xl leading-relaxed">
|
||||
Advancing trauma care through innovative research and evidence-based medical practices
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
{/* Ongoing Projects Section */}
|
||||
<div className="mb-20">
|
||||
<h2 className="text-3xl font-semibold mb-12 text-center" style={{color: '#012068'}}>
|
||||
Ongoing Projects
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" >
|
||||
{ongoingProjects.map((project, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg p-6 hover:shadow-lg transition-shadow duration-300"
|
||||
style={{backgroundColor:'#f4f4f4'}}
|
||||
>
|
||||
<div className="flex items-center mb-4">
|
||||
<div
|
||||
className="p-3 rounded-lg mr-4"
|
||||
style={{color: '#012068'}}
|
||||
>
|
||||
{project.icon}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold" style={{color: '#012068'}}>
|
||||
{project.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
{project.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Past Work Section */}
|
||||
<div className="mb-6 p-8"style={{backgroundColor:'#f4f4f4'}}>
|
||||
<h2 className="text-3xl font-semibold mb-4 text-center" style={{color: '#012068'}}>
|
||||
Past Work
|
||||
</h2>
|
||||
<div className="relative">
|
||||
{/* Timeline line */}
|
||||
<div
|
||||
className="absolute left-1/2 h-full w-0.5"
|
||||
style={{backgroundColor: '#012068'}}
|
||||
></div>
|
||||
|
||||
{milestones.map((milestone, index) => (
|
||||
<div key={index} className={`flex items-center mb-12 ${milestone.position === 'left' ? 'flex-row-reverse' : ''}`}>
|
||||
<div className={`w-1/2 ${milestone.position === 'left' ? 'pr-8 text-right' : 'pl-8'}`}>
|
||||
<div
|
||||
className="p-6 rounded-lg shadow-sm group"
|
||||
style={{backgroundColor: 'white', borderColor: '#f4f4f4'}}
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<span
|
||||
className="text-2xl font-bold mr-3 group-hover:scale-110 transition-transform duration-300"
|
||||
style={{color: '#e64838'}}
|
||||
>
|
||||
{milestone.year}
|
||||
</span>
|
||||
<Award className="w-5 h-5 group-hover:rotate-12 transition-transform duration-300" style={{color: '#012068'}} />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2 group-hover:text-opacity-80 transition-all duration-300" style={{color: '#012068'}}>
|
||||
{milestone.event}
|
||||
</h3>
|
||||
<p className="text-gray-700 group-hover:text-gray-600 transition-colors duration-300">
|
||||
{milestone.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline dot */}
|
||||
<div
|
||||
className="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 rounded-full border-4 border-white shadow-lg hover:scale-125 transition-transform duration-300"
|
||||
style={{backgroundColor: '#e64838'}}
|
||||
></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collaborators Section */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold mb-4 text-center" style={{color: '#012068'}}>
|
||||
Collaborators
|
||||
</h2>
|
||||
<p className="text-center text-gray-600 mb-12">
|
||||
Our research partnerships span leading medical institutions worldwide
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{collaborators.map((collaborator, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-center p-6 rounded-lg border hover:shadow-md transition-shadow duration-300"
|
||||
style={{backgroundColor: '#f4f4f4', borderColor: '#f4f4f4'}}
|
||||
>
|
||||
<div
|
||||
className="w-20 h-20 mx-auto mb-4 rounded-full flex items-center justify-center text-2xl font-bold"
|
||||
style={{backgroundColor: '#012068', color: 'white'}}
|
||||
>
|
||||
{collaborator.short.substring(0, 2)}
|
||||
</div>
|
||||
<h3 className="font-semibold mb-2" style={{color: '#012068'}}>
|
||||
{collaborator.short}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
{collaborator.name}
|
||||
</p>
|
||||
<div className="flex items-center justify-center text-sm text-gray-500">
|
||||
<MapPin className="w-4 h-4 mr-1" />
|
||||
{collaborator.location}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResearchComponent;
|
||||
162
src/lib/api.ts
Normal file
@ -0,0 +1,162 @@
|
||||
// lib/api.ts - API service for Next.js
|
||||
export interface Event {
|
||||
id: number;
|
||||
code: string;
|
||||
year: string;
|
||||
subject: string;
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
description: string;
|
||||
detail: string;
|
||||
date: string;
|
||||
mainImage?: string;
|
||||
galleryImages?: string[];
|
||||
venue?: Venue[];
|
||||
highlights?: string[];
|
||||
organisers?: string[];
|
||||
fee?: Fee[]; // Updated to match backend structure
|
||||
phone: string;
|
||||
email: string;
|
||||
isActive: boolean;
|
||||
professors?: Professor[];
|
||||
}
|
||||
|
||||
interface Venue {
|
||||
title: string;
|
||||
date: string;
|
||||
address: string;
|
||||
info: string;
|
||||
}
|
||||
|
||||
interface Fee {
|
||||
description: string; // Updated from 'desc' to 'description'
|
||||
cost: number;
|
||||
}
|
||||
|
||||
interface Professor {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
class EventAPI {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
// Use environment variable for API URL
|
||||
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
}
|
||||
|
||||
// File upload method
|
||||
async uploadImage(file: File): Promise<{url: string, filename: string}> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/api/files/upload`, {
|
||||
method: 'POST',
|
||||
body: formData, // Don't set Content-Type header for FormData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Upload multiple images
|
||||
async uploadMultipleImages(files: File[]): Promise<{url: string, filename: string}[]> {
|
||||
try {
|
||||
const uploadPromises = files.map(file => this.uploadImage(file));
|
||||
return await Promise.all(uploadPromises);
|
||||
} catch (error) {
|
||||
console.error('Error uploading multiple images:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAllEvents(): Promise<Event[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/events`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getEventById(id: number): Promise<Event> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/events/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching event:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getUpcomingEvents(): Promise<Event[]> {
|
||||
try {
|
||||
const events = await this.getAllEvents();
|
||||
const currentDate = new Date();
|
||||
|
||||
return events
|
||||
.filter(event => {
|
||||
if (!event.isActive) return false;
|
||||
|
||||
// Parse the date string (assuming format like "28 September 2025")
|
||||
const eventDate = new Date(event.date);
|
||||
return eventDate >= currentDate;
|
||||
})
|
||||
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
} catch (error) {
|
||||
console.error('Error fetching upcoming events:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getPastEvents(): Promise<Event[]> {
|
||||
try {
|
||||
const events = await this.getAllEvents();
|
||||
const currentDate = new Date();
|
||||
|
||||
return events
|
||||
.filter(event => {
|
||||
if (!event.isActive) return false;
|
||||
|
||||
const eventDate = new Date(event.date);
|
||||
return eventDate < currentDate;
|
||||
})
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
} catch (error) {
|
||||
console.error('Error fetching past events:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventAPI = new EventAPI();
|
||||
237
src/lib/facultyData.ts
Normal file
@ -0,0 +1,237 @@
|
||||
// lib/facultyData.ts
|
||||
|
||||
export interface TeamMember {
|
||||
id: number;
|
||||
professorId: string;
|
||||
name: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
position: string;
|
||||
designation: string;
|
||||
image: string;
|
||||
profileUrl?: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
experience: string;
|
||||
description: string;
|
||||
specialty: string;
|
||||
certification: string;
|
||||
training: string;
|
||||
workDays: string[];
|
||||
skills: Skill[];
|
||||
awards: Award[];
|
||||
status?: string;
|
||||
department?: string;
|
||||
officeLocation?: string;
|
||||
joinDate?: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface Skill {
|
||||
name: string;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export interface Award {
|
||||
title: string;
|
||||
year: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface FacultyApiResponse {
|
||||
content: TeamMember[];
|
||||
last: boolean;
|
||||
first: boolean;
|
||||
totalElements: number;
|
||||
size: number;
|
||||
numberOfElements: number;
|
||||
number: number;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
// API service class
|
||||
export class FacultyService {
|
||||
private static baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
static async getAllFaculty(): Promise<TeamMember[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/professor?size=100`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: FacultyApiResponse = await response.json();
|
||||
return this.transformProfessorsToTeamMembers(data.content);
|
||||
} catch (error) {
|
||||
console.error('Error fetching faculty data:', error);
|
||||
return this.getFallbackData();
|
||||
}
|
||||
}
|
||||
|
||||
static async getFacultyById(id: number): Promise<TeamMember | null> {
|
||||
try {
|
||||
// Find by array index from getAllFaculty for now
|
||||
// In production, you might want a direct API call
|
||||
const allFaculty = await this.getAllFaculty();
|
||||
return allFaculty.find(member => member.id === id) || null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching faculty member:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getFacultyByProfessorId(professorId: string): Promise<TeamMember | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/professor/${professorId}`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const professor = await response.json();
|
||||
const transformed = this.transformProfessorsToTeamMembers([professor]);
|
||||
return transformed[0] || null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching professor by ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getFacultyByCategory(category: string): Promise<TeamMember[]> {
|
||||
try {
|
||||
const allFaculty = await this.getAllFaculty();
|
||||
return allFaculty.filter(member => member.category === category);
|
||||
} catch (error) {
|
||||
console.error('Error fetching faculty by category:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to check if an image URL is accessible
|
||||
static async checkImageExists(imageUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(imageUrl, { method: 'HEAD' });
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get a working image URL with fallback
|
||||
static async getWorkingImageUrl(originalUrl: string, fallbackUrl: string = '/images/default-avatar.jpg'): Promise<string> {
|
||||
const isAccessible = await this.checkImageExists(originalUrl);
|
||||
return isAccessible ? originalUrl : fallbackUrl;
|
||||
}
|
||||
|
||||
private static transformProfessorsToTeamMembers(professors: any[]): TeamMember[] {
|
||||
return professors.map((prof, index) => {
|
||||
// Create proper image URL - remove /api from baseUrl for images
|
||||
const imageBaseUrl = this.baseUrl;
|
||||
let imageUrl = '/images/default-avatar.jpg'; // Default fallback
|
||||
|
||||
if (prof.profileImageUrl) {
|
||||
// If it's already a full URL, use it as is
|
||||
if (prof.profileImageUrl.startsWith('http')) {
|
||||
imageUrl = prof.profileImageUrl;
|
||||
} else {
|
||||
// If it's a relative URL, construct the full URL
|
||||
imageUrl = `${imageBaseUrl}${prof.profileImageUrl}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: index + 1, // Generate sequential ID for UI
|
||||
professorId: prof.professorId,
|
||||
name: prof.name || `${prof.firstName} ${prof.lastName}`,
|
||||
firstName: prof.firstName,
|
||||
lastName: prof.lastName,
|
||||
position: prof.position || 'Faculty Member',
|
||||
designation: prof.designation || prof.position || 'Faculty Member',
|
||||
image: imageUrl,
|
||||
profileUrl: `/faculty/${index + 1}`,
|
||||
phone: prof.phone || 'Not available',
|
||||
email: prof.email,
|
||||
experience: prof.experience || 'Not specified',
|
||||
description: prof.description || `${prof.firstName} ${prof.lastName} is a dedicated member of our faculty.`,
|
||||
specialty: prof.specialty || prof.department || 'General Medicine',
|
||||
certification: prof.certification || 'Medical certification details not available',
|
||||
training: prof.training || 'Professional training details not available',
|
||||
workDays: prof.workDays || ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
skills: prof.skills?.map((skill: any) => ({
|
||||
name: skill.name,
|
||||
level: skill.level
|
||||
})) || [
|
||||
{ name: 'Clinical Practice', level: 90 },
|
||||
{ name: 'Research', level: 85 },
|
||||
{ name: 'Teaching', level: 88 }
|
||||
],
|
||||
awards: prof.awards?.map((award: any) => ({
|
||||
title: award.title,
|
||||
year: award.year,
|
||||
description: award.description,
|
||||
image: award.imageUrl || '/images/award-icon.png'
|
||||
})) || [
|
||||
{
|
||||
title: 'Excellence in Medical Practice',
|
||||
year: new Date().getFullYear().toString(),
|
||||
description: 'Recognized for outstanding contribution to medical practice and patient care.',
|
||||
image: '/images/award-icon.png'
|
||||
}
|
||||
],
|
||||
status: prof.status,
|
||||
department: prof.department,
|
||||
officeLocation: prof.officeLocation,
|
||||
joinDate: prof.joinDate,
|
||||
category: prof.category || 'FACULTY'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static getFallbackData(): TeamMember[] {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
professorId: 'fallback-1',
|
||||
name: "Loading Faculty Data...",
|
||||
firstName: "Loading",
|
||||
lastName: "Data",
|
||||
position: "Please wait while we fetch the latest faculty information",
|
||||
designation: "System Message",
|
||||
image: "/images/default-avatar.jpg",
|
||||
phone: "Not available",
|
||||
email: "support@institution.edu",
|
||||
experience: "N/A",
|
||||
description: "Faculty data is currently being loaded from the server.",
|
||||
specialty: "N/A",
|
||||
certification: "N/A",
|
||||
training: "N/A",
|
||||
workDays: [],
|
||||
skills: [],
|
||||
awards: [],
|
||||
category: 'FACULTY'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get team members (for backward compatibility)
|
||||
export const getTeamMembers = async (): Promise<TeamMember[]> => {
|
||||
return await FacultyService.getAllFaculty();
|
||||
};
|
||||
|
||||
// Helper function to get team member by ID
|
||||
export const getTeamMemberById = async (id: number): Promise<TeamMember | null> => {
|
||||
return await FacultyService.getFacultyById(id);
|
||||
};
|
||||
256
src/services/blogService.ts
Normal file
@ -0,0 +1,256 @@
|
||||
// services/blogService.ts
|
||||
export interface Professor {
|
||||
id: number;
|
||||
firstName?: string;
|
||||
name?: string; // Fallback for different naming
|
||||
}
|
||||
|
||||
export interface ApiBlog {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
professors: Professor[];
|
||||
tags: string[];
|
||||
posted: boolean; // From your Angular form
|
||||
author?: string;
|
||||
createdDate?: string;
|
||||
updatedDate?: string;
|
||||
publishDate?: string;
|
||||
imageUrl?: string; // Added for uploaded images
|
||||
}
|
||||
|
||||
export interface Blog {
|
||||
id: number;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
tags: string[];
|
||||
image: string;
|
||||
publishDate: string;
|
||||
readTime: string;
|
||||
content?: string;
|
||||
professors?: Professor[];
|
||||
}
|
||||
|
||||
class BlogService {
|
||||
private apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
async getAllBlogs(): Promise<Blog[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/posts`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiBlogs: ApiBlog[] = await response.json();
|
||||
return this.transformApiBlogsToBlogs(apiBlogs);
|
||||
} catch (error) {
|
||||
console.error('Error fetching blogs:', error);
|
||||
return this.getFallbackBlogs(); // Return fallback data if API fails
|
||||
}
|
||||
}
|
||||
|
||||
async getBlogById(id: number): Promise<Blog | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/posts/${id}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiBlog: ApiBlog = await response.json();
|
||||
return this.transformApiBlogToBlog(apiBlog);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching blog ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get only posted blogs for public display
|
||||
async getPostedBlogs(): Promise<Blog[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/posts/posted`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiBlogs: ApiBlog[] = await response.json();
|
||||
return this.transformApiBlogsToBlogs(apiBlogs);
|
||||
} catch (error) {
|
||||
console.error('Error fetching posted blogs:', error);
|
||||
return this.getFallbackBlogs();
|
||||
}
|
||||
}
|
||||
|
||||
// Get blogs by tag
|
||||
async getBlogsByTag(tag: string): Promise<Blog[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/posts/tag/${encodeURIComponent(tag)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiBlogs: ApiBlog[] = await response.json();
|
||||
return this.transformApiBlogsToBlogs(apiBlogs);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching blogs by tag ${tag}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get tag counts
|
||||
async getTagsWithCount(): Promise<{ [key: string]: number }> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/posts/tags/count`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const tagCounts: { [key: string]: number } = await response.json();
|
||||
return tagCounts;
|
||||
} catch (error) {
|
||||
console.error('Error fetching tag counts:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private transformApiBlogsToBlogs(apiBlogs: ApiBlog[]): Blog[] {
|
||||
return apiBlogs.map(apiBlog => this.transformApiBlogToBlog(apiBlog));
|
||||
}
|
||||
|
||||
private transformApiBlogToBlog(apiBlog: ApiBlog): Blog {
|
||||
return {
|
||||
id: apiBlog.id,
|
||||
title: apiBlog.title,
|
||||
excerpt: this.generateExcerpt(apiBlog.content),
|
||||
tags: apiBlog.tags || [],
|
||||
image: this.getImageUrl(apiBlog.imageUrl), // Use uploaded image or default
|
||||
publishDate: this.formatDate(apiBlog.publishDate || apiBlog.createdDate || new Date().toISOString()),
|
||||
readTime: this.calculateReadTime(apiBlog.content),
|
||||
content: apiBlog.content,
|
||||
professors: apiBlog.professors
|
||||
};
|
||||
}
|
||||
|
||||
private generateExcerpt(content: string, maxLength: number = 150): string {
|
||||
if (!content) return 'No content available.';
|
||||
|
||||
// Strip HTML tags and get plain text
|
||||
const plainText = content.replace(/<[^>]*>/g, '');
|
||||
|
||||
if (plainText.length <= maxLength) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
// Find the last complete word within the limit
|
||||
const truncated = plainText.substr(0, maxLength);
|
||||
const lastSpaceIndex = truncated.lastIndexOf(' ');
|
||||
|
||||
if (lastSpaceIndex > 0) {
|
||||
return truncated.substr(0, lastSpaceIndex) + '...';
|
||||
}
|
||||
|
||||
return truncated + '...';
|
||||
}
|
||||
|
||||
private calculateReadTime(content: string): string {
|
||||
if (!content) return '1 min read';
|
||||
|
||||
// Strip HTML and count words
|
||||
const plainText = content.replace(/<[^>]*>/g, '');
|
||||
const wordCount = plainText.split(/\s+/).filter(word => word.length > 0).length;
|
||||
|
||||
// Average reading speed is 200-250 words per minute
|
||||
const readingSpeed = 225;
|
||||
const minutes = Math.ceil(wordCount / readingSpeed);
|
||||
|
||||
return `${minutes} min read`;
|
||||
}
|
||||
|
||||
private formatDate(dateString: string): string {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split('T')[0]; // Returns YYYY-MM-DD format
|
||||
} catch (error) {
|
||||
return new Date().toISOString().split('T')[0]; // Fallback to current date
|
||||
}
|
||||
}
|
||||
|
||||
private getImageUrl(imageUrl?: string): string {
|
||||
if (imageUrl) {
|
||||
// If the imageUrl is a full URL, return as is
|
||||
if (imageUrl.startsWith('http')) {
|
||||
return imageUrl;
|
||||
}
|
||||
// If it's a relative path from your backend, construct full URL
|
||||
return `${this.apiBaseUrl}${imageUrl}`;
|
||||
}
|
||||
|
||||
// Return default image when no image is uploaded
|
||||
return this.getDefaultImage();
|
||||
}
|
||||
|
||||
private getDefaultImage(): string {
|
||||
// Return a single default image from your public folder
|
||||
// Make sure to add this image to your Next.js public/images directory
|
||||
return '/images/default-blog-image.jpg';
|
||||
}
|
||||
|
||||
private getFallbackBlogs(): Blog[] {
|
||||
// Return the original hardcoded blogs as fallback when API is unavailable
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
title: "Understanding PTSD: Signs, Symptoms, and Treatment Options",
|
||||
excerpt: "Post-traumatic stress disorder affects millions worldwide. Learn about the key symptoms, triggers, and evidence-based treatment approaches that can help individuals reclaim their lives.",
|
||||
tags: ["PTSD", "Mental Health", "Treatment"],
|
||||
image: "/images/default-blog-image.jpg", // Use default image for fallback
|
||||
publishDate: "2024-01-15",
|
||||
readTime: "8 min read"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Building Resilience After Childhood Trauma",
|
||||
excerpt: "Discover practical strategies and therapeutic approaches for healing from childhood trauma. Explore how resilience can be developed and nurtured throughout the recovery journey.",
|
||||
tags: ["Childhood Trauma", "Resilience", "Healing"],
|
||||
image: "/images/default-blog-image.jpg",
|
||||
publishDate: "2024-01-12",
|
||||
readTime: "6 min read"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "The Role of Family Support in Trauma Recovery",
|
||||
excerpt: "Family support plays a crucial role in trauma recovery. Learn how loved ones can provide effective support and create a healing environment for trauma survivors.",
|
||||
tags: ["Family Support", "Recovery", "Relationships"],
|
||||
image: "/images/default-blog-image.jpg",
|
||||
publishDate: "2024-01-10",
|
||||
readTime: "5 min read"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Trauma-Informed Care: A Comprehensive Approach",
|
||||
excerpt: "Explore the principles of trauma-informed care and how healthcare providers can create safe, supportive environments for trauma survivors seeking treatment.",
|
||||
tags: ["Trauma-Informed Care", "Healthcare", "Best Practices"],
|
||||
image: "/images/default-blog-image.jpg",
|
||||
publishDate: "2024-01-08",
|
||||
readTime: "7 min read"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Mindfulness and Meditation for Trauma Healing",
|
||||
excerpt: "Discover how mindfulness practices and meditation techniques can be powerful tools in trauma recovery, helping to regulate emotions and reduce anxiety.",
|
||||
tags: ["Mindfulness", "Meditation", "Coping Strategies"],
|
||||
image: "/images/default-blog-image.jpg",
|
||||
publishDate: "2024-01-05",
|
||||
readTime: "6 min read"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Workplace Trauma: Recognition and Response",
|
||||
excerpt: "Understanding workplace trauma and its impact on employees. Learn about creating supportive work environments and implementing effective response strategies.",
|
||||
tags: ["Workplace Trauma", "Employee Support", "Mental Health"],
|
||||
image: "/images/default-blog-image.jpg",
|
||||
publishDate: "2024-01-03",
|
||||
readTime: "9 min read"
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const blogService = new BlogService();
|
||||
206
src/services/careerService.ts
Normal file
@ -0,0 +1,206 @@
|
||||
// services/careerService.ts
|
||||
export interface ApiJob {
|
||||
id: number;
|
||||
title: string;
|
||||
department: string;
|
||||
location: string;
|
||||
type: string;
|
||||
experience: string;
|
||||
salary: string;
|
||||
description: string;
|
||||
requirements: string[];
|
||||
responsibilities: string[];
|
||||
isActive: boolean;
|
||||
createdDate?: string;
|
||||
updatedDate?: string;
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: string;
|
||||
title: string;
|
||||
department: string;
|
||||
location: string;
|
||||
type: string;
|
||||
experience: string;
|
||||
salary: string;
|
||||
description: string;
|
||||
requirements: string[];
|
||||
responsibilities: string[];
|
||||
}
|
||||
|
||||
export interface JobApplicationData {
|
||||
jobId: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
experience: string;
|
||||
coverLetter?: string;
|
||||
resumeUrl?: string;
|
||||
}
|
||||
|
||||
class CareerService {
|
||||
private apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
async getActiveJobs(): Promise<Job[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/jobs/active`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiJobs: ApiJob[] = await response.json();
|
||||
return this.transformApiJobsToJobs(apiJobs);
|
||||
} catch (error) {
|
||||
console.error('Error fetching jobs:', error);
|
||||
return this.getFallbackJobs(); // Return fallback data if API fails
|
||||
}
|
||||
}
|
||||
|
||||
async getJobById(id: number): Promise<Job | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/jobs/${id}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiJob: ApiJob = await response.json();
|
||||
return this.transformApiJobToJob(apiJob);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching job ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async submitApplication(applicationData: JobApplicationData): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/job-applications`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(applicationData),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error submitting application:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private transformApiJobsToJobs(apiJobs: ApiJob[]): Job[] {
|
||||
return apiJobs.map(apiJob => this.transformApiJobToJob(apiJob));
|
||||
}
|
||||
|
||||
private transformApiJobToJob(apiJob: ApiJob): Job {
|
||||
return {
|
||||
id: apiJob.id.toString(),
|
||||
title: apiJob.title,
|
||||
department: apiJob.department,
|
||||
location: apiJob.location,
|
||||
type: apiJob.type,
|
||||
experience: apiJob.experience,
|
||||
salary: apiJob.salary,
|
||||
description: apiJob.description,
|
||||
requirements: apiJob.requirements || [],
|
||||
responsibilities: apiJob.responsibilities || []
|
||||
};
|
||||
}
|
||||
|
||||
private getFallbackJobs(): Job[] {
|
||||
// Return the original hardcoded jobs as fallback
|
||||
return [
|
||||
{
|
||||
id: 'trauma-registrar',
|
||||
title: 'Trauma Registrar',
|
||||
department: 'Trauma Surgery',
|
||||
location: 'Vellore, India',
|
||||
type: 'Rotational',
|
||||
experience: 'MBBS + MS preferred',
|
||||
salary: 'As per hospital norms',
|
||||
description: 'Join our trauma surgery department as a registrar. Open year-round position with rotational duties in emergency trauma care.',
|
||||
requirements: [
|
||||
'MBBS degree required',
|
||||
'MS qualification preferred',
|
||||
'Experience in emergency medicine',
|
||||
'Ability to work in high-pressure environments'
|
||||
],
|
||||
responsibilities: [
|
||||
'Manage trauma cases in emergency situations',
|
||||
'Assist in trauma surgeries',
|
||||
'Participate in rotational duties',
|
||||
'Maintain patient records and documentation'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'trauma-nurse-coordinator',
|
||||
title: 'Trauma Nurse Coordinator',
|
||||
department: 'Trauma Surgery',
|
||||
location: 'Vellore, India',
|
||||
type: 'Full-time',
|
||||
experience: 'BSc Nursing + ATCN or 2 years ICU',
|
||||
salary: 'As per hospital norms',
|
||||
description: 'Coordinate trauma nursing activities and ensure quality patient care in our trauma unit.',
|
||||
requirements: [
|
||||
'BSc Nursing degree required',
|
||||
'ATCN certification OR 2 years ICU experience',
|
||||
'Strong coordination and leadership skills',
|
||||
'Knowledge of trauma protocols'
|
||||
],
|
||||
responsibilities: [
|
||||
'Coordinate trauma nursing activities',
|
||||
'Ensure quality patient care standards',
|
||||
'Train and supervise nursing staff',
|
||||
'Maintain trauma unit protocols'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'clinical-research-fellow',
|
||||
title: 'Clinical Research Fellow',
|
||||
department: 'Trauma Research',
|
||||
location: 'Vellore, India',
|
||||
type: 'Contract (1 year)',
|
||||
experience: 'Research background preferred',
|
||||
salary: 'Fellowship stipend',
|
||||
description: '1-year contract position for trauma study work. Ideal for those interested in clinical research in trauma medicine.',
|
||||
requirements: [
|
||||
'Medical degree or related field',
|
||||
'Interest in clinical research',
|
||||
'Data analysis skills',
|
||||
'Good written and verbal communication'
|
||||
],
|
||||
responsibilities: [
|
||||
'Conduct trauma-related clinical studies',
|
||||
'Collect and analyze research data',
|
||||
'Prepare research reports and publications',
|
||||
'Collaborate with research team'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'short-term-observer',
|
||||
title: 'Short-Term Observer',
|
||||
department: 'Trauma Surgery',
|
||||
location: 'Vellore, India',
|
||||
type: 'Observership (4-8 weeks)',
|
||||
experience: 'Junior residents',
|
||||
salary: 'Observership program',
|
||||
description: '4-8 week observership slots for junior residents. Apply 2 months ahead for this learning opportunity.',
|
||||
requirements: [
|
||||
'Junior resident status',
|
||||
'Interest in trauma surgery',
|
||||
'Apply 2 months in advance',
|
||||
'Valid medical credentials'
|
||||
],
|
||||
responsibilities: [
|
||||
'Observe trauma surgery procedures',
|
||||
'Learn trauma management protocols',
|
||||
'Attend clinical rounds and discussions',
|
||||
'Prepare observership reports'
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const careerService = new CareerService();
|
||||
254
src/services/educationService.ts
Normal file
@ -0,0 +1,254 @@
|
||||
// services/educationService.ts
|
||||
export interface ApiCourse {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
duration: string;
|
||||
seats: number;
|
||||
category: string;
|
||||
level: string;
|
||||
instructor: string;
|
||||
price?: string;
|
||||
startDate?: string;
|
||||
imageUrl?: string;
|
||||
eligibility: string[];
|
||||
objectives: string[];
|
||||
isActive: boolean;
|
||||
createdDate?: string;
|
||||
updatedDate?: string;
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
duration: string;
|
||||
seats: number;
|
||||
category: string;
|
||||
level: string;
|
||||
instructor: string;
|
||||
price: string;
|
||||
startDate: string;
|
||||
image: string;
|
||||
eligibility: string[];
|
||||
objectives: string[];
|
||||
}
|
||||
|
||||
export interface CourseApplicationData {
|
||||
courseId: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
qualification: string;
|
||||
experience?: string;
|
||||
coverLetter?: string;
|
||||
resumeUrl?: string;
|
||||
}
|
||||
|
||||
class EducationService {
|
||||
private apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
async getActiveCourses(): Promise<Course[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/courses/active`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiCourses: ApiCourse[] = await response.json();
|
||||
return this.transformApiCoursesToCourses(apiCourses);
|
||||
} catch (error) {
|
||||
console.error('Error fetching courses:', error);
|
||||
return this.getFallbackCourses(); // Return fallback data if API fails
|
||||
}
|
||||
}
|
||||
|
||||
async getCourseById(id: number): Promise<Course | null> {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken'); // Or from cookies/session
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/courses/${id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) return null;
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const apiCourse: ApiCourse = await response.json();
|
||||
return this.transformApiCourseToCourse(apiCourse);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching course ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async submitApplication(applicationData: CourseApplicationData): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/course-applications`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(applicationData),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error submitting application:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private transformApiCoursesToCourses(apiCourses: ApiCourse[]): Course[] {
|
||||
return apiCourses.map(apiCourse => this.transformApiCourseToCourse(apiCourse));
|
||||
}
|
||||
|
||||
private transformApiCourseToCourse(apiCourse: ApiCourse): Course {
|
||||
return {
|
||||
id: apiCourse.id.toString(),
|
||||
title: apiCourse.title,
|
||||
description: apiCourse.description,
|
||||
duration: apiCourse.duration,
|
||||
seats: apiCourse.seats,
|
||||
category: apiCourse.category,
|
||||
level: apiCourse.level,
|
||||
instructor: apiCourse.instructor,
|
||||
price: apiCourse.price || 'N/A',
|
||||
startDate: apiCourse.startDate || '',
|
||||
image: this.getImageUrl(apiCourse.imageUrl),
|
||||
eligibility: apiCourse.eligibility || [],
|
||||
objectives: apiCourse.objectives || []
|
||||
};
|
||||
}
|
||||
|
||||
private getImageUrl(imageUrl?: string): string {
|
||||
if (imageUrl) {
|
||||
// If imageUrl starts with /uploads/, prepend the full API path
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
return `${this.apiBaseUrl}/api/files${imageUrl}`; // This adds /api/files before /uploads/
|
||||
}
|
||||
// If it's already a full URL, return as is
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
return imageUrl;
|
||||
}
|
||||
// Otherwise, assume it's a relative path and prepend base URL with API path
|
||||
return `${this.apiBaseUrl}/api/files/${imageUrl}`;
|
||||
}
|
||||
// Return random default image if no imageUrl provided
|
||||
return this.getRandomDefaultImage();
|
||||
}
|
||||
|
||||
private getRandomDefaultImage(): string {
|
||||
const defaultImages = [
|
||||
"https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
|
||||
"https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=300&fit=crop&crop=center",
|
||||
"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=300&fit=crop&crop=center",
|
||||
"https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=400&h=300&fit=crop&crop=center",
|
||||
"https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=400&h=300&fit=crop&crop=center",
|
||||
"https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&crop=center"
|
||||
];
|
||||
return defaultImages[Math.floor(Math.random() * defaultImages.length)];
|
||||
}
|
||||
|
||||
private getFallbackCourses(): Course[] {
|
||||
// Return the original hardcoded courses as fallback
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
title: "ATLS® (Advanced Trauma Life Support)",
|
||||
description: "Eligibility: MBBS + internship complete. Last Course: Aug 31 – Sep 2, 2023 (60 doctors certified). Next Schedule: [#Incomplete – Date TBD]",
|
||||
duration: "3 Days",
|
||||
seats: 60,
|
||||
category: "Certification",
|
||||
level: "Professional",
|
||||
image: "https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop&crop=center",
|
||||
instructor: "Trauma Faculty Team",
|
||||
price: "N/A",
|
||||
startDate: "2023-08-31",
|
||||
eligibility: ["MBBS + internship complete"],
|
||||
objectives: ["Advanced trauma life support skills", "Emergency trauma management"]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: "ATCN® (Advanced Trauma Care for Nurses)",
|
||||
description: "First Course: Apr 11-13, 2024 (manikin-based training). Participants: 40 critical care nurses from CMC and partner hospitals. Next Batch: [#Incomplete – Date TBD]",
|
||||
duration: "3 Days",
|
||||
seats: 40,
|
||||
category: "Training",
|
||||
level: "Professional",
|
||||
image: "https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=300&fit=crop&crop=center",
|
||||
instructor: "Nursing Faculty Team",
|
||||
price: "N/A",
|
||||
startDate: "2024-04-11",
|
||||
eligibility: ["Registered Nurse", "Critical care experience preferred"],
|
||||
objectives: ["Advanced trauma nursing skills", "Manikin-based training proficiency"]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: "Trauma First Responder Program",
|
||||
description: "Partners: RCPSG Hope Foundation, local colleges. Locations: Walajapet, Auxilium College—250 students trained. Curriculum: CPR, airway support, bleeding control, scene assessment.",
|
||||
duration: "Varies",
|
||||
seats: 250,
|
||||
category: "Workshop",
|
||||
level: "Beginner",
|
||||
image: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=300&fit=crop&crop=center",
|
||||
instructor: "Community Trainers",
|
||||
price: "N/A",
|
||||
startDate: "2023-01-01",
|
||||
eligibility: ["Students", "Community members"],
|
||||
objectives: ["CPR proficiency", "Basic trauma response", "Scene safety assessment"]
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: "FNB in Trauma Surgery",
|
||||
description: "3-year structured training program in acute surgery, ICU management, and research. Open to MS-qualified surgeons seeking specialized trauma surgery expertise.",
|
||||
duration: "3 Years",
|
||||
seats: 8,
|
||||
category: "Certification",
|
||||
level: "Advanced",
|
||||
image: "https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=400&h=300&fit=crop&crop=center",
|
||||
instructor: "Senior Trauma Surgeons",
|
||||
price: "N/A",
|
||||
startDate: "2025-07-01",
|
||||
eligibility: ["MS qualification in Surgery", "Valid medical license"],
|
||||
objectives: ["Advanced trauma surgery skills", "ICU management", "Research methodology"]
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: "Observerships & Electives",
|
||||
description: "4-8 week clinical blocks for national and international residents. Includes ATLS® course access and hands-on trauma experience. Application by email required.",
|
||||
duration: "4-8 Weeks",
|
||||
seats: 20,
|
||||
category: "Training",
|
||||
level: "Intermediate",
|
||||
image: "https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=400&h=300&fit=crop&crop=center",
|
||||
instructor: "Clinical Faculty",
|
||||
price: "N/A",
|
||||
startDate: "2025-01-15",
|
||||
eligibility: ["Medical residency status", "Valid medical credentials"],
|
||||
objectives: ["Clinical observation skills", "Hands-on trauma experience", "ATLS certification"]
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: "Nursing Skills Lab",
|
||||
description: "Trauma-focused skills laboratory sessions open quarterly (Q2, Q4). Includes chest tube insertion, airway management drills, and EFAST simulation training.",
|
||||
duration: "2 Days",
|
||||
seats: 30,
|
||||
category: "Workshop",
|
||||
level: "Intermediate",
|
||||
image: "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&crop=center",
|
||||
instructor: "Nursing Skills Faculty",
|
||||
price: "N/A",
|
||||
startDate: "2025-04-01",
|
||||
eligibility: ["Licensed nurse", "Basic trauma knowledge"],
|
||||
objectives: ["Chest tube insertion", "Airway management", "EFAST simulation"]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const educationService = new EducationService();
|
||||
160
src/services/eventService.ts
Normal file
@ -0,0 +1,160 @@
|
||||
// services/eventService.ts
|
||||
export interface Event {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
subtitle: string;
|
||||
mainImage: string;
|
||||
galleryImages: string[];
|
||||
price?: number;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface ApiEvent {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
subtitle?: string;
|
||||
imageUrl?: string;
|
||||
galleryImages?: string[];
|
||||
price?: number;
|
||||
location?: string;
|
||||
// Add other fields that your Spring Boot API returns
|
||||
}
|
||||
|
||||
class EventService {
|
||||
private apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
async getAllEvents(): Promise<Event[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/events`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiEvents: ApiEvent[] = await response.json();
|
||||
return this.transformApiEventsToEvents(apiEvents);
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error);
|
||||
return this.getFallbackEvents(); // Return fallback data if API fails
|
||||
}
|
||||
}
|
||||
|
||||
async getEventById(id: number): Promise<Event | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/events/${id}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiEvent: ApiEvent = await response.json();
|
||||
return this.transformApiEventToEvent(apiEvent);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching event ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private transformApiEventsToEvents(apiEvents: ApiEvent[]): Event[] {
|
||||
return apiEvents.map(apiEvent => this.transformApiEventToEvent(apiEvent));
|
||||
}
|
||||
|
||||
private transformApiEventToEvent(apiEvent: ApiEvent): Event {
|
||||
return {
|
||||
id: apiEvent.id,
|
||||
title: apiEvent.title,
|
||||
description: apiEvent.description,
|
||||
date: this.formatDate(apiEvent.date),
|
||||
subtitle: apiEvent.subtitle || 'More details coming soon',
|
||||
mainImage: apiEvent.imageUrl || this.getDefaultMainImage(),
|
||||
galleryImages: apiEvent.galleryImages || this.getDefaultGalleryImages(),
|
||||
price: apiEvent.price,
|
||||
location: apiEvent.location
|
||||
};
|
||||
}
|
||||
|
||||
private formatDate(dateString: string): string {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString; // Return original if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultMainImage(): string {
|
||||
const defaultImages = [
|
||||
'https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=400&h=200&fit=crop'
|
||||
];
|
||||
return defaultImages[Math.floor(Math.random() * defaultImages.length)];
|
||||
}
|
||||
|
||||
private getDefaultGalleryImages(): string[] {
|
||||
return [
|
||||
'https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1638202993928-7267aad84c31?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=200&h=100&fit=crop'
|
||||
];
|
||||
}
|
||||
|
||||
private getFallbackEvents(): Event[] {
|
||||
// Return the original hardcoded events as fallback
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
date: '28 September 2025',
|
||||
title: 'Advanced Cardiac Surgery Symposium',
|
||||
description: 'Cutting-edge techniques in minimally invasive cardiac procedures',
|
||||
subtitle: 'Learn from world-renowned cardiac surgeons about the latest innovations',
|
||||
mainImage: 'https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=200&fit=crop',
|
||||
galleryImages: [
|
||||
'https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1638202993928-7267aad84c31?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1551601651-2a8555f1a136?w=200&h=100&fit=crop'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '2 October 2025',
|
||||
title: 'Pediatric Immunization Update Conference',
|
||||
description: 'Latest developments in childhood vaccination protocols',
|
||||
subtitle: 'Comprehensive review of new vaccine guidelines and safety data',
|
||||
mainImage: 'https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=400&h=200&fit=crop',
|
||||
galleryImages: [
|
||||
'https://images.unsplash.com/photo-1584362917165-526a968579e8?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1581056771107-24ca5f033842?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=200&h=100&fit=crop'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: '8 October 2025',
|
||||
title: 'Mental Health in Healthcare Workers',
|
||||
description: 'Addressing burnout and psychological well-being in medical practice',
|
||||
subtitle: 'Strategies for maintaining mental health in high-pressure environments',
|
||||
mainImage: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=200&fit=crop',
|
||||
galleryImages: [
|
||||
'https://images.unsplash.com/photo-1582719508461-905c673771fd?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=200&h=100&fit=crop',
|
||||
'https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=200&h=100&fit=crop'
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const eventService = new EventService();
|
||||
34
src/services/fileUploadService.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// services/fileUploadService.ts
|
||||
export interface FileUploadResponse {
|
||||
url: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
class FileUploadService {
|
||||
private apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
async uploadFile(file: File): Promise<FileUploadResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/files/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result: FileUploadResponse = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('File upload error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const fileUploadService = new FileUploadService();
|
||||
72
src/services/upcomingEventsService.ts
Normal file
@ -0,0 +1,72 @@
|
||||
// services/upcomingEventsService.ts
|
||||
export interface ApiUpcomingEvent {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
schedule: string;
|
||||
eventDate?: string;
|
||||
isActive: boolean;
|
||||
createdDate?: string;
|
||||
updatedDate?: string;
|
||||
}
|
||||
|
||||
export interface UpcomingEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
schedule: string;
|
||||
eventDate?: string;
|
||||
}
|
||||
|
||||
class UpcomingEventsService {
|
||||
private apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
async getActiveUpcomingEvents(): Promise<UpcomingEvent[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/api/upcoming-events/active`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiEvents: ApiUpcomingEvent[] = await response.json();
|
||||
return this.transformApiEventsToEvents(apiEvents);
|
||||
} catch (error) {
|
||||
console.error('Error fetching upcoming events:', error);
|
||||
return this.getFallbackEvents();
|
||||
}
|
||||
}
|
||||
|
||||
private transformApiEventsToEvents(apiEvents: ApiUpcomingEvent[]): UpcomingEvent[] {
|
||||
return apiEvents.map(apiEvent => ({
|
||||
id: apiEvent.id.toString(),
|
||||
title: apiEvent.title,
|
||||
description: apiEvent.description,
|
||||
schedule: apiEvent.schedule,
|
||||
eventDate: apiEvent.eventDate
|
||||
}));
|
||||
}
|
||||
|
||||
private getFallbackEvents(): UpcomingEvent[] {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Simulation-based Team Drills',
|
||||
description: 'Hands-on simulation training designed to improve team coordination and emergency response in high-pressure trauma situations.',
|
||||
schedule: 'Q3 2025'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Online Webinar Series',
|
||||
description: 'Monthly online sessions covering trauma ethics, young doctor support, and professional development in emergency medicine.',
|
||||
schedule: 'Monthly Sessions'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Community Education',
|
||||
description: 'Road safety fairs and school education sessions to promote trauma prevention and basic first aid awareness in the community.',
|
||||
schedule: 'Ongoing'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const upcomingEventsService = new UpcomingEventsService();
|
||||
27
tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||