Initial commit of project without large files
This commit is contained in:
3
.env
Normal file
3
.env
Normal file
@@ -0,0 +1,3 @@
|
||||
REACT_APP_API_BASE_URL=http://localhost:8000
|
||||
HOST=0.0.0.0
|
||||
|
||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
build/static/media/* filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
build/static/js/*.js filter=lfs diff=lfs merge=lfs -text
|
||||
build/static/css/*.css filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
70
README.md
Normal file
70
README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
19803
package-lock.json
generated
Normal file
19803
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
67
package.json
Normal file
67
package.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "lcepl-website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "",
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.11.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"helmet": "^8.1.0",
|
||||
"morgan": "^1.10.1",
|
||||
"react": "^18.2.0",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-intersection-observer": "^10.0.0",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"react-slick": "^0.31.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"web-vitals": "^2.1.4",
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"server": "node server.js",
|
||||
"server:dev": "nodemon server.js",
|
||||
"dev": "concurrently -k -n server,client -p \"[{name}]\" \"npm:server:dev\" \"npm:start\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"concurrently": "*",
|
||||
"nodemon": "*",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
3
public/gallery/cert1.jpg
Normal file
3
public/gallery/cert1.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6151d682778ae9a8794065adc611381fda940841aacbc1aa9aeb6d5dea51ffea
|
||||
size 771090
|
||||
3
public/gallery/cert2.jpg
Normal file
3
public/gallery/cert2.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c498106d312c3e6df6b7cee5575dc4c535bc6f2c965dc7338abf93fc16863125
|
||||
size 945980
|
||||
3
public/gallery/cert3.jpg
Normal file
3
public/gallery/cert3.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aebbabebc80ebf16465b20f24b3ce88153c6c9d8fbb0b042cb492d2ad959a185
|
||||
size 387688
|
||||
3
public/gallery/cert4.jpg
Normal file
3
public/gallery/cert4.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c6756c2a1f395f71f0bea7b12fec08f0e6c39a74e4ce41beac8e40bd32cdd71d
|
||||
size 653272
|
||||
3
public/gallery/cert5.jpg
Normal file
3
public/gallery/cert5.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00f9b20bb8f70db9d7ccd87a223729c8bfca846f8def0e3084448b0a0a564734
|
||||
size 203639
|
||||
3
public/gallery/cert6.jpg
Normal file
3
public/gallery/cert6.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:35d31b3aadbcfae40b3dba305cd501ed994e76c797ef27e977868f787804be75
|
||||
size 61054
|
||||
3
public/gallery/cert7.jpg
Normal file
3
public/gallery/cert7.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e8b393b7f4278e5584cdde6579574980c685a561a72b269132e91d01622ff82
|
||||
size 151321
|
||||
3
public/gallery/cert8.jpg
Normal file
3
public/gallery/cert8.jpg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b3b9897d820f46be4bf2166dd72017a9ad54bb1bc0fd63c92ceb4e7960cf7aee
|
||||
size 226604
|
||||
32
public/index.html
Normal file
32
public/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- Updated favicon with larger sizes -->
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" sizes="256x256 128x128 70x70 34x34 26x26 18x18" type="image/x-icon" />
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<!-- Updated description -->
|
||||
<Helmet>
|
||||
<title>Laxmi Civil Engineering Services Pvt. Ltd. (LCEPL)</title>
|
||||
<meta name="description" content="Laxmi Civil Engineering Services Pvt. Ltd. – experts in infrastructure, water supply, roads, tunnels, and wastewater projects." />
|
||||
</Helmet>
|
||||
|
||||
<!-- Updated Apple Touch Icon -->
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
|
||||
<!-- Manifest for PWA -->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<title>Laxmi Civil Engineering Services Pvt. Ltd. (LCEPL)</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
3
public/logo192.png
Normal file
3
public/logo192.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3090d5d57128fe707c6021b851437e1f0a084f5f65e5b81528fe30305ae717d1
|
||||
size 38136
|
||||
3
public/logo512.png
Normal file
3
public/logo512.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3090d5d57128fe707c6021b851437e1f0a084f5f65e5b81528fe30305ae717d1
|
||||
size 38136
|
||||
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "LCEPL",
|
||||
"name": "Laxmi Civil Engineering Services Pvt. Ltd.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon_large.ico",
|
||||
"sizes": "256x256 128x128 70x70 34x34 26x26 18x18",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
src/App.css
Normal file
38
src/App.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
194
src/App.js
Normal file
194
src/App.js
Normal file
@@ -0,0 +1,194 @@
|
||||
// src/App.js
|
||||
import React, { useEffect } from 'react';
|
||||
import './index.css';
|
||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
import Header from './components/Header';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
import Home from './pages/Home';
|
||||
import AboutUs from './pages/AboutUs';
|
||||
import Careers from './pages/Careers';
|
||||
import Office from './pages/Office';
|
||||
import ContactUs from './pages/ContactUs';
|
||||
import Gallery from './pages/Gallery';
|
||||
import Projects from './pages/Projects';
|
||||
import AddProjects from './pages/AddProjects';
|
||||
import GalleryAdmin from './pages/GalleryAdmin';
|
||||
import BlogList from './pages/BlogList';
|
||||
import BlogDetail from './pages/BlogDetail';
|
||||
import JobApplicationForm from './pages/JobApplicationForm';
|
||||
|
||||
// Admin
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import AdminLogin from './pages/AdminLogin';
|
||||
import AdminLayout from './components/AdminLayout';
|
||||
import HRAdmin from './pages/HRAdmin';
|
||||
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
// ScrollToTop component
|
||||
const ScrollToTop = () => {
|
||||
const { pathname } = useLocation();
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [pathname]);
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<ScrollToTop /> {/* ensures scroll starts at top on route change */}
|
||||
|
||||
<Routes>
|
||||
{/* Admin Login */}
|
||||
<Route path="/admin-login" element={<AdminLogin />} />
|
||||
|
||||
{/* Protected Admin Routes with Layout */}
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AdminLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route path="projects" element={<AddProjects />} />
|
||||
<Route path="gallery" element={<GalleryAdmin />} />
|
||||
<Route path="hr" element={<HRAdmin />} />
|
||||
</Route>
|
||||
|
||||
{/* Public Routes */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<Home />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/job-application"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<JobApplicationForm />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/about-us"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<AboutUs />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/careers"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<Careers />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/careers"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<Careers />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/office"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<Office />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/contact-us"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<ContactUs />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/gallery"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<Gallery />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/projects"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<Projects />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/blog"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<BlogList />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/blog/:slug"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<BlogDetail />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Fallback Route */}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<>
|
||||
<Header />
|
||||
<Home />
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
8
src/App.test.js
Normal file
8
src/App.test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
97
src/components/AdminLayout.jsx
Normal file
97
src/components/AdminLayout.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useState } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import PasswordPopup from "./PasswordPopup";
|
||||
|
||||
const AdminLayout = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [popupFor, setPopupFor] = useState("");
|
||||
|
||||
const PASSWORDS = {
|
||||
Projects: "Lcepl@2026", // ← set your Projects password
|
||||
Gallery: "Lcepl#2026", // ← set your Gallery password
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ name: "Projects", path: "/admin/projects" },
|
||||
{ name: "Gallery", path: "/admin/gallery" },
|
||||
{ name: "HR", path: "/admin/hr" },
|
||||
];
|
||||
|
||||
// When user clicks menu
|
||||
const handleMenuClick = (item) => {
|
||||
if (item.name === "Projects" || item.name === "Gallery") {
|
||||
setPopupFor(item.name);
|
||||
setShowPopup(true);
|
||||
} else {
|
||||
navigate(item.path);
|
||||
}
|
||||
};
|
||||
|
||||
// Validate password
|
||||
const handlePasswordSubmit = (password) => {
|
||||
if (password === PASSWORDS[popupFor]) {
|
||||
setShowPopup(false);
|
||||
navigate(`/admin/${popupFor.toLowerCase()}`);
|
||||
} else {
|
||||
alert("❌ Incorrect password");
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
sessionStorage.removeItem("admin");
|
||||
navigate("/admin-login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-gray-800 text-white flex flex-col">
|
||||
<div className="text-2xl font-bold p-4 border-b border-gray-700">
|
||||
Admin Panel
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4">
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item.name}
|
||||
onClick={() => handleMenuClick(item)}
|
||||
className={`w-full text-left px-4 py-2 rounded mb-2 hover:bg-gray-700 transition-colors ${
|
||||
location.pathname === item.path ? "bg-gray-700 font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 bg-gray-100 p-4 relative">
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="absolute top-4 right-4 bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* Password Popup */}
|
||||
{showPopup && (
|
||||
<PasswordPopup
|
||||
title={`${popupFor} Access`}
|
||||
onSubmit={handlePasswordSubmit}
|
||||
onClose={() => setShowPopup(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLayout;
|
||||
23
src/components/AwardsCard.jsx
Normal file
23
src/components/AwardsCard.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
import '../styles/AwardsCard.css';
|
||||
|
||||
const AwardsCard = () => {
|
||||
return (
|
||||
<section className="awards-section">
|
||||
<div className="awards-container">
|
||||
<h2 className="highlight-title">Awards & Certifications</h2>
|
||||
<p>
|
||||
We are proudly certified under <strong>ISO 9001:2015</strong>, <strong>ISO 14001:2015</strong>, and
|
||||
<strong> ISO 45001:2018</strong>, showcasing our unwavering commitment to quality, environmental
|
||||
responsibility, and occupational safety.
|
||||
</p>
|
||||
<p>
|
||||
Our achievements and accolades reflect our drive for excellence, innovation, and industry leadership over the
|
||||
past four decades.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AwardsCard;
|
||||
13
src/components/ClientLogo.jsx
Normal file
13
src/components/ClientLogo.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import '../styles/AboutUs.css';
|
||||
|
||||
const ClientLogo = ({ logo }) => {
|
||||
return (
|
||||
<div className="client-logo">
|
||||
<img src={logo} alt="Client Logo" className="client-logo-img" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientLogo;
|
||||
|
||||
214
src/components/ClientsSection.jsx
Normal file
214
src/components/ClientsSection.jsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const clients = [
|
||||
{ name: 'Public Works Department', logo: require('../assets/clientLogos/pwd.png') },
|
||||
{ name: 'Water Resource Department', logo: require('../assets/clientLogos/wrd.png') },
|
||||
{ name: 'Maharashtra Jeevan Pradhikaran', logo: require('../assets/clientLogos/mjp.png') },
|
||||
{ name: 'The City & Industrial Development Corporation of Maharashtra Limited', logo: require('../assets/clientLogos/cidco.png') },
|
||||
{ name: 'Maharashtra Industrial Development Corporation', logo: require('../assets/clientLogos/midc.png') },
|
||||
{ name: 'Bengaluru Water Supply and Sewerage Board', logo: require('../assets/clientLogos/bwssb.png') },
|
||||
{ name: 'Karnataka Urban Infrastructure Development and Finance Corporation', logo: require('../assets/clientLogos/kuidfc.png') },
|
||||
{ name: 'Krishna Bhagya Jal Nigam Limited', logo: require('../assets/clientLogos/kbjnl.png') },
|
||||
{ name: 'Karnataka Urban Water Supply And Drainage Board', logo: require('../assets/clientLogos/kuwsdb.png') },
|
||||
{ name: 'Kerala Water Authority', logo: require('../assets/clientLogos/kwa.png') },
|
||||
{ name: 'Gail India Limited', logo: require('../assets/clientLogos/gail.png') },
|
||||
{ name: 'Municipal Corporation', logo: require('../assets/clientLogos/municipal.png') },
|
||||
{ name: 'Narmada Valley Development Authority', logo: require('../assets/clientLogos/nvda.png') },
|
||||
{ name: 'Rural Water Supply & Sanitation Department', logo: require('../assets/clientLogos/rwss.png') },
|
||||
{ name: 'Madhya Pradesh Jal Nigam Maryadit', logo: require('../assets/clientLogos/mpjnm.png') },
|
||||
{ name: 'Public Health Engineering Department', logo: require('../assets/clientLogos/phed.png') },
|
||||
{ name: 'Goa Tourism Development Corporation Limited', logo: require('../assets/clientLogos/gtdcl.png') },
|
||||
{ name: 'Central Railway', logo: require('../assets/clientLogos/cr.png') },
|
||||
{ name: 'Gujarat Water Infrastructure Limited', logo: require('../assets/clientLogos/gwil.png') },
|
||||
{ name: 'JUIDCO', logo: require('../assets/clientLogos/judaico.png') },
|
||||
{ name: 'Hindustan Aeronautics Limited', logo: require('../assets/clientLogos/hal.png') },
|
||||
{ name: 'Municipal Councils', logo: require('../assets/clientLogos/municipal-council.png') },
|
||||
{ name: 'Solapur Smart City', logo: require('../assets/clientLogos/solapur.png') },
|
||||
{ name: 'Vadodara Smart City', logo: require('../assets/clientLogos/vadodara.png') },
|
||||
|
||||
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const cardVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 100,
|
||||
damping: 20,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const hoverVariants = {
|
||||
hover: {
|
||||
y: -10,
|
||||
scale: 1.05,
|
||||
boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 10
|
||||
}
|
||||
},
|
||||
tap: {
|
||||
y: -15,
|
||||
scale: 1.1,
|
||||
boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ClientsSection = () => {
|
||||
const [selectedCard, setSelectedCard] = useState(null);
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
|
||||
const isTablet = typeof window !== 'undefined' && window.innerWidth >= 768 && window.innerWidth < 1024;
|
||||
|
||||
let row1, row2, row3;
|
||||
|
||||
if (isMobile) {
|
||||
row1 = clients.slice(0, 8);
|
||||
} else if (isTablet) {
|
||||
const chunkSize = Math.ceil(clients.length / 2);
|
||||
row1 = clients.slice(0, chunkSize);
|
||||
row2 = clients.slice(chunkSize);
|
||||
} else {
|
||||
const chunkSize = Math.ceil(clients.length / 3);
|
||||
row1 = clients.slice(0, chunkSize);
|
||||
row2 = clients.slice(chunkSize, chunkSize * 2);
|
||||
row3 = clients.slice(chunkSize * 2);
|
||||
}
|
||||
|
||||
const handleCardClick = (clientKey) => {
|
||||
setSelectedCard(selectedCard === clientKey ? null : clientKey);
|
||||
};
|
||||
|
||||
const renderScrollingRow = (rowItems, rowKey) => (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
variants={containerVariants}
|
||||
key={rowKey}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex overflow-hidden w-full">
|
||||
<div className="flex animate-[scroll_30s_linear_infinite] hover:animation-pause">
|
||||
{[...rowItems, ...rowItems].map((client, index) => {
|
||||
const clientKey = `${rowKey}-${index}`;
|
||||
return (
|
||||
<motion.div
|
||||
key={clientKey}
|
||||
variants={cardVariants}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
animate={selectedCard === clientKey ? "tap" : "visible"}
|
||||
onClick={() => handleCardClick(clientKey)}
|
||||
className="inline-flex flex-col items-center justify-center mx-2 md:mx-4 bg-white p-3 md:p-4 rounded-lg shadow-md w-36 h-32 md:w-48 md:h-40 flex-shrink-0 cursor-pointer relative z-0"
|
||||
style={{
|
||||
zIndex: selectedCard === clientKey ? 10 : 1
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
variants={hoverVariants}
|
||||
className="w-full h-full flex flex-col items-center justify-center"
|
||||
>
|
||||
<div className="h-12 md:h-16 flex items-center mb-2 md:mb-3">
|
||||
<img
|
||||
src={client.logo}
|
||||
alt={client.name}
|
||||
className="max-h-full max-w-full object-contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs md:text-sm font-medium text-blue-900 text-center break-words w-full px-1 md:px-2">
|
||||
{client.name}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="bg-blue-900 py-12 md:py-20 px-0 overflow-hidden relative">
|
||||
{/* Backdrop that appears when a card is selected */}
|
||||
{selectedCard && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-30 z-20"
|
||||
onClick={() => setSelectedCard(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 relative z-10">
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold text-white mb-3 md:mb-4 text-center"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
Clients who trust us
|
||||
</motion.h2>
|
||||
<motion.h3
|
||||
className="text-xl md:text-2xl font-semibold text-center text-blue-200 mb-4 md:mb-6"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.5 }}
|
||||
>
|
||||
RISING TOGETHER WITH ELITE COLLABORATIONS
|
||||
</motion.h3>
|
||||
<motion.p
|
||||
className="text-center text-blue-100 max-w-4xl mx-auto mb-8 md:mb-12 px-4 text-sm md:text-base"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4, duration: 0.5 }}
|
||||
>
|
||||
At LCEPL, we take pride in unveiling our elite portfolio – a testament to our commitment to elevating horizons and seamlessly connecting the landmarks that shape the world around us.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 md:space-y-6 w-full relative z-10">
|
||||
{renderScrollingRow(row1, 'row1')}
|
||||
{!isMobile && renderScrollingRow(row2, 'row2')}
|
||||
{!isMobile && !isTablet && renderScrollingRow(row3, 'row3')}
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
@keyframes scroll {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
.animate-\\[scroll_30s_linear_infinite\\] {
|
||||
animation: scroll 30s linear infinite;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.animate-\\[scroll_30s_linear_infinite\\] {
|
||||
animation: scroll 30s linear infinite;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientsSection;
|
||||
35
src/components/CounterCard.jsx
Normal file
35
src/components/CounterCard.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import CountUp from 'react-countup';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import '../styles/CounterCard.css';
|
||||
|
||||
const CounterCard = ({ count, label, icon }) => {
|
||||
// Extract numeric part from count string (e.g. "200+" => 200)
|
||||
const numericCount = typeof count === 'string' ? parseInt(count, 10) : count;
|
||||
|
||||
// Check if original count string ends with '+'
|
||||
const hasPlus = typeof count === 'string' && count.trim().endsWith('+');
|
||||
|
||||
// Intersection Observer to detect when the card is visible
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true, // animate only once
|
||||
threshold: 0.3 // 30% of the card visible triggers animation
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="counter-card" ref={ref}>
|
||||
{icon && (
|
||||
<div className="icon" style={{ fontSize: '3rem', marginBottom: '15px' }}>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<h3>
|
||||
{inView ? <CountUp end={numericCount} duration={2.5} /> : 0}
|
||||
{hasPlus && '+'}
|
||||
</h3>
|
||||
<p>{label}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CounterCard;
|
||||
13
src/components/DirectorCard.jsx
Normal file
13
src/components/DirectorCard.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import '../styles/AboutUs.css';
|
||||
|
||||
const DirectorCard = ({ image, name }) => {
|
||||
return (
|
||||
<div className="director-card">
|
||||
<img src={image} alt={name} className="director-photo" />
|
||||
<p>{name}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectorCard;
|
||||
13
src/components/Footer.jsx
Normal file
13
src/components/Footer.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="bg-gray-900 py-4">
|
||||
<div className="container mx-auto px-4 text-center text-sm text-white">
|
||||
© {new Date().getFullYear()} Laxmi Civil Engineering Services Pvt. Ltd. All Rights Reserved.
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
84
src/components/Header.jsx
Normal file
84
src/components/Header.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { FiMenu, FiX } from 'react-icons/fi';
|
||||
import logo from '../assets/lcepl_logo.png';
|
||||
|
||||
const Header = () => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const navLinks = [
|
||||
{ path: "/", label: "Home" },
|
||||
{ path: "/about-us", label: "About Us" },
|
||||
{ path: "/projects", label: "Projects" },
|
||||
{ path: "/careers", label: "Careers" },
|
||||
// { path: "/office", label: "Office" },
|
||||
{ path: "/gallery", label: "Awards" },
|
||||
{ path: "/contact-us", label: "Contact Us" },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50 font-sans">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex justify-between items-center h-20">
|
||||
{/* Logo and Company Name */}
|
||||
<Link to="/" className="flex items-center space-x-3 shrink-0">
|
||||
<img
|
||||
src={logo}
|
||||
alt="LCEPL Logo"
|
||||
className="h-20 w-20 object-contain"
|
||||
/>
|
||||
<div className="leading-tight hidden sm:block">
|
||||
<h1 className="text-lg md:text-xl font-semibold text-blue-900 whitespace-nowrap">
|
||||
Laxmi Civil Engineering Services Pvt Ltd
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex flex-wrap justify-end gap-x-4 items-center">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className="relative px-3 py-2 text-base font-medium text-gray-700 hover:text-blue-800 transition-colors duration-200 group whitespace-nowrap"
|
||||
>
|
||||
{link.label}
|
||||
<span className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-0 h-0.5 bg-blue-800 transition-all duration-300 group-hover:w-2/3"></span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<button
|
||||
className="md:hidden text-gray-700 focus:outline-none"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<FiX className="h-6 w-6" />
|
||||
) : (
|
||||
<FiMenu className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden bg-white py-3 px-4 shadow-md rounded-b-lg space-y-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className="block py-2 px-3 text-base font-medium text-gray-700 hover:text-blue-800 transition"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
160
src/components/IndiaMap.jsx
Normal file
160
src/components/IndiaMap.jsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
Marker
|
||||
} from "react-simple-maps";
|
||||
import { geoCentroid } from "d3-geo";
|
||||
import indiaMap from "../assets/india.topo.json"; // Make sure this file path is correct
|
||||
|
||||
const IndiaMap = () => {
|
||||
const [hoveredState, setHoveredState] = useState(null);
|
||||
|
||||
|
||||
|
||||
const projectStates = [
|
||||
"Jammu and Kashmir",
|
||||
"Punjab",
|
||||
"Uttarakhand",
|
||||
"Uttar Pradesh",
|
||||
"Bihar",
|
||||
"Jharkhand",
|
||||
"West Bengal",
|
||||
"Odisha",
|
||||
"Chhattisgarh",
|
||||
"Madhya Pradesh",
|
||||
"Gujarat",
|
||||
"Maharashtra",
|
||||
"Goa",
|
||||
"Karnataka",
|
||||
"Telangana",
|
||||
"Andhra Pradesh",
|
||||
"Tamil Nadu",
|
||||
"Kerala"
|
||||
];
|
||||
|
||||
const markerCoordinates = {
|
||||
"Jammu and Kashmir": [75.34, 33.75],
|
||||
"Punjab": [75.5, 31.3],
|
||||
"Uttarakhand": [79.0, 30.1],
|
||||
"Uttar Pradesh": [80.5, 26.5],
|
||||
"Bihar": [85.3, 25.6],
|
||||
"Jharkhand": [85.3, 23.6],
|
||||
"West Bengal": [87.9, 23.2],
|
||||
"Odisha": [85.1, 20.3],
|
||||
"Chhattisgarh": [81.5, 21.3],
|
||||
"Madhya Pradesh": [78.6, 23.4],
|
||||
"Gujarat": [71.2, 22.3],
|
||||
"Maharashtra": [75.7, 19.5],
|
||||
"Goa": [74.0, 15.3],
|
||||
"Karnataka": [76.5, 14.5],
|
||||
"Telangana": [78.4, 17.5],
|
||||
"Andhra Pradesh": [80.6, 15.9],
|
||||
"Tamil Nadu": [78.6, 10.5],
|
||||
"Kerala": [76.2, 10.1],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center mt-10">
|
||||
|
||||
|
||||
<div className="map-wrapper">
|
||||
<ComposableMap
|
||||
projection="geoMercator"
|
||||
width={1850} // increased width
|
||||
height={1100} // increased height
|
||||
projectionConfig={{ scale: 1850, center: [82.8, 22.5] }} // increased scale
|
||||
>
|
||||
|
||||
<Geographies geography={indiaMap}>
|
||||
{({ geographies }) =>
|
||||
geographies.map((geo) => {
|
||||
const stateName = geo.properties.st_nm;
|
||||
const isHighlighted = projectStates.includes(stateName);
|
||||
const centroid = geoCentroid(geo);
|
||||
|
||||
return (
|
||||
<React.Fragment key={geo.rsmKey}>
|
||||
<Geography
|
||||
geography={geo}
|
||||
onMouseEnter={() => setHoveredState({ name: stateName, coords: centroid })}
|
||||
onMouseLeave={() => setHoveredState(null)}
|
||||
style={{
|
||||
default: {
|
||||
fill: isHighlighted ? "#E0E0E0" : "#E0E0E0",
|
||||
stroke: "#607D8B",
|
||||
strokeWidth: 1,
|
||||
outline: "none",
|
||||
},
|
||||
hover: {
|
||||
fill: "#1e40af",
|
||||
cursor: "pointer",
|
||||
outline: "none",
|
||||
},
|
||||
pressed: {
|
||||
fill: "#1e3a8a",
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
|
||||
{/* Project markers */}
|
||||
{projectStates.map((state) => {
|
||||
const coords = markerCoordinates[state];
|
||||
if (!coords) return null;
|
||||
|
||||
return (
|
||||
<Marker key={state} coordinates={coords}>
|
||||
<circle r={12} fill="#0ba512ff" stroke="#fff" strokeWidth={2} />
|
||||
<text
|
||||
textAnchor="middle"
|
||||
y={-12}
|
||||
style={{
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: 22,
|
||||
fill: "#000",
|
||||
fontWeight: "bold",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{state}
|
||||
</text>
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Tooltip/label for hovered state */}
|
||||
{hoveredState?.name && hoveredState?.coords && (
|
||||
<Marker coordinates={hoveredState.coords}>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
y={-15}
|
||||
style={{
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: 22,
|
||||
fontWeight: "bold",
|
||||
fill: "#111827",
|
||||
pointerEvents: "none",
|
||||
paintOrder: "stroke",
|
||||
stroke: "#fff",
|
||||
strokeWidth: 3,
|
||||
strokeLinejoin: "round",
|
||||
}}
|
||||
>
|
||||
{hoveredState.name}
|
||||
</text>
|
||||
</Marker>
|
||||
)}
|
||||
</ComposableMap>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndiaMap;
|
||||
0
src/components/Navbar.jsx
Normal file
0
src/components/Navbar.jsx
Normal file
65
src/components/PasswordPopup.jsx
Normal file
65
src/components/PasswordPopup.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const PasswordPopup = ({ title, onSubmit, onClose }) => {
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(password);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
|
||||
<div className="bg-white w-full max-w-md rounded-lg shadow-lg p-8 animate-fadeIn">
|
||||
|
||||
{/* Title */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-800">{title}</h2>
|
||||
<p className="text-gray-600 mt-2">Enter the password to continue</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="mt-6 space-y-6">
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md
|
||||
shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-300 text-gray-800 rounded-md hover:bg-gray-400"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordPopup;
|
||||
14
src/components/ProtectedRoute.jsx
Normal file
14
src/components/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
|
||||
export default function ProtectedRoute({ children }) {
|
||||
const isAdmin = sessionStorage.getItem("admin"); // session-based login
|
||||
const location = useLocation(); // current path
|
||||
|
||||
if (!isAdmin) {
|
||||
// redirect to login and remember attempted page
|
||||
return <Navigate to="/admin-login" replace state={{ from: location }} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
31
src/components/ScrollDownArrow.jsx
Normal file
31
src/components/ScrollDownArrow.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { FaArrowDown } from "react-icons/fa";
|
||||
|
||||
const ScrollDownArrow = ({ targetId }) => {
|
||||
const scrollToSection = () => {
|
||||
const target = document.getElementById(targetId);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={scrollToSection}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "20px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
cursor: "pointer",
|
||||
zIndex: 50,
|
||||
fontSize: "2rem",
|
||||
color: "white",
|
||||
animation: "bounce 1.5s infinite",
|
||||
}}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollDownArrow;
|
||||
138
src/components/SectorsSlider.jsx
Normal file
138
src/components/SectorsSlider.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import waterIcon from "../assets/water.png";
|
||||
import stormIcon from "../assets/storm.png";
|
||||
import electroIcon from "../assets/electro.png";
|
||||
import realEstateIcon from "../assets/real-estate.png";
|
||||
import tunnelIcon from "../assets/tunnel.png";
|
||||
import roadsIcon from "../assets/roads.png";
|
||||
import wastewaterIcon from "../assets/wastewater.png";
|
||||
import irrigationIcon from "../assets/irrigation.png";
|
||||
import renewableIcon from "../assets/renewable.png";
|
||||
import "../styles/SectorsSlider.css";
|
||||
|
||||
const sectors = [
|
||||
{
|
||||
title: "Water Supply",
|
||||
icon: waterIcon,
|
||||
description: "LCEPL delivers end-to-end water infrastructure solutions, including 24x7 water supply networks, treatment plants, irrigation systems, wastewater management, and underground drainage for urban and rural needs. With over 1,50,000 km of pipelines laid, the company has executed major projects across multiple states, covering design, construction, commissioning, and operation of pumping stations, reservoirs, and SCADA-based automation systems.",
|
||||
},
|
||||
{
|
||||
title: "Storm Water",
|
||||
icon: stormIcon,
|
||||
description: "LCEPL provides efficient storm water solutions to prevent flooding, protect infrastructure, and ensure environmental safety. Our expertise includes designing and constructing drainage systems, retention structures, and rainwater harvesting facilities. We integrate sustainable methods like permeable pavements and bioswales to manage runoff effectively. With advanced engineering and proven execution, we deliver reliable solutions for urban and industrial needs.S",
|
||||
},
|
||||
{
|
||||
title: "Electromechanical & Instrumentation",
|
||||
icon: electroIcon,
|
||||
description: "LCEPL offers end-to-end solutions in pump systems—from project design and consultation to installation, operation, and maintenance of horizontal split casing, vertical turbine, and submersible pumps. The company handles complex installations, including floating pontoon setups and high-voltage turnkey projects, delivering complete electrical substation installations. Expertise spans pumping machinery up to 30,000 HP, high-voltage switchyards, transformers up to 26 MVA, and advanced surge protection systems for diverse schemes.",
|
||||
},
|
||||
{
|
||||
title: "Real Estate / Buildings",
|
||||
icon: realEstateIcon,
|
||||
description: "LCEPL delivers end-to-end civil engineering solutions for residential, commercial, and industrial real estate / buildings projects. Our expertise spans land development, structural construction, and infrastructure integration with a focus on quality and timely delivery. We ensure sustainable design, robust engineering, and adherence to safety and regulatory standards to create lasting value for our clients.",
|
||||
},
|
||||
{
|
||||
title: "Tunnel",
|
||||
icon: tunnelIcon,
|
||||
description: "LCEPL specializes in the design and construction of tunnels for transportation, irrigation, and utility purposes. We leverage advanced engineering techniques and modern tunneling equipment to ensure safety, precision, and durability. Our expertise covers geological assessment, structural stability, and efficient project execution, even in challenging terrains.",
|
||||
},
|
||||
{
|
||||
title: "Roads",
|
||||
icon: roadsIcon,
|
||||
description: "LCEPL excels in the design, construction, and maintenance of roads and bridges, delivering high-quality infrastructure that meets all standard norms and guidelines. The company has successfully executed projects covering about 700 km of national highways, state highways, and village roads across Maharashtra, Madhya Pradesh, Chhattisgarh, and Bihar.",
|
||||
},
|
||||
{
|
||||
title: "Wastewater / Sewerage",
|
||||
icon: wastewaterIcon,
|
||||
description: "LCEPL delivers sustainable wastewater management solutions, ensuring safe disposal while protecting health and the environment. The company has executed over 1,000 km of underground drainage pipelines and designed and built sewage treatment plants (STPs) of various capacities, using advanced processes such as SBR, MBR, FAL, and oxidation ponds, along with pumping stations for efficient wastewater handling.",
|
||||
},
|
||||
{
|
||||
title: "Irrigation",
|
||||
icon: irrigationIcon,
|
||||
description: "LCEPL specializes in Lift Irrigation Systems, enabling water to be pumped from lower to higher elevations for agriculture, landscaping, and soil restoration in dry regions. With extensive experience in building dams, canals, and related infrastructure in collaboration with the government, the company has executed projects involving over 1,00,000 km of pipelines, pumping stations, earthen dams, barrages, weirs, and lift irrigation schemes across multiple states, supported by advanced SCADA-based automation systems.",
|
||||
},
|
||||
{
|
||||
title: "Renewable Energy",
|
||||
icon: renewableIcon,
|
||||
description: "LCEPL offers high-capacity pumped storage systems, with solutions reaching up to 300 MW. The system primarily consists of reversible Francis turbines, reversible pump-turbine motor-generators operating as a combined unit, and a grid interface with speed governors and control systems.",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const SectorsSlider = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
|
||||
const goToSlide = (index) => setActiveIndex(index);
|
||||
const goToPrev = () => setActiveIndex((prev) => (prev === 0 ? sectors.length - 1 : prev - 1));
|
||||
const goToNext = () => setActiveIndex((prev) => (prev === sectors.length - 1 ? 0 : prev + 1));
|
||||
|
||||
return (
|
||||
<section className="infraCont">
|
||||
<div className="max-w-7xl mx-auto px-4 text-center relative z-10 pt-0">
|
||||
<h2 className="text-4xl font-bold text-blue-900 mb-2 relative inline-block">
|
||||
<span className="relative z-10">
|
||||
Our Core Sectors
|
||||
<motion.span
|
||||
initial={{ scaleX: 0 }}
|
||||
whileInView={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className="absolute bottom-0 left-0 w-full h-1 bg-red-600 z-0 transform origin-left"
|
||||
/>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto mt-2">
|
||||
Explore our expertise across diverse infrastructure sectors
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="custom">
|
||||
{sectors.map((sector, index) => (
|
||||
<div key={index} className={`infraWrap ${index === activeIndex ? "active" : ""}`}>
|
||||
<div className="slideTing">
|
||||
<div className="sector-card">
|
||||
<img src={sector.icon} alt={sector.title} className="sector-image" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="infraright">
|
||||
<div className="infraInfo">
|
||||
<h2>{sector.title}</h2>
|
||||
<h3>
|
||||
{sector.title === "Roads"
|
||||
? "TRUSTED PATHWAYS TO TOMORROW"
|
||||
: `REDEFINING ${sector.title.toUpperCase()} FOR THE FUTURE`}
|
||||
</h3>
|
||||
<p>{sector.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="slider-nav">
|
||||
<button onClick={goToPrev} className="nav-arrow prev" aria-label="Previous sector">
|
||||
<
|
||||
</button>
|
||||
<button onClick={goToNext} className="nav-arrow next" aria-label="Next sector">
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="indicators">
|
||||
{sectors.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => goToSlide(index)}
|
||||
className={`indicator ${activeIndex === index ? "active" : ""}`}
|
||||
aria-label={`Go to sector ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectorsSlider;
|
||||
|
||||
14
src/components/TeamCard.jsx
Normal file
14
src/components/TeamCard.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import '../styles/AboutUs.css';
|
||||
|
||||
const TeamCard = ({ image, name, position }) => {
|
||||
return (
|
||||
<div className="team-card">
|
||||
<img src={image} alt={name} className="team-photo" />
|
||||
<h4>{name}</h4>
|
||||
<p>{position}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamCard;
|
||||
13
src/components/VisionCard.jsx
Normal file
13
src/components/VisionCard.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import '../styles/VisionCard.css'; // ✅ Adjust path if needed
|
||||
|
||||
const VisionCard = ({ title, text }) => {
|
||||
return (
|
||||
<div className="vision-card">
|
||||
<h3>{title}</h3>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisionCard;
|
||||
23
src/context/ApplicationContext.js
Normal file
23
src/context/ApplicationContext.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { createContext, useState, useEffect } from 'react';
|
||||
|
||||
export const ApplicationContext = createContext();
|
||||
|
||||
export const ApplicationProvider = ({ children }) => {
|
||||
const [applications, setApplications] = useState(() => {
|
||||
const saved = localStorage.getItem('applications');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
// Save to localStorage whenever applications change
|
||||
useEffect(() => {
|
||||
localStorage.setItem('applications', JSON.stringify(applications));
|
||||
}, [applications]);
|
||||
|
||||
return (
|
||||
<ApplicationContext.Provider value={{ applications, setApplications }}>
|
||||
{children}
|
||||
</ApplicationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
39
src/context/ProjectContext.js
Normal file
39
src/context/ProjectContext.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
|
||||
// 1. Create Context
|
||||
const ProjectContext = createContext();
|
||||
|
||||
// 2. Custom hook to use the context
|
||||
export const useProjects = () => useContext(ProjectContext);
|
||||
|
||||
// 3. Provider Component
|
||||
export const ProjectProvider = ({ children }) => {
|
||||
const [projects, setProjects] = useState([]);
|
||||
|
||||
// Add new project
|
||||
const addProject = (project) => {
|
||||
setProjects((prev) => [...prev, project]);
|
||||
};
|
||||
|
||||
// Edit existing project by ID
|
||||
const editProject = (updatedProject) => {
|
||||
setProjects((prev) =>
|
||||
prev.map((proj) =>
|
||||
proj.id === updatedProject.id ? updatedProject : proj
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// Delete project by ID
|
||||
const deleteProject = (id) => {
|
||||
setProjects((prev) => prev.filter((proj) => proj.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectContext.Provider
|
||||
value={{ projects, addProject, editProject, deleteProject }}
|
||||
>
|
||||
{children}
|
||||
</ProjectContext.Provider>
|
||||
);
|
||||
};
|
||||
28
src/data/blogs.js
Normal file
28
src/data/blogs.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const blogs = [
|
||||
{
|
||||
slug: "construction-company-in-india",
|
||||
title: "Construction Company in India – Complete Guide 2025",
|
||||
description: "Explore top construction companies in India, their projects, and future trends.",
|
||||
content: `
|
||||
<h2>Introduction</h2>
|
||||
<p>India is home to many leading construction companies...</p>
|
||||
|
||||
<h2>Top Construction Projects</h2>
|
||||
<p>Infrastructure, road construction, tunnels, water supply, LCEPL, irrigation, wastewater, India, Infrastructure, Building, sewerage, building construction, road construction, water supply, water treatment plant, commercial construction...</p>
|
||||
|
||||
<h2>Conclusion</h2>
|
||||
<p>Choosing the right construction company ensures quality and trust...</p>
|
||||
`
|
||||
},
|
||||
{
|
||||
slug: "water-treatment-plant",
|
||||
title: "Importance of Water Treatment Plants",
|
||||
description: "Understand the role of water treatment plants in urban development.",
|
||||
content: `
|
||||
<h2>Why Water Treatment is Important</h2>
|
||||
<p>Clean water is essential for sustainable cities...</p>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
export default blogs;
|
||||
6
src/index.css
Normal file
6
src/index.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
body {
|
||||
@apply font-sans;
|
||||
}
|
||||
20
src/index.js
Normal file
20
src/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { ApplicationProvider } from './context/ApplicationContext';
|
||||
import { ProjectProvider } from './context/ProjectContext';
|
||||
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<BrowserRouter>
|
||||
<ApplicationProvider>
|
||||
<ProjectProvider>
|
||||
<App />
|
||||
</ProjectProvider>
|
||||
</ApplicationProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
169
src/pages/AboutUs.jsx
Normal file
169
src/pages/AboutUs.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import CounterCard from '../components/CounterCard';
|
||||
import '../styles/AboutUs.css';
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
// Import background images
|
||||
import civilBg from "../assets/civil-bg.jpg";
|
||||
import mechanicalBg from "../assets/mechanical-bg.jpg";
|
||||
import electricalBg from "../assets/electrical-bg.jpg";
|
||||
import electromechanicalBg from "../assets/electromechanical-bg.jpg";
|
||||
import itBg from "../assets/it-bg.jpg";
|
||||
import renewableBg from "../assets/renewable-bg.jpg"; // renamed image
|
||||
|
||||
// Animation variants
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.2 }
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
const AboutUs = () => {
|
||||
return (
|
||||
<>
|
||||
{/* HERO SECTION */}
|
||||
<section className="about-hero">
|
||||
<div className="hero-overlay">
|
||||
<h1>About Us</h1>
|
||||
<p className="hero-subtitle">Building the Nation with Precision & Excellence</p>
|
||||
<p className="mt-4 text-white text-lg italic">“Engineering the future, one project at a time.”</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SLOGAN BANNER */}
|
||||
<div className="bg-gradient-to-r from-blue-400 to-red-500 text-white text-center py-4 font-semibold text-lg tracking-wide shadow-md">
|
||||
Empowering Infrastructure. Enabling Progress. Elevating Lives.
|
||||
</div>
|
||||
|
||||
{/* MAIN ABOUT SECTION */}
|
||||
<section className="about-section px-6 lg:px-20 py-16">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-start">
|
||||
{/* LEFT TEXT */}
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-6">
|
||||
About Laxmi Civil Engineering Services Pvt. Ltd.
|
||||
</h2>
|
||||
<p className="text-gray-700 leading-relaxed mb-4">
|
||||
Laxmi Civil Engineering Services Pvt. Ltd. was established on <strong>15th August 1980</strong> as a partnership firm and later
|
||||
converted into a Private Limited Company on <strong>31st March 2000</strong>.
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed mb-4">
|
||||
For over four decades, LCEPL has been delivering exceptional engineering and construction services, making us one of the leading civil engineering solution providers in India.
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed mb-4">
|
||||
We specialize in major infrastructure projects such as portable water supply, irrigation, pumping stations, electromechanical works, and operation & maintenance.
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed mb-4">
|
||||
Our leadership in these unique services has allowed us to establish a strong foothold in the central, western, and southern regions of India, while also expanding into the northern region.
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
With a workforce of <strong>1300+ professionals</strong>, supported by advanced technology and an inspiring work environment, we continue to move forward with confidence.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* COUNTERS */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="grid grid-cols-1 gap-6"
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<CounterCard count="45+" label="Years of Experience" icon="🎖️" className="bg-white shadow-lg border border-gray-200 px-4 py-3 rounded-lg" />
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants}>
|
||||
<CounterCard count="18+" label="Number of States" icon="🗺️" className="bg-white shadow-lg border border-gray-200 px-4 py-3 rounded-lg" />
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants}>
|
||||
<CounterCard count="50+" label="Clients" icon="🏭" className="bg-white shadow-lg border border-gray-200 px-4 py-3 rounded-lg" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* QUOTE BANNER */}
|
||||
<div className="bg-blue-100 text-blue-800 text-center py-6 text-xl font-semibold italic tracking-wide">
|
||||
“Driven by innovation, guided by integrity, delivering with passion.”
|
||||
</div>
|
||||
|
||||
{/* VISION & MISSION SECTION */}
|
||||
<motion.section initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} transition={{ duration: 0.6 }} viewport={{ once: true }} className="relative py-20 px-4 md:px-8 lg:px-20 bg-gradient-to-br from-blue-900 to-blue-800 overflow-hidden">
|
||||
<motion.div initial={{ y: 30, opacity: 0 }} whileInView={{ y: 0, opacity: 1 }} transition={{ duration: 0.6 }} viewport={{ once: true }} className="max-w-7xl mx-auto relative z-10">
|
||||
<h2 className="text-4xl font-bold text-center text-white mb-16 relative pb-2">
|
||||
<span className="relative inline-block">
|
||||
Vision & Mission
|
||||
<motion.span initial={{ scaleX: 0 }} whileInView={{ scaleX: 1 }} transition={{ duration: 0.6 }} viewport={{ once: true }} className="absolute bottom-0 left-0 w-full h-1 bg-red-500 z-0 transform origin-center" />
|
||||
</span>
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-16">
|
||||
{/* Vision */}
|
||||
<motion.div initial={{ x: -50, opacity: 0 }} whileInView={{ x: 0, opacity: 1 }} transition={{ duration: 0.5 }} viewport={{ once: true }} className="bg-white p-8 rounded-xl shadow-lg hover:shadow-xl transition-all border-l-4 border-blue-500 hover:border-blue-400">
|
||||
<div className="flex items-start mb-6">
|
||||
<div className="bg-blue-100 p-3 rounded-full mr-4">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-blue-900 mb-2">Vision</h3>
|
||||
<p className="text-gray-700">To be a company at the forefront of engineering and construction, renowned for excellence, quality, performance, and reliability.</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Mission */}
|
||||
<motion.div initial={{ x: 50, opacity: 0 }} whileInView={{ x: 0, opacity: 1 }} transition={{ duration: 0.5 }} viewport={{ once: true }} className="bg-white p-8 rounded-xl shadow-lg hover:shadow-xl transition-all border-l-4 border-red-500 hover:border-red-400">
|
||||
<div className="flex items-start mb-6">
|
||||
<div className="bg-blue-100 p-3 rounded-full mr-4">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-blue-900 mb-2">Mission</h3>
|
||||
<p className="text-gray-700">To complete every project undertaken with sincerity, excellence, and in a time-bound manner, meeting all expectations of the client.</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.section>
|
||||
|
||||
{/* WORKING FIELDS SECTION */}
|
||||
<section className="working-fields px-6 lg:px-20 py-16 bg-gray-50">
|
||||
<h2 className="highlight-title text-4xl font-bold text-center mb-4 text-black">Expertise Across Multiple Sectors</h2>
|
||||
<p className="text-center text-gray-600 mb-12">Our multidisciplinary approach helps us lead across multiple domains.</p>
|
||||
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{ title: "Civil Engineering", icon: "🏗️", desc: "Urban infrastructure, roadworks, dams, WTPs, STPs, stormwater management, and structural development.", bg: civilBg },
|
||||
{ title: "Mechanical Engineering", icon: "⚙️", desc: "Fabrication, installation, and maintenance of industrial systems, heavy machinery, and automated solutions including SCADA.", bg: mechanicalBg },
|
||||
{ title: "Electrical Engineering", icon: "💡", desc: "Reliable installations, power distribution, and maintenance services.", bg: electricalBg },
|
||||
{ title: "Electromechanical & Instrumentation", icon: "🔌", desc: "Integration of mechanical and electrical systems for pumping stations and automation.", bg: electromechanicalBg },
|
||||
{ title: "Information Technology", icon: "💻", desc: "Project planning, GIS, data systems, and automation tools for smart engineering.", bg: itBg },
|
||||
{ title: "Renewable Energy", icon: "☀️", desc: "Solar, wind and sustainable energy solutions with advanced engineering support.", bg: renewableBg },
|
||||
].map((field, idx) => (
|
||||
<div key={idx} className="relative field-card rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition-all cursor-pointer bg-cover bg-center" style={{ backgroundImage: `url(${field.bg})` }}>
|
||||
<div className="absolute inset-0 bg-black/50"></div>
|
||||
<div className="relative p-6 flex flex-col items-center text-center text-white z-10">
|
||||
<div className="text-5xl mb-4">{field.icon}</div>
|
||||
<h3 className="text-xl font-bold mb-2">{field.title}</h3>
|
||||
<p>{field.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutUs;
|
||||
200
src/pages/AddProjects.jsx
Normal file
200
src/pages/AddProjects.jsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const AddProjects = () => {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [sector, setSector] = useState("");
|
||||
const [image, setImage] = useState(null);
|
||||
const [imagePreview, setImagePreview] = useState(null);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editProjectId, setEditProjectId] = useState(null);
|
||||
|
||||
const sectors = [
|
||||
{ label: "Water Supply", value: "water supply" },
|
||||
{ label: "Storm Water", value: "storm water" },
|
||||
{ label: "Electromechanical", value: "electromechanical" },
|
||||
{ label: "Real Estate / Buildings", value: "real estate / buildings" },
|
||||
{ label: "Tunnel", value: "tunnel" },
|
||||
{ label: "Roads", value: "roads" },
|
||||
{ label: "Wastewater / Sewerage ", value: "wastewater / sewerage " },
|
||||
{ label: "Irrigation", value: "irrigation" },
|
||||
{ label: "Renewable Energy", value: "renewable energy" }
|
||||
];
|
||||
|
||||
// Fetch all projects
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_API_BASE_URL}/api/projects`);
|
||||
setProjects(res.data);
|
||||
} catch (err) {
|
||||
console.error("Error fetching projects", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
// Reset the form
|
||||
const resetForm = () => {
|
||||
setSector("");
|
||||
setImage(null);
|
||||
setImagePreview(null);
|
||||
setEditMode(false);
|
||||
setEditProjectId(null);
|
||||
};
|
||||
|
||||
// Handle image selection
|
||||
const handleImageChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
setImage(file);
|
||||
setImagePreview(file ? URL.createObjectURL(file) : null);
|
||||
};
|
||||
|
||||
// Handle form submit (add or update)
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!sector || (!editMode && !image)) {
|
||||
toast.error("Sector and image are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("sector", sector);
|
||||
if (image) formData.append("image", image);
|
||||
|
||||
try {
|
||||
if (editMode) {
|
||||
await axios.post(`${process.env.REACT_APP_API_BASE_URL}/api/projects/update/${editProjectId}`, formData);
|
||||
toast.success("Project updated successfully!");
|
||||
} else {
|
||||
await axios.post(`${process.env.REACT_APP_API_BASE_URL}/api/projects`, formData);
|
||||
toast.success("Project added successfully!");
|
||||
}
|
||||
|
||||
resetForm();
|
||||
fetchProjects();
|
||||
} catch (err) {
|
||||
console.error("Failed to save project", err);
|
||||
toast.error("Error saving project");
|
||||
}
|
||||
};
|
||||
|
||||
// Load project into form for editing
|
||||
const handleEdit = (project) => {
|
||||
setSector(project.sector);
|
||||
setImage(null);
|
||||
setImagePreview(`${process.env.REACT_APP_API_BASE_URL}${project.image}`);
|
||||
setEditMode(true);
|
||||
setEditProjectId(project.id);
|
||||
};
|
||||
|
||||
// Delete a project
|
||||
const handleDelete = async (id) => {
|
||||
if (!window.confirm("Are you sure you want to delete this project?")) return;
|
||||
|
||||
try {
|
||||
await axios.delete(`${process.env.REACT_APP_API_BASE_URL}/api/projects/${id}`);
|
||||
toast.success("Project deleted");
|
||||
fetchProjects();
|
||||
} catch (err) {
|
||||
console.error("Failed to delete", err);
|
||||
toast.error("Failed to delete project");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 min-h-screen bg-gray-50">
|
||||
<h1 className="text-3xl font-bold text-blue-900 mb-6">
|
||||
{editMode ? "Edit Project" : "Add New Project"}
|
||||
</h1>
|
||||
|
||||
{/* FORM */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4 max-w-md mx-auto bg-white p-6 rounded-lg shadow">
|
||||
<select
|
||||
value={sector}
|
||||
onChange={(e) => setSector(e.target.value)}
|
||||
className="w-full border p-2 rounded"
|
||||
>
|
||||
<option value="">Select Sector</option>
|
||||
{sectors.map((s) => (
|
||||
<option key={s.value} value={s.value}>
|
||||
{s.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
{imagePreview && (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
className="h-48 w-full object-cover rounded"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-blue-900 text-white py-2 rounded hover:bg-blue-800"
|
||||
>
|
||||
{editMode ? "Update Project" : "Save Project"}
|
||||
</button>
|
||||
{editMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
className="flex-1 bg-gray-300 text-black py-2 rounded hover:bg-gray-400"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* PROJECT LIST */}
|
||||
<div className="mt-10">
|
||||
<h2 className="text-2xl font-semibold text-blue-800 mb-4">All Projects</h2>
|
||||
<div className="grid md:grid-cols-3 sm:grid-cols-2 gap-6">
|
||||
{projects.map((proj) => (
|
||||
<div
|
||||
key={proj.id}
|
||||
className="bg-white p-4 rounded-xl shadow hover:shadow-lg transition-all relative"
|
||||
>
|
||||
<img
|
||||
src={`${process.env.REACT_APP_API_BASE_URL}${proj.image}`}
|
||||
alt={proj.sector}
|
||||
className="h-56 w-full object-cover rounded mb-3"
|
||||
/>
|
||||
<p className="text-sm text-gray-700 capitalize mb-1">Sector: {proj.sector}</p>
|
||||
|
||||
<div className="flex justify-between mt-2">
|
||||
<button
|
||||
onClick={() => handleEdit(proj)}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(proj.id)}
|
||||
className="text-red-600 hover:underline"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddProjects;
|
||||
25
src/pages/AdminGallery.jsx
Normal file
25
src/pages/AdminGallery.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
const AdminGallery = () => {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 py-16">
|
||||
<h2 className="text-3xl font-bold text-blue-900 mb-4">Admin - Gallery</h2>
|
||||
<p className="text-gray-600 mb-6">Manage gallery images and media files here.</p>
|
||||
|
||||
{/* Example: Gallery cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{/* Replace with API dynamic mapping */}
|
||||
<div className="bg-gray-100 p-4 rounded-lg shadow">
|
||||
<img src="https://via.placeholder.com/300" alt="Gallery 1" className="rounded-md mb-2" />
|
||||
<p className="text-gray-700 font-medium">Project Image 1</p>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button className="bg-blue-600 text-white px-3 py-1 rounded-md">Edit</button>
|
||||
<button className="bg-red-600 text-white px-3 py-1 rounded-md">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminGallery;
|
||||
97
src/pages/AdminHR.jsx
Normal file
97
src/pages/AdminHR.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const AdminHR = () => {
|
||||
const [submissions, setSubmissions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch HR submissions from API
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`${process.env.REACT_APP_API_BASE_URL}/api/hr-submissions`)
|
||||
.then((res) => {
|
||||
setSubmissions(res.data || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error fetching HR submissions:', err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDelete = (id) => {
|
||||
if (window.confirm('Are you sure you want to delete this submission?')) {
|
||||
axios
|
||||
.delete(`${process.env.REACT_APP_API_BASE_URL}/api/hr-submissions/${id}`)
|
||||
.then(() => {
|
||||
setSubmissions(submissions.filter((sub) => sub.id !== id));
|
||||
})
|
||||
.catch((err) => console.error('Error deleting submission:', err));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 py-16">
|
||||
<h2 className="text-3xl font-bold text-blue-900 mb-4">Admin - HR</h2>
|
||||
<p className="text-gray-600 mb-6">Manage HR contacts and career submissions.</p>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-gray-500">Loading submissions...</p>
|
||||
) : submissions.length === 0 ? (
|
||||
<p className="text-gray-500">No submissions found.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse border border-gray-200">
|
||||
<thead>
|
||||
<tr className="bg-blue-100 text-left">
|
||||
<th className="p-2 border">ID</th>
|
||||
<th className="p-2 border">Name</th>
|
||||
<th className="p-2 border">Email</th>
|
||||
<th className="p-2 border">Phone</th>
|
||||
<th className="p-2 border">Resume</th>
|
||||
<th className="p-2 border">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{submissions.map((sub, index) => (
|
||||
<tr key={sub.id || index} className="hover:bg-gray-50">
|
||||
<td className="p-2 border">{sub.id}</td>
|
||||
<td className="p-2 border">{sub.name}</td>
|
||||
<td className="p-2 border">{sub.email}</td>
|
||||
<td className="p-2 border">{sub.phone}</td>
|
||||
<td className="p-2 border">
|
||||
{sub.resume ? (
|
||||
<a href={sub.resume} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">
|
||||
View Resume
|
||||
</a>
|
||||
) : (
|
||||
'N/A'
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2 border flex space-x-2">
|
||||
<a
|
||||
href={sub.resume || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-green-600 text-white px-3 py-1 rounded-md"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
<button
|
||||
onClick={() => handleDelete(sub.id)}
|
||||
className="bg-red-600 text-white px-3 py-1 rounded-md"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHR;
|
||||
45
src/pages/AdminLogin.jsx
Normal file
45
src/pages/AdminLogin.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
export default function AdminLogin() {
|
||||
const [password, setPassword] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const from = location.state?.from?.pathname || "/admin"; // redirect after login
|
||||
|
||||
const handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (password === "Laxmi@#$2026") {
|
||||
sessionStorage.setItem("admin", "true"); // session-based login
|
||||
navigate(from, { replace: true });
|
||||
} else {
|
||||
alert("Wrong password");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<form
|
||||
onSubmit={handleLogin}
|
||||
className="bg-white p-8 rounded shadow-md w-full max-w-sm"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4">LCEPL ADMIN LOGIN</h2>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter LCEPL ADMIN password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded mb-4"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
src/pages/AdminPanel.jsx
Normal file
8
src/pages/AdminPanel.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
const AdminPanel = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default AdminPanel;
|
||||
38
src/pages/AdminProjects.jsx
Normal file
38
src/pages/AdminProjects.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
const AdminProjects = () => {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 py-16">
|
||||
<h2 className="text-3xl font-bold text-blue-900 mb-4">Admin - Projects</h2>
|
||||
<p className="text-gray-600 mb-6">Manage all your projects here.</p>
|
||||
|
||||
{/* Example: Projects Table */}
|
||||
<table className="w-full border-collapse border border-gray-200">
|
||||
<thead>
|
||||
<tr className="bg-blue-100 text-left">
|
||||
<th className="p-2 border">ID</th>
|
||||
<th className="p-2 border">Title</th>
|
||||
<th className="p-2 border">Location</th>
|
||||
<th className="p-2 border">Status</th>
|
||||
<th className="p-2 border">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* Replace with dynamic map from API */}
|
||||
<tr>
|
||||
<td className="p-2 border">1</td>
|
||||
<td className="p-2 border">Bridge Project</td>
|
||||
<td className="p-2 border">Mumbai</td>
|
||||
<td className="p-2 border">Ongoing</td>
|
||||
<td className="p-2 border">
|
||||
<button className="bg-blue-600 text-white px-3 py-1 rounded-md mr-2">Edit</button>
|
||||
<button className="bg-red-600 text-white px-3 py-1 rounded-md">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminProjects;
|
||||
414
src/pages/Apply.jsx
Normal file
414
src/pages/Apply.jsx
Normal file
@@ -0,0 +1,414 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { ApplicationContext } from '../context/ApplicationContext';
|
||||
|
||||
const Apply = () => {
|
||||
const { applications, setApplications } = useContext(ApplicationContext);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [credentials, setCredentials] = useState({ id: '', password: '' });
|
||||
const [formData, setFormData] = useState({
|
||||
positionName: '',
|
||||
qualification: '',
|
||||
experience: '',
|
||||
location: '',
|
||||
|
||||
expertise: '',
|
||||
skills: [],
|
||||
numberOfOpenings: '',
|
||||
jobDescription: '',
|
||||
postingDate: new Date().toISOString().split('T')[0],
|
||||
isActive: true,
|
||||
closingDate: '',
|
||||
});
|
||||
const [showApplications, setShowApplications] = useState(false);
|
||||
const [editIndex, setEditIndex] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`${process.env.REACT_APP_API_BASE_URL}/api/jobs`)
|
||||
.then((res) => {
|
||||
const currentDate = new Date();
|
||||
const updatedJobs = res.data.map(job => {
|
||||
if (job.closingDate && new Date(job.closingDate) < currentDate) {
|
||||
return { ...job, isActive: false };
|
||||
}
|
||||
return job;
|
||||
});
|
||||
setApplications(updatedJobs);
|
||||
})
|
||||
.catch((err) => console.error('Error fetching jobs:', err));
|
||||
}, [setApplications]);
|
||||
|
||||
const handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
if (credentials.id === 'HR' && credentials.password === 'HR@#$2026') {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
alert('Invalid credentials. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
...(name === 'positionName' ? { skills: [] } : {}),
|
||||
}));
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
positionName: '',
|
||||
qualification: '',
|
||||
experience: '',
|
||||
location: '',
|
||||
|
||||
expertise: '',
|
||||
skills: [],
|
||||
numberOfOpenings: '',
|
||||
jobDescription: '',
|
||||
postingDate: new Date().toISOString().split('T')[0],
|
||||
isActive: true,
|
||||
closingDate: '',
|
||||
});
|
||||
setEditIndex(null);
|
||||
};
|
||||
|
||||
const handleApplicationSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const currentDate = new Date();
|
||||
const closingDate = formData.closingDate ? new Date(formData.closingDate) : null;
|
||||
const isActive = closingDate ? closingDate >= currentDate : formData.isActive;
|
||||
|
||||
const jobData = {
|
||||
...formData,
|
||||
isActive
|
||||
};
|
||||
|
||||
if (editIndex !== null) {
|
||||
const jobId = applications[editIndex].id;
|
||||
axios
|
||||
.put(`${process.env.REACT_APP_API_BASE_URL}/api/jobs/${jobId}`, jobData)
|
||||
.then((res) => {
|
||||
const updatedApps = [...applications];
|
||||
updatedApps[editIndex] = res.data.job;
|
||||
setApplications(updatedApps);
|
||||
resetForm();
|
||||
})
|
||||
.catch((err) => console.error('Error updating job:', err));
|
||||
} else {
|
||||
axios
|
||||
.post(`${process.env.REACT_APP_API_BASE_URL}/api/jobs`, jobData)
|
||||
.then((res) => {
|
||||
setApplications([...applications, res.data.job]);
|
||||
resetForm();
|
||||
})
|
||||
.catch((err) => console.error('Error creating job:', err));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (index) => {
|
||||
const jobId = applications[index].id;
|
||||
if (window.confirm('Are you sure you want to delete this job posting?')) {
|
||||
axios
|
||||
.delete(`${process.env.REACT_APP_API_BASE_URL}/api/jobs/${jobId}`)
|
||||
.then(() => {
|
||||
const updated = [...applications];
|
||||
updated.splice(index, 1);
|
||||
setApplications(updated);
|
||||
})
|
||||
.catch((err) => console.error('Error deleting job:', err));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (index) => {
|
||||
setEditIndex(index);
|
||||
setFormData(applications[index]);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const toggleActiveStatus = (index) => {
|
||||
const job = applications[index];
|
||||
const updatedJob = { ...job, isActive: !job.isActive };
|
||||
|
||||
axios
|
||||
.put(`${process.env.REACT_APP_API_BASE_URL}/api/jobs/${job.id}`, updatedJob)
|
||||
.then((res) => {
|
||||
const updatedApps = [...applications];
|
||||
updatedApps[index] = res.data.job;
|
||||
setApplications(updatedApps);
|
||||
})
|
||||
.catch((err) => console.error('Error updating job status:', err));
|
||||
};
|
||||
|
||||
const filteredApplications = applications.filter((app) =>
|
||||
app.positionName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(Array.isArray(app.skills) && app.skills.some((skill) => skill.toLowerCase().includes(searchTerm.toLowerCase())))
|
||||
);
|
||||
|
||||
const handleLogout = () => {
|
||||
setIsAuthenticated(false);
|
||||
setCredentials({ id: '', password: '' });
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8 px-4 sm:px-6 lg:px-8">
|
||||
{!isAuthenticated ? (
|
||||
<div className="max-w-md mx-auto bg-white p-8 rounded-xl shadow-lg border border-gray-200">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800">HR Portal Login</h2>
|
||||
<p className="text-gray-600 mt-2">Enter your credentials to access the job application system</p>
|
||||
</div>
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="id" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="id"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
autoComplete="off"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
value={credentials.id}
|
||||
onChange={(e) => setCredentials({ ...credentials, id: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
autoComplete="new-password"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
value={credentials.password}
|
||||
onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Job Application Management</h1>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors text-sm font-medium"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Job Form */}
|
||||
<div className="bg-white shadow-lg rounded-lg overflow-hidden mb-8">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
{editIndex !== null ? 'Edit Job Posting' : 'Create New Job Posting'}
|
||||
</h2>
|
||||
</div>
|
||||
<form onSubmit={handleApplicationSubmit} className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
{/* Position */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Position *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="positionName"
|
||||
placeholder="Enter position name"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.positionName}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Qualification */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Qualification *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="qualification"
|
||||
placeholder="e.g. Bachelor's Degree"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.qualification}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Experience */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Experience *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="experience"
|
||||
placeholder="e.g. 3-5 years"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.experience}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Location *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
placeholder="e.g. New York, NY"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.location}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Number of Openings */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Number of Openings *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="numberOfOpenings"
|
||||
placeholder="e.g. 2"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.numberOfOpenings}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Posting Date */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Posting Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="postingDate"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.postingDate}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Closing Date */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Closing Date (optional)</label>
|
||||
<input
|
||||
type="date"
|
||||
name="closingDate"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.closingDate}
|
||||
onChange={handleFormChange}
|
||||
min={formData.postingDate}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
If set, position will automatically deactivate after this date
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Active Status */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
name="isActive"
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({...formData, isActive: e.target.checked})}
|
||||
disabled={!!formData.closingDate}
|
||||
/>
|
||||
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-700">
|
||||
Active Position
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Skills Multi-select */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Required Skills *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="skills"
|
||||
placeholder="Enter skills separated by commas (e.g. AutoCAD, Billing, Teamwork)"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
|
||||
value={formData.skills.join(", ")}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
skills: e.target.value.split(",").map((s) => s.trim())
|
||||
})
|
||||
}
|
||||
/>
|
||||
{formData.skills.length > 0 && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Selected: <span className="font-medium">{formData.skills.join(', ')}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Job Description */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Job Description *</label>
|
||||
<textarea
|
||||
name="jobDescription"
|
||||
placeholder="Detailed description of the job..."
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm h-32"
|
||||
value={formData.jobDescription}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Form Buttons */}
|
||||
<div className="md:col-span-2 flex justify-end space-x-3">
|
||||
{editIndex !== null && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{editIndex !== null ? 'Update Job Posting' : 'Create Job Posting'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Job Postings List */}
|
||||
{/* Keep the table as before, just remove Salary column if needed */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Apply;
|
||||
24
src/pages/BlogDetail.jsx
Normal file
24
src/pages/BlogDetail.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import blogs from "../data/blogs";
|
||||
|
||||
function BlogDetail() {
|
||||
const { slug } = useParams();
|
||||
const blog = blogs.find((b) => b.slug === slug);
|
||||
|
||||
if (!blog) return <h2>Blog not found</h2>;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Helmet>
|
||||
<title>{blog.title}</title>
|
||||
<meta name="description" content={blog.description} />
|
||||
</Helmet>
|
||||
|
||||
<h1>{blog.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: blog.content }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlogDetail;
|
||||
0
src/pages/BlogList.jsx
Normal file
0
src/pages/BlogList.jsx
Normal file
693
src/pages/Careers.jsx
Normal file
693
src/pages/Careers.jsx
Normal file
@@ -0,0 +1,693 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import careersBanner from '../assets/careers-banner.jpg';
|
||||
import careerImg from '../assets/career.jpg';
|
||||
|
||||
const Careers = () => {
|
||||
const navigate = useNavigate();
|
||||
const [jobs, setJobs] = useState([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||
const [showContactForm, setShowContactForm] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
resume: null,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [jobsLoading, setJobsLoading] = useState(true);
|
||||
const [jobsError, setJobsError] = useState(null);
|
||||
const [searchFilters, setSearchFilters] = useState({
|
||||
position: '',
|
||||
skills: '',
|
||||
location: ''
|
||||
});
|
||||
const [filteredJobs, setFilteredJobs] = useState([]);
|
||||
const [activeOnly, setActiveOnly] = useState(true);
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleShortcut = (e) => {
|
||||
// if (e.altKey && e.key.toLowerCase() === "n") {
|
||||
// navigate("/Apply");
|
||||
// }
|
||||
// };
|
||||
// document.addEventListener("keydown", handleShortcut);
|
||||
|
||||
// // Cleanup event listener on unmount
|
||||
// return () => {
|
||||
// document.removeEventListener("keydown", handleShortcut);
|
||||
// };
|
||||
// }, [navigate]);
|
||||
|
||||
// Fetch jobs from backend API
|
||||
useEffect(() => {
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
setJobsLoading(true);
|
||||
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/api/jobs`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs: ${response.status}`);
|
||||
}
|
||||
|
||||
const jobsData = await response.json();
|
||||
setJobs(jobsData);
|
||||
setJobsError(null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching jobs:", error);
|
||||
setJobsError("Failed to load job openings. Please try again later.");
|
||||
} finally {
|
||||
setJobsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchJobs();
|
||||
}, []);
|
||||
|
||||
// Filter jobs based on search criteria
|
||||
useEffect(() => {
|
||||
const filtered = jobs.filter(job => {
|
||||
// Check position filter
|
||||
const positionMatch = searchFilters.position === '' ||
|
||||
job.positionName?.toLowerCase().includes(searchFilters.position.toLowerCase());
|
||||
|
||||
// Check skills filter
|
||||
const skillsMatch = searchFilters.skills === '' ||
|
||||
(Array.isArray(job.skills) &&
|
||||
job.skills.some(skill =>
|
||||
skill.toLowerCase().includes(searchFilters.skills.toLowerCase())
|
||||
));
|
||||
|
||||
// Check location filter
|
||||
const locationMatch = searchFilters.location === '' ||
|
||||
job.location?.toLowerCase().includes(searchFilters.location.toLowerCase());
|
||||
|
||||
// Check active status
|
||||
const isActive = !job.closingDate || new Date(job.closingDate) >= new Date();
|
||||
|
||||
return positionMatch && skillsMatch && locationMatch &&
|
||||
(!activeOnly || (activeOnly && job.isActive && isActive));
|
||||
});
|
||||
|
||||
setFilteredJobs(filtered);
|
||||
}, [jobs, searchFilters, activeOnly]);
|
||||
|
||||
const getDaysAgo = (dateString) => {
|
||||
if (!dateString) return 'Recently';
|
||||
|
||||
const postedDate = new Date(dateString);
|
||||
const currentDate = new Date();
|
||||
const timeDiff = currentDate - postedDate;
|
||||
const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff === 0) return 'Today';
|
||||
if (daysDiff === 1) return '1 day ago';
|
||||
if (daysDiff < 7) return `${daysDiff} days ago`;
|
||||
if (daysDiff < 30) {
|
||||
const weeks = Math.floor(daysDiff / 7);
|
||||
return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;
|
||||
}
|
||||
if (daysDiff < 365) {
|
||||
const months = Math.floor(daysDiff / 30);
|
||||
return months === 1 ? '1 month ago' : `${months} months ago`;
|
||||
}
|
||||
const years = Math.floor(daysDiff / 365);
|
||||
return years === 1 ? '1 year ago' : `${years} years ago`;
|
||||
};
|
||||
|
||||
const handleToggle = (index) => {
|
||||
setSelectedIndex((prevIndex) => (prevIndex === index ? null : index));
|
||||
};
|
||||
|
||||
// const handleContactClick = () => {
|
||||
// setShowContactForm(!showContactForm);
|
||||
// };
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
resume: e.target.files[0] || null,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.resume) {
|
||||
alert("Please upload your resume.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = new FormData();
|
||||
data.append("fullName", formData.fullName);
|
||||
data.append("email", formData.email);
|
||||
data.append("phone", formData.phone);
|
||||
data.append("resume", formData.resume);
|
||||
|
||||
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/api/careers/contact`, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert(result.message || "Thank you for contacting our HR team. We will get back to you soon!");
|
||||
setFormData({
|
||||
fullName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
resume: null,
|
||||
});
|
||||
setShowContactForm(false);
|
||||
} else {
|
||||
alert(result.error || "Failed to submit the form. Please try again.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
alert("Server error. Please try again later.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const benefits = [
|
||||
{ title: "Competitive Compensation", icon: "💰", description: "Industry-leading salaries and performance bonuses" },
|
||||
{ title: "Professional Growth", icon: "📈", description: "Continuous learning opportunities and career advancement paths" },
|
||||
{ title: "Work-Life Balance", icon: "⚖️", description: "Flexible work arrangements and generous leave policies" },
|
||||
{ title: "Health & Wellness", icon: "🏥", description: "Comprehensive health insurance and wellness programs" },
|
||||
{ title: "Innovative Culture", icon: "💡", description: "Work with cutting-edge technologies and industry experts" },
|
||||
// { title: "Global Impact", icon: "🌍", description: "Opportunity to work on projects with international reach" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white text-gray-900 min-h-screen">
|
||||
{/* Hero Banner */}
|
||||
<div
|
||||
className="relative w-full h-[60vh] bg-center bg-cover flex items-center justify-center"
|
||||
style={{ backgroundImage: `url(${careersBanner})` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-900/80 to-blue-700/80 z-0" />
|
||||
<div className="relative z-10 text-center px-4 text-white max-w-4xl">
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold mb-4 drop-shadow-lg">
|
||||
Build Your Future With Us
|
||||
</h1>
|
||||
<p className="text-lg md:text-2xl font-medium drop-shadow mb-8">
|
||||
Shape India's infrastructure landscape while advancing your professional journey.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => document.getElementById('openings').scrollIntoView({ behavior: 'smooth' })}
|
||||
className="bg-white text-blue-900 px-8 py-3 rounded-md font-bold hover:bg-gray-100 transition shadow-lg"
|
||||
>
|
||||
Explore Opportunities
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Why Join Us Section */}
|
||||
<div className="bg-gray-50 py-16">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<h2 className="text-3xl font-bold text-blue-900 mb-2 text-center">
|
||||
Why Build Your Career With Us?
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center max-w-3xl mx-auto mb-12">
|
||||
We're committed to fostering an environment where professionals can thrive, innovate,
|
||||
and make meaningful contributions to India's infrastructure development.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{benefits.map((benefit, index) => (
|
||||
<div key={index} className="bg-white p-6 rounded-lg shadow-md hover:shadow-xl transition">
|
||||
<div className="text-4xl mb-4">{benefit.icon}</div>
|
||||
<h3 className="text-xl font-semibold text-blue-800 mb-2">{benefit.title}</h3>
|
||||
<p className="text-gray-600">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Our Culture Section */}
|
||||
<div className="py-16 bg-white">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="flex flex-col lg:flex-row gap-12 items-center">
|
||||
<div className="lg:w-1/2">
|
||||
<h2 className="text-3xl font-bold text-blue-900 mb-6">Our Professional Culture</h2>
|
||||
<p className="text-gray-700 mb-4 leading-relaxed">
|
||||
At our company, we cultivate a culture of excellence, collaboration, and continuous
|
||||
improvement. We believe that our people are our greatest asset, and we invest in their
|
||||
professional development.
|
||||
</p>
|
||||
<p className="text-gray-700 mb-6 leading-relaxed">
|
||||
Our teams work on challenging projects that push the boundaries of infrastructure
|
||||
development, while maintaining the highest standards of professional ethics and quality.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<span className="bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-medium">Innovation</span>
|
||||
<span className="bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-medium">Integrity</span>
|
||||
<span className="bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-medium">Excellence</span>
|
||||
<span className="bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-medium">Collaboration</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:w-1/2 bg-gray-100 rounded-xl overflow-hidden">
|
||||
<img
|
||||
src={careerImg}
|
||||
alt="Company Culture"
|
||||
className="w-full h-80 object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Openings Section */}
|
||||
<div id="openings" className="max-w-6xl mx-auto px-4 py-16">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-blue-900 mb-2">
|
||||
Current Professional Opportunities
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-2xl mx-auto">
|
||||
Explore our current openings and find the perfect position to advance your professional career.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loading state */}
|
||||
{jobsLoading && (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
|
||||
<p className="text-gray-600">Loading job openings...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{jobsError && (
|
||||
<div className="text-center py-12 bg-red-50 rounded-lg">
|
||||
<p className="text-red-600 mb-4">{jobsError}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jobs listing */}
|
||||
{!jobsLoading && !jobsError && (
|
||||
<>
|
||||
{/* Enhanced Search Filters */}
|
||||
<div className="mb-8 grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter by position..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
value={searchFilters.position}
|
||||
onChange={(e) => setSearchFilters({...searchFilters, position: e.target.value})}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter by skills..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
value={searchFilters.skills}
|
||||
onChange={(e) => setSearchFilters({...searchFilters, skills: e.target.value})}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter by location..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
value={searchFilters.location}
|
||||
onChange={(e) => setSearchFilters({...searchFilters, location: e.target.value})}
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
|
||||
<div className="md:col-span-3 flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="activeOnly"
|
||||
checked={activeOnly}
|
||||
onChange={() => setActiveOnly(!activeOnly)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="activeOnly" className="ml-2 text-sm text-gray-700">
|
||||
Show active positions only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{(searchFilters.position || searchFilters.skills || searchFilters.location) && (
|
||||
<button
|
||||
onClick={() => setSearchFilters({ position: '', skills: '', location: '' })}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium text-sm"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredJobs.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{filteredJobs.map((job, index) => {
|
||||
const closingDate = job.closingDate ? new Date(job.closingDate) : null;
|
||||
const isExpired = closingDate ? new Date() > closingDate : false;
|
||||
const actualStatus = isExpired ? false : job.isActive;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={job.id}
|
||||
className={`bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition p-6 relative overflow-hidden ${!actualStatus ? 'opacity-80' : ''}`}
|
||||
>
|
||||
<div className={`absolute top-0 left-0 w-1 h-full ${actualStatus ? 'bg-blue-700' : 'bg-gray-400'}`}></div>
|
||||
|
||||
<div className="absolute -left-6 top-6 text-5xl text-blue-100 font-extrabold opacity-30 select-none">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-between md:items-center mb-4 pl-4">
|
||||
<div>
|
||||
<h3 className="text-blue-900 font-bold text-xl md:text-2xl">
|
||||
{job.positionName}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
<span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-xs font-medium">
|
||||
{job.location}
|
||||
</span>
|
||||
<span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-xs font-medium">
|
||||
{job.experience}
|
||||
</span>
|
||||
<span className="bg-purple-100 text-purple-800 px-3 py-1 rounded-full text-xs font-medium">
|
||||
{getDaysAgo(job.postingDate)}
|
||||
</span>
|
||||
{!actualStatus && (
|
||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-xs font-medium">
|
||||
Position Closed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4 md:mt-0">
|
||||
<button
|
||||
onClick={() => handleToggle(index)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium shadow transition flex items-center gap-2 text-sm"
|
||||
>
|
||||
{selectedIndex === index ? (
|
||||
<>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
Hide
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
Details
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{actualStatus && (
|
||||
<button
|
||||
onClick={() => navigate('/job-application', { state: { jobId: job.id } })}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md font-medium shadow transition flex items-center gap-2 text-sm"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
Apply
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedIndex === index && (
|
||||
<div className="bg-gray-50 p-5 rounded-md border border-blue-100 mt-4 ml-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-semibold text-blue-800 mb-3">Position Overview</h4>
|
||||
<div className="space-y-4 text-sm text-gray-700">
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Qualification</p>
|
||||
<p>{job.qualification || 'Not specified'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Experience</p>
|
||||
<p>{job.experience || 'Not specified'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Location</p>
|
||||
<p>{job.location || 'Not specified'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Posted</p>
|
||||
<p>{getDaysAgo(job.postingDate)}</p>
|
||||
</div>
|
||||
{job.closingDate && (
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Closing Date</p>
|
||||
<p>{new Date(job.closingDate).toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-blue-800 mb-3">Position Details</h4>
|
||||
<div className="space-y-4 text-sm text-gray-700">
|
||||
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Openings</p>
|
||||
<p>{job.numberOfOpenings || '1'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-500">Key Skills</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{Array.isArray(job.skills) && job.skills.length > 0 ? (
|
||||
job.skills.map((skill, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">Not specified</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<h4 className="font-semibold text-blue-800 mb-3">Job Description</h4>
|
||||
<div className="prose prose-sm max-w-none text-gray-700">
|
||||
<p>{job.jobDescription || 'No description provided.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actualStatus && (
|
||||
<div className="flex justify-end mt-6">
|
||||
<button
|
||||
onClick={() => navigate('/job-application', { state: { jobId: job.id } })}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-8 py-3 rounded-md font-medium shadow-md transition flex items-center gap-2"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
Apply for this Position
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
!jobsLoading && (
|
||||
<div className="text-center py-12 bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-600">
|
||||
{jobs.length === 0
|
||||
? "No positions are currently available. Please check back later."
|
||||
: "No positions match your current filters."}
|
||||
</p>
|
||||
{(searchFilters.position || searchFilters.skills || searchFilters.location) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchFilters({ position: '', skills: '', location: '' });
|
||||
setActiveOnly(true);
|
||||
}}
|
||||
className="mt-4 text-blue-600 hover:text-blue-800 font-medium"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Call to Action - Contact HR Section (Kept exactly as before) */}
|
||||
<div className="bg-blue-900 text-white py-16">
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">Ready to Build Your Professional Future?</h2>
|
||||
<p className="text-blue-100 mb-8 text-lg">
|
||||
Even if you don't see the perfect role today, we're always interested in connecting with talented professionals.
|
||||
</p>
|
||||
{/* <div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<button
|
||||
onClick={handleContactClick}
|
||||
className="bg-white text-blue-900 px-8 py-3 rounded-md font-bold hover:bg-gray-100 transition shadow-lg"
|
||||
>
|
||||
Stay connected with us
|
||||
</button>
|
||||
</div> */}
|
||||
|
||||
|
||||
<div className="mt-8 bg-white rounded-lg shadow-xl p-6 max-w-2xl mx-auto text-left text-gray-800">
|
||||
<h3 className="text-2xl font-bold text-blue-900 mb-4">HR Contact Form</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="fullName" className="block text-gray-700 font-medium mb-2">
|
||||
Full Name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
value={formData.fullName}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="email" className="block text-gray-700 font-medium mb-2">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="phone" className="block text-gray-700 font-medium mb-2">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="resume" className="block text-gray-700 font-medium mb-2">
|
||||
Upload Resume (PDF) <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="resume"
|
||||
name="resume"
|
||||
onChange={handleFileChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
accept=".pdf,.doc,.docx"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
{/* <button
|
||||
type="button"
|
||||
onClick={handleContactClick}
|
||||
className="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-100 transition"
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button> */}
|
||||
<button
|
||||
type="submit"
|
||||
className={`px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition flex items-center justify-center gap-2 ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading && (
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v8H4z"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Careers;
|
||||
245
src/pages/ContactUs.jsx
Normal file
245
src/pages/ContactUs.jsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import React, { useState } from "react";
|
||||
import "../styles/ContactUs.css";
|
||||
import contactHeroImg from "../assets/contact-hero.jpg";
|
||||
import centerImg from "../assets/contact-center.jpg";
|
||||
|
||||
const ContactUs = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
contact: "",
|
||||
message: "",
|
||||
});
|
||||
|
||||
const [formErrors, setFormErrors] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
contact: "",
|
||||
message: "",
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notice, setNotice] = useState(null);
|
||||
|
||||
const API_BASE = (process.env.REACT_APP_API_BASE_URL || "").replace(/\/+$/, "");
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (formErrors[name]) {
|
||||
setFormErrors((prev) => ({ ...prev, [name]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const errors = {};
|
||||
let isValid = true;
|
||||
|
||||
// Name validation
|
||||
if (!formData.name.trim()) {
|
||||
errors.name = "Name is required";
|
||||
isValid = false;
|
||||
} else if (formData.name.trim().length < 2) {
|
||||
errors.name = "Name must be at least 2 characters";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
if (!formData.email.trim()) {
|
||||
errors.email = "Email is required";
|
||||
isValid = false;
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
errors.email = "Please enter a valid email address";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Contact validation
|
||||
const digits = (formData.contact || "").replace(/\D/g, "");
|
||||
if (!formData.contact.trim()) {
|
||||
errors.contact = "Contact number is required";
|
||||
isValid = false;
|
||||
} else if (digits.length < 7) {
|
||||
errors.contact = "Please enter a valid contact number (at least 7 digits)";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Message validation
|
||||
if (!formData.message.trim()) {
|
||||
errors.message = "Message is required";
|
||||
isValid = false;
|
||||
} else if (formData.message.trim().length < 10) {
|
||||
errors.message = "Message must be at least 10 characters";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setNotice(null);
|
||||
|
||||
if (!validateForm()) {
|
||||
setNotice({ type: "error", text: "Please correct the errors in the form." });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 20000);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/contact`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
if (res.ok) {
|
||||
setNotice({ type: "success", text: data.message || "Message sent successfully! We'll get back to you soon." });
|
||||
setFormData({ name: "", email: "", contact: "", message: "" });
|
||||
} else {
|
||||
setNotice({
|
||||
type: "error",
|
||||
text: data?.error || `Something went wrong (HTTP ${res.status}). Please try again.`,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") {
|
||||
setNotice({ type: "error", text: "Request timed out. Please try again." });
|
||||
} else {
|
||||
setNotice({ type: "error", text: "Network error. Please check your connection and try again." });
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="contact-page">
|
||||
<div
|
||||
className="contact-hero"
|
||||
style={{ backgroundImage: `url(${contactHeroImg})` }}
|
||||
role="img"
|
||||
aria-label="Contact page hero"
|
||||
>
|
||||
<div className="contact-overlay">
|
||||
<h1>CONTACT US</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-heading">
|
||||
<h2>LET'S RISE TOGETHER</h2>
|
||||
<p className="subheading">We have the highest of ambitions and are here to support you rise higher!</p>
|
||||
<p className="description">
|
||||
Whether you are a client looking for a solution or an associate looking for a partner, feel free to connect
|
||||
with us.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="contact-content">
|
||||
<div className="contact-info">
|
||||
<h3>MEET US</h3>
|
||||
<hr />
|
||||
<div className="office-block">
|
||||
<h4>KOLHAPUR OFFICE</h4>
|
||||
<p>
|
||||
Laxmi Civil Engineering Services Pvt. Ltd.<br />
|
||||
1148, E. Sykes Extension, Kolhapur 416 001, Maharashtra, India.
|
||||
</p>
|
||||
<p>Phone No: 0231-2683900</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-center-image">
|
||||
<img src={centerImg} alt="Office" />
|
||||
</div>
|
||||
|
||||
<div className="contact-form">
|
||||
<h3>SHARE YOUR THOUGHTS AND WE'LL CONTACT YOU!</h3>
|
||||
|
||||
{notice && (
|
||||
<div className={`notice ${notice.type === "success" ? "notice-success" : "notice-error"}`} role="alert">
|
||||
{notice.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name*"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
autoComplete="name"
|
||||
disabled={loading}
|
||||
className={formErrors.name ? "error" : ""}
|
||||
/>
|
||||
{formErrors.name && <span className="error-text">{formErrors.name}</span>}
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email*"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
autoComplete="email"
|
||||
disabled={loading}
|
||||
className={formErrors.email ? "error" : ""}
|
||||
/>
|
||||
{formErrors.email && <span className="error-text">{formErrors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
name="contact"
|
||||
placeholder="Contact*"
|
||||
value={formData.contact}
|
||||
onChange={handleChange}
|
||||
required
|
||||
autoComplete="tel"
|
||||
inputMode="tel"
|
||||
disabled={loading}
|
||||
className={formErrors.contact ? "error" : ""}
|
||||
/>
|
||||
{formErrors.contact && <span className="error-text">{formErrors.contact}</span>}
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Message*"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
required
|
||||
rows={5}
|
||||
disabled={loading}
|
||||
className={formErrors.message ? "error" : ""}
|
||||
/>
|
||||
{formErrors.message && <span className="error-text">{formErrors.message}</span>}
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? "SENDING..." : "SUBMIT"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactUs;
|
||||
240
src/pages/Gallery.jsx
Normal file
240
src/pages/Gallery.jsx
Normal file
@@ -0,0 +1,240 @@
|
||||
// src/pages/Gallery.jsx
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import heroBg from "../assets/awards-hero.jpg";
|
||||
import { FiChevronLeft, FiChevronRight, FiX } from "react-icons/fi";
|
||||
import "../styles/Gallery.css";
|
||||
|
||||
const Gallery = () => {
|
||||
const [activeCategory, setActiveCategory] = useState("Awards & Achievements");
|
||||
const [galleryItems, setGalleryItems] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const categoryRef = useRef(null);
|
||||
|
||||
const categories = [
|
||||
{ label: "Awards & Achievements", value: "Awards & Achievements" },
|
||||
{ label: "Client Testimonials", value: "Client Testimonials" },
|
||||
];
|
||||
|
||||
// Disable right-click
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (e) => e.preventDefault();
|
||||
document.addEventListener("contextmenu", handleContextMenu);
|
||||
return () => document.removeEventListener("contextmenu", handleContextMenu);
|
||||
}, []);
|
||||
|
||||
// Fetch gallery items
|
||||
useEffect(() => {
|
||||
const fetchGalleryItems = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_API_BASE_URL}/api/gallery`);
|
||||
setGalleryItems(Array.isArray(response.data) ? response.data : []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching gallery items:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchGalleryItems();
|
||||
}, []);
|
||||
|
||||
// Scroll to categories if hash exists
|
||||
useEffect(() => {
|
||||
if (window.location.hash === "#categories" && categoryRef.current) {
|
||||
setTimeout(() => {
|
||||
categoryRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}, 200);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredItems = galleryItems.filter(
|
||||
(item) => item.category === activeCategory
|
||||
);
|
||||
|
||||
const openLightbox = (item, index) => {
|
||||
setSelectedItem(item);
|
||||
setCurrentIndex(index);
|
||||
};
|
||||
|
||||
const navigateLightbox = (direction) => {
|
||||
const items = filteredItems;
|
||||
let newIndex;
|
||||
if (direction === "prev") {
|
||||
newIndex = (currentIndex - 1 + items.length) % items.length;
|
||||
} else {
|
||||
newIndex = (currentIndex + 1) % items.length;
|
||||
}
|
||||
setSelectedItem(items[newIndex]);
|
||||
setCurrentIndex(newIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className="relative bg-cover bg-center h-[60vh] flex items-center justify-center"
|
||||
style={{ backgroundImage: `url(${heroBg})` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
<div className="relative z-10 text-center px-4 animate-fadeIn">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-white drop-shadow-lg">
|
||||
Our Awards
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl md:text-2xl text-white mt-2 italic drop-shadow-md">
|
||||
Celebrating Excellence & Achievements
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Buttons */}
|
||||
<div
|
||||
ref={categoryRef}
|
||||
className="flex flex-wrap justify-center gap-4 px-4 mt-8 mb-10"
|
||||
>
|
||||
{categories.map((category) => {
|
||||
const isSelected = activeCategory === category.value;
|
||||
return (
|
||||
<button
|
||||
key={category.value}
|
||||
onClick={() => setActiveCategory(category.value)}
|
||||
className={`text-base lg:text-lg text-white font-semibold px-6 py-3 rounded-2xl shadow-lg transition-all duration-300 transform hover:scale-105
|
||||
${isSelected ? "bg-gradient-to-r from-blue-600 to-blue-800 ring-4 ring-offset-2 ring-white" : "bg-gradient-to-r from-gray-600 to-gray-800"}`}
|
||||
>
|
||||
{category.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Gallery Grid */}
|
||||
<div className="px-6">
|
||||
{loading ? (
|
||||
<p className="text-gray-500 text-xl text-center">Loading gallery items...</p>
|
||||
) : filteredItems.length === 0 ? (
|
||||
<p className="text-gray-600 text-xl text-center">No items available in this category.</p>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-3 sm:grid-cols-2 gap-6">
|
||||
{filteredItems.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="rounded-xl shadow-lg overflow-hidden border bg-white hover:shadow-2xl transition-all duration-300"
|
||||
>
|
||||
{item.type === "image" ? (
|
||||
<img
|
||||
src={`${process.env.REACT_APP_API_BASE_URL}${item.url}`}
|
||||
alt={item.caption || "Gallery Image"}
|
||||
className="h-64 w-full object-cover cursor-pointer hover:scale-105 transition-transform duration-300"
|
||||
onClick={() => openLightbox(item, index)}
|
||||
draggable="false"
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
className="h-64 w-full object-cover cursor-pointer"
|
||||
onClick={() => openLightbox(item, index)}
|
||||
controls={false}
|
||||
>
|
||||
<source src={`${process.env.REACT_APP_API_BASE_URL}${item.url}`} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
<div className="p-5 text-left">
|
||||
<p className="text-sm text-gray-700 leading-relaxed">
|
||||
{item.caption || "No caption available"}
|
||||
</p>
|
||||
{item.date && (
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
{new Date(item.date).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lightbox Overlay */}
|
||||
{selectedItem && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
>
|
||||
{/* Close */}
|
||||
<button
|
||||
className="absolute top-4 right-4 text-white text-3xl"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItem(null);
|
||||
}}
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
|
||||
{/* Previous */}
|
||||
<button
|
||||
className="absolute left-4 text-white text-3xl p-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateLightbox("prev");
|
||||
}}
|
||||
>
|
||||
<FiChevronLeft size={32} />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-4xl w-full" onClick={(e) => e.stopPropagation()}>
|
||||
{selectedItem.type === "image" ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={`${process.env.REACT_APP_API_BASE_URL}${selectedItem.url}`}
|
||||
alt={selectedItem.caption || "Gallery Image"}
|
||||
className="max-w-full max-h-[80vh] mx-auto rounded-lg shadow-lg"
|
||||
draggable="false"
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
/>
|
||||
{/* Watermark */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="text-2xl font-bold text-white opacity-30" style={{
|
||||
transform: 'rotate(-45deg)',
|
||||
textShadow: '2px 2px 4px rgba(0,0,0,0.5)'
|
||||
}}>
|
||||
SAMPLE WATERMARK
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<video
|
||||
controls
|
||||
autoPlay
|
||||
className="max-w-full max-h-[80vh] mx-auto rounded-lg shadow-lg"
|
||||
>
|
||||
<source src={`${process.env.REACT_APP_API_BASE_URL}${selectedItem.url}`} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
<div className="text-center mt-4 text-white">
|
||||
<p className="text-lg">{selectedItem.caption}</p>
|
||||
{selectedItem.date && (
|
||||
<p className="text-sm text-gray-300">{new Date(selectedItem.date).toLocaleDateString()}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Next */}
|
||||
<button
|
||||
className="absolute right-4 text-white text-3xl p-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateLightbox("next");
|
||||
}}
|
||||
>
|
||||
<FiChevronRight size={32} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Gallery;
|
||||
320
src/pages/GalleryAdmin.jsx
Normal file
320
src/pages/GalleryAdmin.jsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import { FiX, FiEdit2, FiTrash2 } from "react-icons/fi";
|
||||
|
||||
const GalleryAdmin = () => {
|
||||
const [galleryItems, setGalleryItems] = useState([]);
|
||||
const [category, setCategory] = useState("Awards & Achievements");
|
||||
const [caption, setCaption] = useState("");
|
||||
const [date, setDate] = useState("");
|
||||
const [media, setMedia] = useState(null);
|
||||
const [mediaPreview, setMediaPreview] = useState(null);
|
||||
const [mediaType, setMediaType] = useState("image");
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editItemId, setEditItemId] = useState(null);
|
||||
|
||||
const categories = [
|
||||
{ label: "Awards & Achievements", value: "Awards & Achievements" },
|
||||
{ label: "Client Testimonials", value: "Client Testimonials" },
|
||||
];
|
||||
|
||||
// Fetch all gallery items
|
||||
const fetchGalleryItems = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_API_BASE_URL}/api/gallery`);
|
||||
setGalleryItems(res.data);
|
||||
} catch (err) {
|
||||
console.error("Error fetching gallery items", err);
|
||||
toast.error("Failed to load gallery items");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchGalleryItems();
|
||||
}, []);
|
||||
|
||||
// Reset the form
|
||||
const resetForm = () => {
|
||||
setCategory("Awards & Achievements");
|
||||
setCaption("");
|
||||
setDate("");
|
||||
setMedia(null);
|
||||
setMediaPreview(null);
|
||||
setMediaType("image");
|
||||
setEditMode(false);
|
||||
setEditItemId(null);
|
||||
};
|
||||
|
||||
// Handle media selection
|
||||
const handleMediaChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setMedia(file);
|
||||
|
||||
// Check if file is video
|
||||
if (file.type.includes("video")) {
|
||||
setMediaType("video");
|
||||
setMediaPreview(URL.createObjectURL(file));
|
||||
} else {
|
||||
setMediaType("image");
|
||||
setMediaPreview(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submit (add or update)
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!category || (!editMode && !media)) {
|
||||
toast.error("Category and media file are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("category", category);
|
||||
formData.append("caption", caption);
|
||||
formData.append("date", date);
|
||||
formData.append("type", mediaType);
|
||||
if (media) formData.append("media", media);
|
||||
|
||||
try {
|
||||
if (editMode) {
|
||||
await axios.put(
|
||||
`${process.env.REACT_APP_API_BASE_URL}/api/gallery/${editItemId}`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
toast.success("Gallery item updated successfully!");
|
||||
} else {
|
||||
await axios.post(
|
||||
`${process.env.REACT_APP_API_BASE_URL}/api/gallery`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
toast.success("Gallery item added successfully!");
|
||||
}
|
||||
|
||||
resetForm();
|
||||
fetchGalleryItems();
|
||||
} catch (err) {
|
||||
console.error("Failed to save gallery item", err);
|
||||
toast.error("Error saving gallery item");
|
||||
}
|
||||
};
|
||||
|
||||
// Load item into form for editing
|
||||
const handleEdit = (item) => {
|
||||
setCategory(item.category);
|
||||
setCaption(item.caption);
|
||||
setDate(item.date || "");
|
||||
setMedia(null);
|
||||
setMediaType(item.type);
|
||||
setMediaPreview(`${process.env.REACT_APP_API_BASE_URL}${item.url}`);
|
||||
setEditMode(true);
|
||||
setEditItemId(item.id);
|
||||
};
|
||||
|
||||
// Delete an item
|
||||
const handleDelete = async (id) => {
|
||||
if (!window.confirm("Are you sure you want to delete this item?")) return;
|
||||
|
||||
try {
|
||||
await axios.delete(`${process.env.REACT_APP_API_BASE_URL}/api/gallery/${id}`);
|
||||
toast.success("Item deleted");
|
||||
fetchGalleryItems();
|
||||
} catch (err) {
|
||||
console.error("Failed to delete", err);
|
||||
toast.error("Failed to delete item");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 min-h-screen bg-gray-50">
|
||||
<h1 className="text-3xl font-bold text-blue-900 mb-6">
|
||||
{editMode ? "Edit Gallery Item" : "Add New Gallery Item"}
|
||||
</h1>
|
||||
|
||||
{/* FORM */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4 max-w-md mx-auto bg-white p-6 rounded-lg shadow">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="w-full border p-2 rounded"
|
||||
required
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat.value} value={cat.value}>
|
||||
{cat.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Media Type</label>
|
||||
<select
|
||||
value={mediaType}
|
||||
onChange={(e) => setMediaType(e.target.value)}
|
||||
className="w-full border p-2 rounded"
|
||||
disabled={editMode && !media} // Disable if editing and no new media selected
|
||||
>
|
||||
<option value="image">Image</option>
|
||||
<option value="video">Video</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{mediaType === "image" ? "Image" : "Video"} Upload
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept={mediaType === "image" ? "image/*" : "video/*"}
|
||||
onChange={handleMediaChange}
|
||||
className="w-full"
|
||||
required={!editMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mediaPreview && (
|
||||
<div className="relative">
|
||||
{mediaType === "image" ? (
|
||||
<img
|
||||
src={mediaPreview}
|
||||
alt="Preview"
|
||||
className="h-48 w-full object-contain rounded border"
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
controls
|
||||
className="h-48 w-full object-contain rounded border"
|
||||
>
|
||||
<source src={mediaPreview} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMedia(null);
|
||||
setMediaPreview(null);
|
||||
}}
|
||||
className="absolute top-2 right-2 bg-black bg-opacity-50 text-white p-1 rounded-full"
|
||||
>
|
||||
<FiX size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Caption</label>
|
||||
<input
|
||||
type="text"
|
||||
value={caption}
|
||||
onChange={(e) => setCaption(e.target.value)}
|
||||
className="w-full border p-2 rounded"
|
||||
placeholder="Enter caption"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Date (optional)</label>
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
className="w-full border p-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-blue-900 text-white py-2 rounded hover:bg-blue-800"
|
||||
>
|
||||
{editMode ? "Update Item" : "Save Item"}
|
||||
</button>
|
||||
{editMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
className="flex-1 bg-gray-300 text-black py-2 rounded hover:bg-gray-400"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* GALLERY ITEMS LIST */}
|
||||
<div className="mt-10">
|
||||
<h2 className="text-2xl font-semibold text-blue-800 mb-4">All Gallery Items</h2>
|
||||
<div className="grid md:grid-cols-3 sm:grid-cols-2 gap-6">
|
||||
{galleryItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white p-4 rounded-xl shadow hover:shadow-lg transition-all relative"
|
||||
>
|
||||
{item.type === "image" ? (
|
||||
<img
|
||||
src={`${process.env.REACT_APP_API_BASE_URL}${item.url}`}
|
||||
alt={item.caption || "Gallery item"}
|
||||
className="h-56 w-full object-cover rounded mb-3"
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
className="h-56 w-full object-cover rounded mb-3"
|
||||
controls
|
||||
>
|
||||
<source src={`${process.env.REACT_APP_API_BASE_URL}${item.url}`} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">
|
||||
<span className="font-semibold">Category:</span> {item.category}
|
||||
</p>
|
||||
{item.caption && (
|
||||
<p className="text-sm text-gray-600 mb-1 line-clamp-2">
|
||||
<span className="font-semibold">Caption:</span> {item.caption}
|
||||
</p>
|
||||
)}
|
||||
{item.date && (
|
||||
<p className="text-xs text-gray-500">
|
||||
{new Date(item.date).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between mt-3">
|
||||
<button
|
||||
onClick={() => handleEdit(item)}
|
||||
className="flex items-center text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<FiEdit2 className="mr-1" /> Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.id)}
|
||||
className="flex items-center text-red-600 hover:text-red-800"
|
||||
>
|
||||
<FiTrash2 className="mr-1" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryAdmin;
|
||||
14
src/pages/HRAdmin.jsx
Normal file
14
src/pages/HRAdmin.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// src/pages/HRAdmin.jsx
|
||||
import React from "react";
|
||||
import Apply from "./Apply"; // reuse your Apply form component
|
||||
|
||||
const HRAdmin = () => {
|
||||
return (
|
||||
<div className="min-h-screen p-6 bg-gray-50">
|
||||
<h1 className="text-3xl font-bold mb-6 text-blue-900">HR Applications</h1>
|
||||
<Apply />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HRAdmin;
|
||||
300
src/pages/Home.jsx
Normal file
300
src/pages/Home.jsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import videoSrc from '../assets/video.mp4';
|
||||
import SectorsSlider from '../components/SectorsSlider';
|
||||
import CounterCard from '../components/CounterCard';
|
||||
import '../styles/Home.css';
|
||||
import IndiaMap from "../components/IndiaMap";
|
||||
import ClientsSection from '../components/ClientsSection';
|
||||
import '../styles/CounterCard.css';
|
||||
|
||||
// Animation variants
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.3,
|
||||
when: "beforeChildren"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: "easeOut"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fadeIn = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.8 }
|
||||
}
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div className="w-full bg-gradient-to-b from-blue-50 via-gray-50 to-white">
|
||||
{/* Hero Video Section */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={fadeIn}
|
||||
className="relative w-full h-[60vh] overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-900/40 to-blue-600/30 z-10"></div>
|
||||
<video
|
||||
className="absolute top-0 left-0 w-full h-full object-cover z-0"
|
||||
src={videoSrc}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
className="absolute top-0 left-0 w-full h-full z-20 flex items-center justify-center overflow-visible"
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="text-center text-white px-4 max-w-4xl"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl lg:text-5xl font-extrabold mb-6 drop-shadow-lg leading-snug md:leading-normal bg-clip-text text-transparent bg-gradient-to-r from-white to-blue-100">
|
||||
Building the Future Restoring the Past
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl lg:text-2xl font-medium drop-shadow max-w-2xl mx-auto bg-blue-900/30 backdrop-blur-sm rounded-lg p-4">
|
||||
CONSTRUCTING WITH EXCELLENCE. DELIVERING PRIDE FOR GENERATIONS
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
</motion.div>
|
||||
|
||||
{/* Floating particles background */}
|
||||
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute rounded-full bg-blue-200/30"
|
||||
initial={{
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
width: Math.random() * 10 + 5,
|
||||
height: Math.random() * 10 + 5,
|
||||
}}
|
||||
animate={{
|
||||
y: [0, Math.random() * 100 - 50],
|
||||
x: [0, Math.random() * 100 - 50],
|
||||
transition: {
|
||||
duration: Math.random() * 10 + 10,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
ease: "easeInOut",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sector Slider */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative py-12 bg-gradient-to-br from-blue-50 to-blue-100"
|
||||
>
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCIgdmlld0JveD0iMCAwIDYwIDYwIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiMxRTg0RkYiIGZpbGwtb3BhY2l0eT0iMC4yIj48cGF0aCBkPSJNMzYgMzRjMC0yLjIgMS44LTQgNC00czQgMS44IDQgNC0xLjggNC00IDQtNC0xLjgtNC00eiIvPjwvZz48L2c+PC9zdmc+')]"></div>
|
||||
</div>
|
||||
<SectorsSlider />
|
||||
</motion.div>
|
||||
|
||||
{/* About Section */}
|
||||
<motion.section
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
variants={containerVariants}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
className="relative py-16 px-4 md:px-8 lg:px-20 bg-white rounded-t-3xl shadow-xl overflow-hidden"
|
||||
>
|
||||
{/* Decorative corner */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-600/5 clip-path-polygon-[0_0,_100%_0,_100%_100%]"></div>
|
||||
|
||||
|
||||
</motion.section>
|
||||
{/* Vision & Mission Section */}
|
||||
<motion.section
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative py-20 px-4 md:px-8 lg:px-20 bg-gradient-to-br from-blue-900 to-blue-800 overflow-hidden"
|
||||
>
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-0 left-0 w-full h-full opacity-10">
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCIgdmlld0JveD0iMCAwIDYwIDYwIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiNGRkYiIGZpbGwtb3BhY2l0eT0iMC4yIj48cGF0aCBkPSJNMzYgMzRjMC0yLjIgMS44LTQgNC00czQgMS44IDQgNC0xLjggNC00IDQtNC0xLjgtNC00eiIvPjwvZz48L2c+PC9zdmc+')]"></div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ y: 30, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="max-w-7xl mx-auto relative z-10"
|
||||
>
|
||||
<h2 className="text-4xl font-bold text-center text-white mb-16 relative pb-2">
|
||||
<span className="relative inline-block">
|
||||
Vision & Mission
|
||||
<motion.span
|
||||
initial={{ scaleX: 0 }}
|
||||
whileInView={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="absolute bottom-0 left-0 w-full h-1 bg-red-500 z-0 transform origin-center"
|
||||
/>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-16">
|
||||
{/* Vision */}
|
||||
<motion.div
|
||||
initial={{ x: -50, opacity: 0 }}
|
||||
whileInView={{ x: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white p-8 rounded-xl shadow-lg hover:shadow-xl transition-all border-l-4 border-blue-500 hover:border-blue-400"
|
||||
>
|
||||
<div className="flex items-start mb-6">
|
||||
<div className="bg-blue-100 p-3 rounded-full mr-4">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-blue-900 mb-2">Vision</h3>
|
||||
<p className="text-gray-700">
|
||||
To be a company at the forefront of engineering and construction, renowned for excellence, quality, performance, and reliability.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Mission */}
|
||||
<motion.div
|
||||
initial={{ x: 50, opacity: 0 }}
|
||||
whileInView={{ x: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white p-8 rounded-xl shadow-lg hover:shadow-xl transition-all border-l-4 border-red-500 hover:border-red-400"
|
||||
>
|
||||
<div className="flex items-start mb-6">
|
||||
<div className="bg-blue-100 p-3 rounded-full mr-4">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-blue-900 mb-2">Mission</h3>
|
||||
<p className="text-gray-700">
|
||||
To complete every project undertaken with sincerity, excellence, and in a time-bound manner, meeting all expectations of the client.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.section>
|
||||
|
||||
{/* Clients Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative py-20 bg-gradient-to-br from-white to-blue-50 overflow-hidden"
|
||||
>
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute -top-20 -right-20 w-64 h-64 rounded-full bg-blue-200/20"></div>
|
||||
<div className="absolute -bottom-20 -left-20 w-80 h-80 rounded-full bg-blue-300/10"></div>
|
||||
|
||||
<ClientsSection />
|
||||
</motion.div>
|
||||
|
||||
{/* Map Section */}
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative py-20 px-4 md:px-8 lg:px-20 text-white"
|
||||
>
|
||||
{/* Animated background */}
|
||||
<div className="absolute inset-0 overflow-hidden opacity-10">
|
||||
{[...Array(10)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute rounded-full bg-white"
|
||||
initial={{
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
width: Math.random() * 20 + 5,
|
||||
height: Math.random() * 20 + 5,
|
||||
opacity: Math.random() * 0.3,
|
||||
}}
|
||||
animate={{
|
||||
y: [0, Math.random() * 100 - 50],
|
||||
x: [0, Math.random() * 100 - 50],
|
||||
transition: {
|
||||
duration: Math.random() * 15 + 15,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
ease: "easeInOut",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto text-center relative z-10">
|
||||
<h2 className="text-4xl font-bold mb-4 relative inline-block text-blue-900">
|
||||
<span className="relative z-10">
|
||||
Our Presence Across India
|
||||
<motion.span
|
||||
initial={{ scaleX: 0 }}
|
||||
whileInView={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="absolute bottom-0 left-0 w-full h-1 bg-red-500 z-0 transform origin-center"
|
||||
/>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
|
||||
We have successfully delivered projects in multiple states across India, with a growing national footprint.
|
||||
</p>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white/10 backdrop-blur-sm p-6 rounded-xl shadow-lg border border-blue-400/30"
|
||||
>
|
||||
<IndiaMap />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
326
src/pages/JobApplicationForm.jsx
Normal file
326
src/pages/JobApplicationForm.jsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const JobApplicationForm = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
education: '',
|
||||
skill: '',
|
||||
interest: '',
|
||||
totalExperience: '',
|
||||
expectedSalary: '',
|
||||
currentCompany: '',
|
||||
currentDesignation: '',
|
||||
preferredLocation: '', // new field
|
||||
infrastructureExpertise: '', // new field
|
||||
resume: null,
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, files } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: type === 'file' ? files[0] : value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setSubmitSuccess(false);
|
||||
|
||||
const form = new FormData();
|
||||
for (let key in formData) {
|
||||
form.append(key, formData[key]);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/send-application`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setSubmitSuccess(true); // show popup
|
||||
setFormData({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
education: '',
|
||||
skill: '',
|
||||
interest: '',
|
||||
totalExperience: '',
|
||||
expectedSalary: '',
|
||||
currentCompany: '',
|
||||
currentDesignation: '',
|
||||
preferredLocation: '',
|
||||
infrastructureExpertise: '',
|
||||
resume: null,
|
||||
});
|
||||
} else {
|
||||
alert(result.message || 'Error submitting application');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Submission error:', err);
|
||||
alert('Error submitting application. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-close success popup after 2 seconds
|
||||
useEffect(() => {
|
||||
if (submitSuccess) {
|
||||
const timer = setTimeout(() => setSubmitSuccess(false), 2000);
|
||||
return () => clearTimeout(timer); // cleanup
|
||||
}
|
||||
}, [submitSuccess]);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 py-12">
|
||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<div className="bg-blue-800 text-white p-6">
|
||||
<h2 className="text-2xl font-bold">Job Application Form</h2>
|
||||
<p className="text-blue-100">Please fill out the form below to apply for the position</p>
|
||||
</div>
|
||||
|
||||
{/* Success Popup */}
|
||||
{submitSuccess && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-30">
|
||||
<div
|
||||
className="bg-green-100 border-l-4 border-green-500 text-green-700 p-6 rounded shadow-lg max-w-md w-full animate-slide-in"
|
||||
style={{ animation: 'slideIn 0.5s ease-out' }}
|
||||
>
|
||||
<p className="font-semibold text-lg">Application Submitted Successfully!</p>
|
||||
<p className="mt-2">
|
||||
Thank you for your application. We will review your information and contact you soon.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-6" encType="multipart/form-data">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Personal Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 border-b pb-2">Personal Information</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Full Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
value={formData.fullName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Phone *</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Address</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Area of Expertise (Infrastructure)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="infrastructureExpertise"
|
||||
value={formData.infrastructureExpertise}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Your expertise in infrastructure field"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Professional Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 border-b pb-2">Professional Information</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Current Company</label>
|
||||
<input
|
||||
type="text"
|
||||
name="currentCompany"
|
||||
value={formData.currentCompany}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Current Designation</label>
|
||||
<input
|
||||
type="text"
|
||||
name="currentDesignation"
|
||||
value={formData.currentDesignation}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Total Experience (Years) *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="totalExperience"
|
||||
value={formData.totalExperience}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Expected Salary</label>
|
||||
<input
|
||||
type="text"
|
||||
name="expectedSalary"
|
||||
value={formData.expectedSalary}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Preferred Location</label>
|
||||
<input
|
||||
type="text"
|
||||
name="preferredLocation"
|
||||
value={formData.preferredLocation}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="City / Region you prefer to work in"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Education and Skills */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 border-b pb-2">Qualifications</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Education *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="education"
|
||||
value={formData.education}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Degrees, Certifications, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Skills *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="skill"
|
||||
value={formData.skill}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="List your key skills separated by commas"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Professional Interests</label>
|
||||
<input
|
||||
type="text"
|
||||
name="interest"
|
||||
value={formData.interest}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Areas of professional interest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume Upload */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800 border-b pb-2">Resume</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Upload Resume *</label>
|
||||
<div className="mt-1 flex items-center">
|
||||
<label className="cursor-pointer bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Choose File
|
||||
<input
|
||||
type="file"
|
||||
name="resume"
|
||||
onChange={handleChange}
|
||||
accept=".pdf,.doc,.docx"
|
||||
required
|
||||
className="sr-only"
|
||||
/>
|
||||
</label>
|
||||
<span className="ml-4 text-sm text-gray-500">
|
||||
{formData.resume ? formData.resume.name : "No file chosen"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">PDF, DOC, DOCX (Max. 5MB)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Submission */}
|
||||
<div className="pt-4 border-t">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={`px-6 py-3 rounded-md text-white font-medium ${isSubmitting ? 'bg-blue-400' : 'bg-blue-600 hover:bg-blue-700'} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors`}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Application'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobApplicationForm;
|
||||
181
src/pages/Office.jsx
Normal file
181
src/pages/Office.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
const Office = () => {
|
||||
const offices = {
|
||||
"Registered Office": [
|
||||
{
|
||||
address: "1148/E, Sykes Extension, Kolhapur - 416001, Maharashtra, India.",
|
||||
},
|
||||
],
|
||||
"Corporate Office": [
|
||||
{
|
||||
address:
|
||||
"C.S.NO. 1031/K2, E Ward, 3rd Floor, Sterling Tower, Gavat Mandai Road, Kolhapur - 416001, Maharashtra, India.",
|
||||
phone: "+91 231 2686910/11, 2529199",
|
||||
fax: "+91 231 2668199",
|
||||
email: "career@lcepl.com",
|
||||
},
|
||||
],
|
||||
"Regional Offices": {
|
||||
Maharashtra: {
|
||||
Mumbai: "141 & 142, 14th Floor, Mittal Court, 224 Nariman Point, Mumbai - 400021, India",
|
||||
"Navi Mumbai":
|
||||
"Flat No. F-1, Pariwar Housing Society, Above Ramdev Hotel, Sector No. 4/5, Vashi, Navi Mumbai - 400703, India",
|
||||
Pune:
|
||||
'"A.J. Residency" 5th Floor, Flat No. 502, S. No. 4711, Plot No. 6, Tawre Colony, Near Chougule Showroom, Pune - 411 009, India',
|
||||
Nagpur:
|
||||
"10-A, Samata Layout, beside NIT office, Diagonal opposite to NIT swimming Pool, Ambazari, Nagpur - 440010",
|
||||
},
|
||||
Karnataka: {
|
||||
Bengaluru:
|
||||
"198, 33rd Cross, 3rd Main Road, 7th Block, Jayanagar, Bangalore - 560070",
|
||||
Belgavi:
|
||||
"Plot No. 51, Teachers Colony, Vinayaknagar, Hindalga Road, Belgavi - 591 108, India",
|
||||
},
|
||||
"Madhya Pradesh": {
|
||||
Bhopal: "E-3/165, Arera Colony, Bhopal - 462 016, Madhya Pradesh, India.",
|
||||
Indore:
|
||||
"Block No. 608, 6th Floor, Apollo Premier Towers, Vijay Nagar, Ring Road, Indore - 452010, India",
|
||||
},
|
||||
Goa: {
|
||||
Panaji:
|
||||
"Office No. 406, Kamat Grand, Behind Caculo Mall, Saint Intez, Panaji City 403001 (Goa), India",
|
||||
},
|
||||
Chhattisgarh: {
|
||||
Raipur:
|
||||
"Flat No. 202, Kashi Apartment, Geetanjali Nagar, Raipur - 492006, Chhattisgarh, India.",
|
||||
},
|
||||
"Andhra Pradesh": {
|
||||
Vijayawada:
|
||||
"42 2 1 202 Drwa No. 423, 1st Floor, Janaki Nilayam, Main Road, Devi Nagar, Vijayawada, Krishna, Andhra Pradesh - 520 003, India",
|
||||
},
|
||||
"Uttar Pradesh": {
|
||||
Lucknow:
|
||||
"Parth Aadyant Tower, Ashlesha A House no. 203, Near Phoenix Palassio Mall, Gomtinagar Extension, Along Shahid Path, Lucknow - 226010, India",
|
||||
},
|
||||
Bihar: {
|
||||
Patna:
|
||||
"501, Manan Mansion Apartment, Gorkhnathpath Lane, East Boring, Canal Road, Patna - 800001, Bihar, India.",
|
||||
},
|
||||
Gujarat: {
|
||||
Ahmedabad:
|
||||
"3rd Floor, A 9, 10, Sardar Mall, Near Dimond Mill, Nocol, Ahmedabad - 382350, Gujarat, India.",
|
||||
},
|
||||
Uttarakhand: {
|
||||
Dehradun:
|
||||
"House No. 12-B, Ekta Enclave, Lane No. 6, Near Success Point Global Academy, Neharu Gram Road, Nathanpur, Deharadun - 248005, Uttarakhand, India.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header with animation */}
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4 relative inline-block">
|
||||
<span className="relative z-10">Our Offices</span>
|
||||
<span className="absolute bottom-0 left-0 w-full h-2 bg-blue-200 z-0 opacity-70 transform translate-y-1"></span>
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
Connecting with you across multiple locations in India
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main content with glassmorphism effect */}
|
||||
<div className="space-y-16">
|
||||
{/* Registered Office */}
|
||||
<section className="relative">
|
||||
<div className="absolute -inset-4 bg-blue-100 rounded-xl opacity-25 blur"></div>
|
||||
<div className="relative bg-white rounded-xl shadow-lg p-6 md:p-8 backdrop-blur-sm border border-blue-100">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="p-2 bg-blue-100 rounded-lg mr-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-blue-800">REGISTERED OFFICE</h2>
|
||||
</div>
|
||||
<div className="pl-14">
|
||||
<p className="text-gray-700 text-lg">{offices["Registered Office"][0].address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Corporate Office */}
|
||||
<section className="relative">
|
||||
<div className="absolute -inset-4 bg-blue-100 rounded-xl opacity-25 blur"></div>
|
||||
<div className="relative bg-white rounded-xl shadow-lg p-6 md:p-8 backdrop-blur-sm border border-blue-100">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="p-2 bg-blue-100 rounded-lg mr-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-blue-800">CORPORATE OFFICE</h2>
|
||||
</div>
|
||||
<div className="pl-14 space-y-3">
|
||||
<p className="text-gray-700 text-lg">{offices["Corporate Office"][0].address}</p>
|
||||
<div className="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-blue-500 mt-1 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<p className="text-gray-700 text-lg">{offices["Corporate Office"][0].phone}</p>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-blue-500 mt-1 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
|
||||
</svg>
|
||||
<p className="text-gray-700 text-lg">{offices["Corporate Office"][0].fax}</p>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-blue-500 mt-1 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.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>
|
||||
<p className="text-gray-700 text-lg">{offices["Corporate Office"][0].email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Regional Offices */}
|
||||
<section>
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-bold text-gray-900 inline-block relative">
|
||||
<span className="relative z-10">REGIONAL OFFICES</span>
|
||||
<span className="absolute bottom-0 left-0 w-full h-2 bg-blue-200 z-0 opacity-50 transform translate-y-1"></span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{Object.entries(offices["Regional Offices"]).map(([state, cities]) => (
|
||||
<div key={state} className="relative group">
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-blue-100 to-blue-50 rounded-lg opacity-75 group-hover:opacity-100 blur transition duration-200"></div>
|
||||
<div className="relative bg-white rounded-lg shadow-md overflow-hidden border border-gray-100 hover:border-blue-200 transition duration-200 h-full">
|
||||
<div className="bg-blue-600 px-6 py-3">
|
||||
<h3 className="text-xl font-bold text-white">{state}</h3>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{Object.entries(cities).map(([city, address]) => (
|
||||
<div key={city} className="pb-4 border-b border-gray-100 last:border-0 last:pb-0">
|
||||
<h4 className="font-bold text-blue-800 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
{city}
|
||||
</h4>
|
||||
<p className="text-gray-600 text-sm mt-1 pl-7">{address}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Office;
|
||||
284
src/pages/Projects.jsx
Normal file
284
src/pages/Projects.jsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import Slider from "react-slick";
|
||||
import heroBg from "../assets/projects-hero.jpg";
|
||||
import "../styles/Projects.css";
|
||||
import "slick-carousel/slick/slick.css";
|
||||
import "slick-carousel/slick/slick-theme.css";
|
||||
|
||||
/* =====================
|
||||
Custom Arrows
|
||||
===================== */
|
||||
const NextArrow = ({ className, style, onClick }) => (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
...style,
|
||||
display: "block",
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
borderRadius: "50%",
|
||||
padding: "10px",
|
||||
right: "10px",
|
||||
zIndex: 2,
|
||||
}}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const PrevArrow = ({ className, style, onClick }) => (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
...style,
|
||||
display: "block",
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
borderRadius: "50%",
|
||||
padding: "10px",
|
||||
left: "10px",
|
||||
zIndex: 2,
|
||||
}}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const Projects = () => {
|
||||
const [selectedSector, setSelectedSector] = useState("all");
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedDescriptions, setExpandedDescriptions] = useState({});
|
||||
const [lightboxImage, setLightboxImage] = useState(null);
|
||||
|
||||
const sectorRef = useRef(null);
|
||||
|
||||
const sectors = [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Water Supply", value: "water supply" },
|
||||
{ label: "Storm Water", value: "storm water" },
|
||||
{ label: "Electromechanical", value: "electromechanical" },
|
||||
{ label: "Real Estate / Buildings", value: "real estate / buildings" },
|
||||
{ label: "Tunnel", value: "tunnel" },
|
||||
{ label: "Roads", value: "roads" },
|
||||
{ label: "Wastewater / Sewerage", value: "wastewater / sewerage" },
|
||||
{ label: "Irrigation", value: "irrigation" },
|
||||
{ label: "Renewable Energy", value: "renewable energy" },
|
||||
];
|
||||
|
||||
/* =====================
|
||||
Fetch Projects
|
||||
===================== */
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`${process.env.REACT_APP_API_BASE_URL}/api/projects`
|
||||
);
|
||||
setProjects(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (err) {
|
||||
console.error("Error fetching projects:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
/* =====================
|
||||
Scroll to sectors
|
||||
===================== */
|
||||
useEffect(() => {
|
||||
if (window.location.hash === "#sectors" && sectorRef.current) {
|
||||
setTimeout(() => {
|
||||
sectorRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, 200);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredProjects = projects.filter((p) => {
|
||||
if (selectedSector === "all") return true;
|
||||
return (
|
||||
p.sector &&
|
||||
p.sector.toLowerCase().trim() === selectedSector.toLowerCase().trim()
|
||||
);
|
||||
});
|
||||
|
||||
const toggleDescription = (id) => {
|
||||
setExpandedDescriptions((prev) => ({
|
||||
...prev,
|
||||
[id]: !prev[id],
|
||||
}));
|
||||
};
|
||||
|
||||
/* =====================
|
||||
Slider Settings
|
||||
===================== */
|
||||
const sliderSettings = {
|
||||
dots: false,
|
||||
infinite: true,
|
||||
speed: 600,
|
||||
slidesToShow: 3,
|
||||
slidesToScroll: 1,
|
||||
autoplay: true,
|
||||
autoplaySpeed: 1500,
|
||||
arrows: true,
|
||||
nextArrow: <NextArrow />,
|
||||
prevArrow: <PrevArrow />,
|
||||
responsive: [
|
||||
{ breakpoint: 1024, settings: { slidesToShow: 3 } },
|
||||
{ breakpoint: 768, settings: { slidesToShow: 2 } },
|
||||
{ breakpoint: 480, settings: { slidesToShow: 1 } },
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* =====================
|
||||
Hero Section
|
||||
====================== */}
|
||||
<div
|
||||
className="relative bg-cover bg-center h-[60vh] flex items-center justify-center"
|
||||
style={{ backgroundImage: `url(${heroBg})` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
<div className="relative z-10 text-center px-4">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-white">
|
||||
Our Projects
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl md:text-2xl text-white mt-2 italic">
|
||||
Stronger Partnerships, Greater Success
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* =====================
|
||||
Sector Buttons
|
||||
====================== */}
|
||||
<div
|
||||
ref={sectorRef}
|
||||
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 px-4 mt-8 mb-10"
|
||||
>
|
||||
{sectors.map((sector, index) => {
|
||||
const bgColors = [
|
||||
"bg-gradient-to-r from-blue-500 to-indigo-500",
|
||||
"bg-gradient-to-r from-green-400 to-green-600",
|
||||
"bg-gradient-to-r from-yellow-400 to-yellow-600",
|
||||
"bg-gradient-to-r from-pink-400 to-pink-600",
|
||||
"bg-gradient-to-r from-purple-400 to-purple-600",
|
||||
"bg-gradient-to-r from-red-400 to-red-600",
|
||||
];
|
||||
|
||||
const isSelected = selectedSector === sector.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={sector.value}
|
||||
onClick={() => setSelectedSector(sector.value)}
|
||||
className={`text-white font-semibold px-5 py-3 rounded-2xl shadow-lg transition-all duration-300 hover:scale-105
|
||||
${bgColors[index % bgColors.length]}
|
||||
${isSelected ? "ring-4 ring-white ring-offset-2" : ""}`}
|
||||
>
|
||||
{sector.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* =====================
|
||||
Projects Slider
|
||||
====================== */}
|
||||
<div className="px-6 mb-16">
|
||||
{loading ? (
|
||||
<p className="text-center text-xl text-gray-500">Loading projects...</p>
|
||||
) : filteredProjects.length === 0 ? (
|
||||
<p className="text-center text-xl text-gray-600">
|
||||
No projects available.
|
||||
</p>
|
||||
) : (
|
||||
<Slider key={selectedSector} {...sliderSettings}>
|
||||
{filteredProjects.map((project) => (
|
||||
<div key={project.id} className="px-2">
|
||||
<div className="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition">
|
||||
|
||||
{/* ✅ FIXED IMAGE SIZE (NO BIG / SMALL ISSUE) */}
|
||||
{project.image && (
|
||||
<div className="relative w-full aspect-[16/9] bg-gray-200 overflow-hidden">
|
||||
<img
|
||||
src={`${process.env.REACT_APP_API_BASE_URL}${project.image}`}
|
||||
alt={project.name}
|
||||
className="absolute inset-0 w-full h-full object-cover cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setLightboxImage(
|
||||
`${process.env.REACT_APP_API_BASE_URL}${project.image}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-bold text-blue-800">
|
||||
{project.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Sector: {project.sector}
|
||||
</p>
|
||||
|
||||
{project.location && (
|
||||
<p className="text-sm text-gray-600">
|
||||
Location: {project.location}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{project.description && (
|
||||
<p className="text-sm text-gray-700 mt-2">
|
||||
{expandedDescriptions[project.id]
|
||||
? project.description
|
||||
: project.description.slice(0, 100) + "..."}
|
||||
{project.description.length > 100 && (
|
||||
<button
|
||||
onClick={() => toggleDescription(project.id)}
|
||||
className="ml-2 text-blue-600 underline text-sm"
|
||||
>
|
||||
{expandedDescriptions[project.id]
|
||||
? "Read Less"
|
||||
: "Read More"}
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Slider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* =====================
|
||||
Image Lightbox
|
||||
====================== */}
|
||||
{lightboxImage && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-80 z-50 flex items-center justify-center"
|
||||
onClick={() => setLightboxImage(null)}
|
||||
>
|
||||
<div className="relative" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
onClick={() => setLightboxImage(null)}
|
||||
className="absolute -top-4 -right-4 text-white text-3xl font-bold"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<img
|
||||
src={lightboxImage}
|
||||
alt="Project"
|
||||
className="max-w-[90vw] max-h-[90vh] object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
||||
0
src/pages/ScrollDownArrow.jsx
Normal file
0
src/pages/ScrollDownArrow.jsx
Normal file
13
src/reportWebVitals.js
Normal file
13
src/reportWebVitals.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
5
src/setupTests.js
Normal file
5
src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
324
src/styles/AboutUs.css
Normal file
324
src/styles/AboutUs.css
Normal file
@@ -0,0 +1,324 @@
|
||||
/* ============================
|
||||
About Hero Section
|
||||
============================ */
|
||||
.about-hero {
|
||||
position: relative;
|
||||
background: url('../assets/about-hero.jpg') center/cover no-repeat;
|
||||
height: 60vh; /* reduced from 80vh */
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.about-hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5); /* semi-transparent overlay */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.about-hero h1,
|
||||
.about-hero p {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
color: white;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.about-hero h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
padding: 1rem 2rem;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
.about-hero p {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
Main Company Info Section
|
||||
============================ */
|
||||
.about-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
padding: 4rem 2rem;
|
||||
gap: 3rem;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.about-text {
|
||||
flex: 1 1 50%;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.about-text h2.highlight-title {
|
||||
font-size: 1.8rem;
|
||||
color: #0d47a1;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
Responsive Adjustments
|
||||
============================ */
|
||||
@media (max-width: 1024px) {
|
||||
.about-hero {
|
||||
height: 50vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.about-hero h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.about-hero p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.about-main {
|
||||
gap: 2rem;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.about-hero {
|
||||
height: 40vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.about-hero h1 {
|
||||
font-size: 2.2rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.about-hero p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.about-main {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.about-text,
|
||||
.counters-grid {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.counters-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.about-hero {
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.about-hero h1 {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.about-hero p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================
|
||||
Vision/Mission Section
|
||||
============================ */
|
||||
.vision-section {
|
||||
padding: 3rem 1.5rem;
|
||||
background: #f9fafb;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vision-section h2 {
|
||||
font-size: 2.8rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 2.5rem;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.vision-grid {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.vision-box {
|
||||
background: #ffffff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: box-shadow 0.3s ease;
|
||||
border-top-width: 5px;
|
||||
border-style: solid;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.vision-box:hover {
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.vision-box h3 {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.vision-box p {
|
||||
color: black;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.border-top-blue { border-top-color: #3b82f6; }
|
||||
.border-top-red { border-top-color: #ef4444; }
|
||||
.border-top-green { border-top-color: #10b981; }
|
||||
|
||||
.icon-circle {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.bg-blue { background-color: #e0f2fe; }
|
||||
.bg-red { background-color: #fee2e2; }
|
||||
.bg-green { background-color: #d1fae5; }
|
||||
|
||||
.icon-svg { width: 28px; height: 28px; }
|
||||
.text-blue { color: #2563eb; }
|
||||
.text-red { color: #dc2626; }
|
||||
.text-green { color: #059669; }
|
||||
|
||||
/* ============================
|
||||
Working Fields Section
|
||||
============================ */
|
||||
.working-fields {
|
||||
padding: 4rem 2rem;
|
||||
background-color: #f0f2f5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.working-fields h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.fields-grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Field Card Backgrounds */
|
||||
.field-card.civil { background: url('../assets/civil-bg.jpg') center/cover no-repeat; }
|
||||
.field-card.mechanical { background: url('../assets/mechanical-bg.jpg') center/cover no-repeat; }
|
||||
.field-card.electrical { background: url('../assets/electrical-bg.jpg') center/cover no-repeat; }
|
||||
.field-card.electromechanical { background: url('../assets/electromechanical-bg.jpg') center/cover no-repeat; }
|
||||
.field-card.it { background: url('../assets/it-bg.jpg') center/cover no-repeat; }
|
||||
.field-card.energy { background: url('../assets/banner-sustainable-energy-1150x650.png') center/cover no-repeat; }
|
||||
|
||||
.field-card {
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
animation: fadeInUp 0.6s ease forwards;
|
||||
transform: translateY(30px);
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.field-card:hover {
|
||||
transform: translateY(-8px) scale(1.05);
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.field-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.field-card:hover .field-overlay {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.field-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
display: inline-block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.field-card:hover .field-icon {
|
||||
transform: scale(1.2) rotate(5deg);
|
||||
}
|
||||
|
||||
.field-content h3 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.field-content p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.field-card { height: 200px; }
|
||||
.field-content h3 { font-size: 1.2rem; }
|
||||
.field-content p { font-size: 0.9rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.field-card { height: 180px; }
|
||||
.field-content h3 { font-size: 1rem; }
|
||||
.field-content p { font-size: 0.85rem; }
|
||||
}
|
||||
66
src/styles/AwardsCard.css
Normal file
66
src/styles/AwardsCard.css
Normal file
@@ -0,0 +1,66 @@
|
||||
/* ---------------------------- */
|
||||
/* Awards Section - Base Style */
|
||||
/* ---------------------------- */
|
||||
.awards-section {
|
||||
background-color: #f5f5f5;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.awards-container {
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.awards-section .highlight-title {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Hero Image - Base */
|
||||
.awards-section img.hero-image {
|
||||
max-width: 100%; /* responsive width */
|
||||
height: auto; /* keeps proportions */
|
||||
display: block;
|
||||
margin: 0 auto 2rem; /* center with some bottom space */
|
||||
border-radius: 8px; /* optional: soft corners */
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Responsive Breakpoints */
|
||||
/* ---------------------------- */
|
||||
|
||||
/* Small Devices (≤ 600px) */
|
||||
@media (max-width: 600px) {
|
||||
.awards-section {
|
||||
padding: 40px 15px;
|
||||
}
|
||||
|
||||
.awards-section .highlight-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.awards-section img.hero-image {
|
||||
max-width: 90%; /* keep some padding */
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium Devices (Tablets: 601px - 1024px) */
|
||||
@media (min-width: 601px) and (max-width: 1024px) {
|
||||
.awards-section {
|
||||
padding: 50px 25px;
|
||||
}
|
||||
|
||||
.awards-section .highlight-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.awards-section img.hero-image {
|
||||
max-width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large Devices (Desktop ≥ 1025px) */
|
||||
/* No change needed – base styles apply */
|
||||
368
src/styles/ContactUs.css
Normal file
368
src/styles/ContactUs.css
Normal file
@@ -0,0 +1,368 @@
|
||||
/* ===============================
|
||||
ContactUs.css (Alternate Design)
|
||||
=============================== */
|
||||
|
||||
:root {
|
||||
--accent: #ff5722; /* Orange accent */
|
||||
--accent-dark: #e64a19;
|
||||
--bg: #f4f6f8;
|
||||
--white: #ffffff;
|
||||
--text: #222;
|
||||
--muted: #555;
|
||||
--border: #e0e0e0;
|
||||
--radius: 14px;
|
||||
--shadow: 0 10px 25px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Reset scope */
|
||||
.contact-page * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* PAGE WRAPPER */
|
||||
.contact-page {
|
||||
background: var(--bg);
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* HERO */
|
||||
.contact-hero {
|
||||
background: url('../assets/contact-hero.jpg') center/cover no-repeat;
|
||||
height: 60vh; /* same as About Hero height */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.contact-overlay {
|
||||
background: linear-gradient(to right, rgba(0,0,0,0.6), rgba(0,0,0,0.4));
|
||||
backdrop-filter: blur(2px);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.contact-overlay h1 {
|
||||
color: #fff;
|
||||
font-size: clamp(28px, 5vw, 46px);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HEADING */
|
||||
.contact-heading {
|
||||
text-align: center;
|
||||
margin: 30px auto 10px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.contact-heading h2 {
|
||||
font-size: 30px;
|
||||
color: var(--accent);
|
||||
margin: 0 0 8px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.contact-heading p {
|
||||
color: var(--muted);
|
||||
margin: 6px auto;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* GRID LAYOUT */
|
||||
.contact-content {
|
||||
display: grid;
|
||||
gap: 30px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
max-width: 1200px;
|
||||
margin: 20px auto 70px;
|
||||
|
||||
}
|
||||
|
||||
/* OFFICE INFO */
|
||||
.contact-info {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius);
|
||||
padding: 22px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.contact-info h3 {
|
||||
color: var(--accent);
|
||||
font-size: 16px;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.contact-info hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.office-block h4 {
|
||||
margin: 6px 0;
|
||||
font-size: 17px;
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
.office-block p {
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* IMAGE CENTER */
|
||||
.contact-center-image {
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
background: #ddd;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.contact-center-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* CONTACT FORM */
|
||||
.contact-form {
|
||||
background: rgba(255,255,255,0.85);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.contact-form h3 {
|
||||
font-size: 16px;
|
||||
color: var(--accent);
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* NOTICE */
|
||||
.notice {
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notice-success {
|
||||
background: #e8f9f1;
|
||||
color: #1b7f4e;
|
||||
}
|
||||
|
||||
.notice-error {
|
||||
background: #fdecea;
|
||||
color: #c43e3e;
|
||||
}
|
||||
|
||||
/* FORM INPUTS */
|
||||
.contact-form form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.contact-form input,
|
||||
.contact-form textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
background: var(--white);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.contact-form input:focus,
|
||||
.contact-form textarea:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(255,87,34,0.2);
|
||||
}
|
||||
|
||||
.contact-form textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* BUTTON */
|
||||
.contact-form button[type="submit"] {
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
border-radius: 40px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.contact-form button:hover {
|
||||
background: var(--accent-dark);
|
||||
}
|
||||
|
||||
.contact-form button[disabled] {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* API hint */
|
||||
.api-hint {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 1000px) {
|
||||
.contact-content {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.contact-center-image {
|
||||
grid-column: span 2;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.contact-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.contact-center-image {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
/* ============================
|
||||
RESPONSIVE IMPROVEMENTS
|
||||
============================ */
|
||||
|
||||
/* Large tablets & small laptops */
|
||||
@media (max-width: 1200px) {
|
||||
.contact-content {
|
||||
max-width: 95%;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.contact-hero {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.contact-overlay h1 {
|
||||
font-size: clamp(24px, 4vw, 40px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets */
|
||||
@media (max-width: 1000px) {
|
||||
.contact-content {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.contact-center-image {
|
||||
grid-column: span 2;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.contact-heading h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.contact-heading p {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile landscape & portrait */
|
||||
@media (max-width: 768px) {
|
||||
.contact-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
margin: 15px auto 50px;
|
||||
}
|
||||
|
||||
.contact-center-image {
|
||||
grid-column: span 1;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.contact-info,
|
||||
.contact-form {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.contact-info h3,
|
||||
.contact-form h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.office-block h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.office-block p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-form input,
|
||||
.contact-form textarea {
|
||||
font-size: 14px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.contact-form button[type="submit"] {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small mobile devices */
|
||||
@media (max-width: 480px) {
|
||||
.contact-hero {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.contact-overlay h1 {
|
||||
font-size: clamp(20px, 5vw, 28px);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.contact-heading {
|
||||
margin: 20px auto 6px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.contact-heading h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.contact-heading p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-info,
|
||||
.contact-form {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.contact-form input,
|
||||
.contact-form textarea {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-form button[type="submit"] {
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
81
src/styles/CounterCard.css
Normal file
81
src/styles/CounterCard.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* ============================
|
||||
Counters Grid - Compact Vertical Line by Line
|
||||
============================ */
|
||||
.counters-grid {
|
||||
display: flex;
|
||||
flex-direction: column; /* vertical stacking */
|
||||
gap: 1rem; /* smaller gap between cards */
|
||||
padding: 15px;
|
||||
max-width: 360px; /* narrower width for compact look */
|
||||
margin: 0 auto; /* center horizontally */
|
||||
}
|
||||
|
||||
/* Individual Counter Cards */
|
||||
.counter-card {
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 15px 20px; /* smaller padding */
|
||||
display: flex;
|
||||
flex-direction: row; /* icon on left, number+label on right */
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
min-height: 70px; /* compact height */
|
||||
}
|
||||
|
||||
/* Optional left accent bar */
|
||||
.counter-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
background-color: #0284c7;
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
/* Number */
|
||||
.counter-card h3 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: #0284c7;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.counter-card p {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Hover effect */
|
||||
.counter-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
|
||||
/* Tablet */
|
||||
@media (max-width: 768px) {
|
||||
.counter-card h3 { font-size: 1.6rem; }
|
||||
.counter-card p { font-size: 0.95rem; }
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 480px) {
|
||||
.counters-grid { max-width: 100%; gap: 0.8rem; }
|
||||
.counter-card {
|
||||
flex-direction: column; /* icon on top, number + label below */
|
||||
align-items: flex-start;
|
||||
padding: 12px 15px;
|
||||
min-height: 60px;
|
||||
}
|
||||
.counter-card h3 { font-size: 1.4rem; margin-bottom: 3px; }
|
||||
.counter-card p { font-size: 0.9rem; }
|
||||
}
|
||||
301
src/styles/Gallery.css
Normal file
301
src/styles/Gallery.css
Normal file
@@ -0,0 +1,301 @@
|
||||
/* ===== Hero Section Styles ===== */
|
||||
.gallery-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Full-width background hero */
|
||||
.gallery-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 85vh;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5); /* Dark overlay */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
max-width: 600px;
|
||||
font-style: italic;
|
||||
color: #ffffff; /* ✅ Changed from #ffb84d (orange) to white */
|
||||
text-shadow: 1px 1px 6px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
|
||||
/* ===== Category Selector ===== */
|
||||
.bannerSlider {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bannerSlider button {
|
||||
padding: 12px 24px;
|
||||
margin: 0 5px;
|
||||
border: none;
|
||||
background: #f0f0f0;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.bannerSlider button.active {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bannerSlider button:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.bannerSlider button.active:hover {
|
||||
background: #1a252f;
|
||||
}
|
||||
|
||||
/* ===== Gallery Grid ===== */
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
border: 1px solid #ddd;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.gallery-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gallery-caption {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* ===== Lightbox ===== */
|
||||
.lightbox-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-height: 80vh;
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.lightbox-caption {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.lightbox-nav:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.lightbox-nav.prev {
|
||||
left: -70px;
|
||||
}
|
||||
|
||||
.lightbox-nav.next {
|
||||
right: -70px;
|
||||
}
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 768px) {
|
||||
.gallery-hero {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.bannerSlider {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bannerSlider button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.lightbox-nav.prev {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.lightbox-nav.next {
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
/* Add these styles to your existing CSS */
|
||||
.gallery-admin-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.media-preview-container {
|
||||
position: relative;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.media-preview {
|
||||
max-height: 250px;
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.clear-preview-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gallery-items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.gallery-item-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.gallery-item-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
|
||||
}
|
||||
/* Prevent selection and force pointer-events */
|
||||
.protected-media {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Overlay technique */
|
||||
.media-protection-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
36
src/styles/Home.css
Normal file
36
src/styles/Home.css
Normal file
@@ -0,0 +1,36 @@
|
||||
/* Main Company Info Section */
|
||||
.about-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 3rem;
|
||||
padding: clamp(2rem, 4vw, 4rem) clamp(1rem, 4vw, 2rem);
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* About text container */
|
||||
.about-text {
|
||||
flex: 1 1 100%; /* Full width on small screens */
|
||||
font-size: clamp(1rem, 1.5vw, 1.1rem); /* Responsive font size */
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Tablet and larger: split columns */
|
||||
@media (min-width: 768px) {
|
||||
.about-text {
|
||||
flex: 1 1 48%; /* Two columns with gap */
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlighted heading */
|
||||
.about-text h2.highlight-title {
|
||||
font-size: clamp(1.5rem, 3vw, 1.8rem); /* Responsive heading size */
|
||||
color: #0d47a1;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Optional: paragraph styling */
|
||||
.about-text p {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
30
src/styles/IndiaMap.css
Normal file
30
src/styles/IndiaMap.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.state {
|
||||
fill: #e2e8f0; /* Default gray */
|
||||
stroke: #ffffff;
|
||||
stroke-width: 1;
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
.state.highlighted {
|
||||
fill: #2563eb; /* Blue for highlighted states */
|
||||
}
|
||||
|
||||
.state:hover {
|
||||
fill: #1e40af; /* Darker blue on hover */
|
||||
}
|
||||
|
||||
/* Mobile view - scale down the entire SVG map container */
|
||||
@media (max-width: 767px) {
|
||||
/* Assuming your ComposableMap SVG has a wrapper with class 'map-wrapper' */
|
||||
.map-wrapper {
|
||||
transform: scale(0.3); /* scale down to 30% size */
|
||||
transform-origin: top left; /* anchor scaling to top-left */
|
||||
width: 100%; /* keep full container width */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Optionally adjust .state stroke width on mobile */
|
||||
.state {
|
||||
stroke-width: 0.7;
|
||||
}
|
||||
}
|
||||
73
src/styles/Projects.css
Normal file
73
src/styles/Projects.css
Normal file
@@ -0,0 +1,73 @@
|
||||
/* Projects.css */
|
||||
|
||||
/* Slider override */
|
||||
.radar-like-slider .slick-slide {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.radar-card {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radar-image {
|
||||
width: 100%;
|
||||
height: 12rem;
|
||||
object-fit: cover;
|
||||
border-radius: 0.75rem;
|
||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.radar-image:hover {
|
||||
transform: translateY(-6px) scale(1.05);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.radar-title {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937; /* text-gray-800 */
|
||||
}
|
||||
|
||||
/* Filter buttons */
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sector-btn {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.sector-btn.selected,
|
||||
.sector-btn:hover {
|
||||
background: #1e40af;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Lightbox */
|
||||
.lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.lightbox img {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
224
src/styles/SectorsSlider.css
Normal file
224
src/styles/SectorsSlider.css
Normal file
@@ -0,0 +1,224 @@
|
||||
/* Main Container Styles - Keep desktop styles unchanged */
|
||||
.infraCont {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #f8fafc;
|
||||
padding: 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.custom {
|
||||
position: relative;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.infraWrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease, transform 0.8s ease;
|
||||
transform: translateX(50px);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.infraWrap.active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
z-index: 2;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.slideTing {
|
||||
width: 50%;
|
||||
padding-right: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Card for Image */
|
||||
/* Card for Image */
|
||||
.sector-card {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: #fff;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||||
overflow: hidden; /* ensures no overflow if image is cropped */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sector-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* fill card completely */
|
||||
}
|
||||
|
||||
|
||||
/* Right side content */
|
||||
.infraright {
|
||||
width: 50%;
|
||||
padding-left: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.infraInfo {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.infraInfo h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.infraInfo h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin-bottom: 1.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.infraInfo p {
|
||||
font-size: 1.1rem;
|
||||
color: #475569;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Slider Navigation Arrows */
|
||||
.slider-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transform: translateY(-50%);
|
||||
padding: 0 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
background: none;
|
||||
color: #1e3a8a;
|
||||
border: none;
|
||||
font-size: 2.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-arrow:hover {
|
||||
color: #d32f2f;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Indicators */
|
||||
.indicators {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.indicator.active {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
|
||||
/* Mobile View - Horizontal Slider */
|
||||
@media (max-width: 768px) {
|
||||
.custom {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.infraCont { padding: 2rem 0; }
|
||||
|
||||
.infraWrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease, transform 0.8s ease;
|
||||
transform: translateX(50px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 0 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.infraWrap.active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.slideTing,
|
||||
.infraright {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sector-card {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.infraInfo {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.infraInfo h2 { font-size: 1.8rem; }
|
||||
.infraInfo h3 { font-size: 1.2rem; }
|
||||
.infraInfo p { font-size: 1rem; margin-bottom: 1.5rem; }
|
||||
|
||||
.indicators { bottom: 1rem; }
|
||||
}
|
||||
|
||||
/* Smaller Mobile Devices */
|
||||
@media (max-width: 480px) {
|
||||
.custom {
|
||||
height: 650px;
|
||||
}
|
||||
|
||||
.infraInfo h2 { font-size: 1.6rem; }
|
||||
.infraInfo h3 { font-size: 1.1rem; }
|
||||
.infraInfo p { font-size: 0.9rem; }
|
||||
|
||||
.sector-card {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
29
src/styles/VisionCard.css
Normal file
29
src/styles/VisionCard.css
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
/* Vision Section */
|
||||
.vision-section {
|
||||
background: url('../assets/background-vision.jpg') center/cover no-repeat;
|
||||
color: white;
|
||||
padding: 4rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.vision-card {
|
||||
flex: 1 1 30%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.vision-card h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vision-card p {
|
||||
font-size: 1rem;
|
||||
color: #eee;
|
||||
}
|
||||
22
tailwind.config.js
Normal file
22
tailwind.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}", // Make sure this path is accurate
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Poppins', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
lcepl: {
|
||||
blue: '#1E3A8A',
|
||||
gray: '#374151',
|
||||
orange: '#F97316',
|
||||
light: '#F9FAFB',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user