285 lines
9.1 KiB
JavaScript
285 lines
9.1 KiB
JavaScript
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;
|