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