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"]
|
||||||
|
}
|
||||||