Initial commit of project without large files
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user