Spaces:
Sleeping
Sleeping
Upload 29 files
Browse files- src/App.jsx +40 -0
- src/components/ContactForm.jsx +169 -0
- src/components/DotCursor.jsx +116 -0
- src/components/Footer.jsx +73 -0
- src/components/Header.jsx +269 -0
- src/components/Hero.jsx +102 -0
- src/components/HeroOverlapGrid.jsx +27 -0
- src/components/ImageFlex.jsx +42 -0
- src/components/ImageSlider.jsx +228 -0
- src/components/InteractiveBackground.jsx +121 -0
- src/components/MagneticButton.jsx +34 -0
- src/components/MapEmbed.jsx +24 -0
- src/components/ProjectCard.jsx +195 -0
- src/components/Reveal.jsx +40 -0
- src/components/ScrollProgress.jsx +33 -0
- src/components/SectionIntro.jsx +218 -0
- src/components/ServicesGrid.jsx +95 -0
- src/components/StatsCounter.jsx +69 -0
- src/components/YouTubeEmbed.jsx +60 -0
- src/global/style.css +23 -0
- src/main.jsx +14 -0
- src/pages/About.jsx +463 -0
- src/pages/Contact.jsx +40 -0
- src/pages/Home.jsx +263 -0
- src/pages/ProjectDetail.jsx +175 -0
- src/pages/Projects.jsx +1058 -0
- src/pages/ProjectsSection.jsx +180 -0
- src/pages/ServiceDetail.jsx +57 -0
- src/styles/global.css +97 -0
src/App.jsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect } from 'react';
|
| 2 |
+
import { Routes, Route, useLocation } from 'react-router-dom';
|
| 3 |
+
import Header from './components/Header.jsx';
|
| 4 |
+
import Footer from './components/Footer.jsx';
|
| 5 |
+
import Home from './pages/Home.jsx';
|
| 6 |
+
import About from './pages/About.jsx';
|
| 7 |
+
import ProjectsSection from './pages/ProjectsSection.jsx';
|
| 8 |
+
import ProjectDetail from './pages/ProjectDetail.jsx';
|
| 9 |
+
import Contact from './pages/Contact.jsx';
|
| 10 |
+
import DotCursor from './components/DotCursor.jsx';
|
| 11 |
+
|
| 12 |
+
export default function App() {
|
| 13 |
+
const { pathname, hash } = useLocation();
|
| 14 |
+
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
if (hash) {
|
| 17 |
+
const el = document.querySelector(hash);
|
| 18 |
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 19 |
+
} else {
|
| 20 |
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
| 21 |
+
}
|
| 22 |
+
}, [pathname, hash]);
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<div className="flex min-h-screen flex-col">
|
| 26 |
+
<Header />
|
| 27 |
+
<main id="main-content" className="flex-1 focus:outline-none" tabIndex={-1}>
|
| 28 |
+
<Routes>
|
| 29 |
+
<Route path="/" element={<Home />} />
|
| 30 |
+
<Route path="/about" element={<About />} />
|
| 31 |
+
<Route path="/projects/:sectionId" element={<ProjectsSection />} />
|
| 32 |
+
<Route path="/project/:slug" element={<ProjectDetail />} />
|
| 33 |
+
<Route path="/contact" element={<Contact />} />
|
| 34 |
+
</Routes>
|
| 35 |
+
</main>
|
| 36 |
+
<Footer />
|
| 37 |
+
<DotCursor />
|
| 38 |
+
</div>
|
| 39 |
+
);
|
| 40 |
+
}
|
src/components/ContactForm.jsx
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import axios from 'axios';
|
| 3 |
+
|
| 4 |
+
const DEFAULT_ENDPOINT = 'https://formsubmit.co/ajax/jadeinfrapune@gmail.com';
|
| 5 |
+
|
| 6 |
+
export default function ContactForm() {
|
| 7 |
+
const [status, setStatus] = useState({ state: 'idle', message: '' });
|
| 8 |
+
|
| 9 |
+
async function onSubmit(e) {
|
| 10 |
+
e.preventDefault();
|
| 11 |
+
const form = new FormData(e.currentTarget);
|
| 12 |
+
const payload = {
|
| 13 |
+
name: String(form.get('name') || '').trim(),
|
| 14 |
+
email: String(form.get('email') || '').trim(),
|
| 15 |
+
phone: String(form.get('phone') || '').trim(),
|
| 16 |
+
message: String(form.get('message') || '').trim()
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
if (!payload.name || !payload.email || !payload.phone || !payload.message) {
|
| 20 |
+
setStatus({
|
| 21 |
+
state: 'error',
|
| 22 |
+
message: 'All fields are required. Please complete the form.'
|
| 23 |
+
});
|
| 24 |
+
return;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const namePattern = /^[A-Za-z\s]{2,60}$/;
|
| 28 |
+
if (!namePattern.test(payload.name)) {
|
| 29 |
+
setStatus({
|
| 30 |
+
state: 'error',
|
| 31 |
+
message: 'Please enter a valid name using alphabets only.'
|
| 32 |
+
});
|
| 33 |
+
return;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
| 37 |
+
if (!emailPattern.test(payload.email)) {
|
| 38 |
+
setStatus({
|
| 39 |
+
state: 'error',
|
| 40 |
+
message: 'Please enter a valid email address.'
|
| 41 |
+
});
|
| 42 |
+
return;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const phonePattern = /^[0-9]{10}$/;
|
| 46 |
+
if (!phonePattern.test(payload.phone)) {
|
| 47 |
+
setStatus({
|
| 48 |
+
state: 'error',
|
| 49 |
+
message: 'Please enter a valid 10-digit phone number.'
|
| 50 |
+
});
|
| 51 |
+
return;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
setStatus({ state: 'loading', message: '' });
|
| 55 |
+
try {
|
| 56 |
+
const endpoint = import.meta.env.VITE_CONTACT_ENDPOINT || DEFAULT_ENDPOINT;
|
| 57 |
+
const structuredMessage = [
|
| 58 |
+
'New enquiry received via jadeinfra.in contact form:',
|
| 59 |
+
'',
|
| 60 |
+
`Name : ${payload.name}`,
|
| 61 |
+
`Email : ${payload.email}`,
|
| 62 |
+
`Phone : ${payload.phone}`,
|
| 63 |
+
'',
|
| 64 |
+
'Message:',
|
| 65 |
+
payload.message
|
| 66 |
+
].join('\n');
|
| 67 |
+
|
| 68 |
+
await axios.post(
|
| 69 |
+
endpoint,
|
| 70 |
+
{
|
| 71 |
+
name: payload.name,
|
| 72 |
+
email: payload.email,
|
| 73 |
+
phone: payload.phone,
|
| 74 |
+
message: payload.message,
|
| 75 |
+
_subject: 'New enquiry via jadeinfra.in contact form',
|
| 76 |
+
_template: 'box',
|
| 77 |
+
_replyto: payload.email,
|
| 78 |
+
_captcha: 'false',
|
| 79 |
+
content: structuredMessage
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
headers: { 'Content-Type': 'application/json' }
|
| 83 |
+
}
|
| 84 |
+
);
|
| 85 |
+
setStatus({ state: 'success', message: 'Thanks! We will get back to you shortly.' });
|
| 86 |
+
e.currentTarget.reset();
|
| 87 |
+
} catch (err) {
|
| 88 |
+
setStatus({
|
| 89 |
+
state: 'error',
|
| 90 |
+
message:
|
| 91 |
+
'Sorry, there was an issue sending your message. Please try again or email us directly.'
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
return (
|
| 97 |
+
<form className="space-y-4" onSubmit={onSubmit} noValidate aria-labelledby="contact-heading">
|
| 98 |
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
| 99 |
+
<div>
|
| 100 |
+
<label htmlFor="name" className="block text-sm font-medium text-slate-700">Name</label>
|
| 101 |
+
<input
|
| 102 |
+
id="name"
|
| 103 |
+
name="name"
|
| 104 |
+
required
|
| 105 |
+
pattern="[A-Za-z\s]{2,60}"
|
| 106 |
+
autoComplete="name"
|
| 107 |
+
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2
|
| 108 |
+
focus:outline-none focus:ring-2 focus:ring-brand-600"
|
| 109 |
+
/>
|
| 110 |
+
</div>
|
| 111 |
+
<div>
|
| 112 |
+
<label htmlFor="email" className="block text-sm font-medium text-slate-700">Email</label>
|
| 113 |
+
<input
|
| 114 |
+
id="email"
|
| 115 |
+
name="email"
|
| 116 |
+
type="email"
|
| 117 |
+
required
|
| 118 |
+
pattern="^[^\s@]+@[^\s@]+\.[^\s@]+$"
|
| 119 |
+
autoComplete="email"
|
| 120 |
+
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2
|
| 121 |
+
focus:outline-none focus:ring-2 focus:ring-brand-600"
|
| 122 |
+
/>
|
| 123 |
+
</div>
|
| 124 |
+
<div className="md:col-span-2">
|
| 125 |
+
<label htmlFor="phone" className="block text-sm font-medium text-slate-700">Phone</label>
|
| 126 |
+
<input
|
| 127 |
+
id="phone"
|
| 128 |
+
name="phone"
|
| 129 |
+
type="tel"
|
| 130 |
+
required
|
| 131 |
+
pattern="[0-9]{10}"
|
| 132 |
+
inputMode="numeric"
|
| 133 |
+
autoComplete="tel"
|
| 134 |
+
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2
|
| 135 |
+
focus:outline-none focus:ring-2 focus:ring-brand-600"
|
| 136 |
+
/>
|
| 137 |
+
</div>
|
| 138 |
+
<div className="md:col-span-2">
|
| 139 |
+
<label htmlFor="message" className="block text-sm font-medium text-slate-700">Message</label>
|
| 140 |
+
<textarea
|
| 141 |
+
id="message"
|
| 142 |
+
name="message"
|
| 143 |
+
required
|
| 144 |
+
rows="5"
|
| 145 |
+
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2
|
| 146 |
+
focus:outline-none focus:ring-2 focus:ring-brand-600"
|
| 147 |
+
></textarea>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<div className="flex items-center gap-3">
|
| 152 |
+
<button type="submit" className="btn btn-primary" aria-live="polite">
|
| 153 |
+
Send Message
|
| 154 |
+
</button>
|
| 155 |
+
{status.state === 'loading' && (
|
| 156 |
+
<span role="status" className="text-sm text-slate-600">Sending…</span>
|
| 157 |
+
)}
|
| 158 |
+
{status.state === 'success' && (
|
| 159 |
+
<span className="text-sm text-green-700">{status.message}</span>
|
| 160 |
+
)}
|
| 161 |
+
{status.state === 'error' && (
|
| 162 |
+
<span className="text-sm text-red-700">{status.message}</span>
|
| 163 |
+
)}
|
| 164 |
+
</div>
|
| 165 |
+
</form>
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
|
src/components/DotCursor.jsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
const INTERACTIVE_SELECTOR = 'a, button, [role="button"], input, textarea, select, .cursor-interactive';
|
| 4 |
+
|
| 5 |
+
export default function DotCursor() {
|
| 6 |
+
const [enabled, setEnabled] = useState(false);
|
| 7 |
+
const dotRef = useRef(null);
|
| 8 |
+
const frameRef = useRef(null);
|
| 9 |
+
const latestPosRef = useRef({ x: 0, y: 0 });
|
| 10 |
+
const activeRef = useRef(false);
|
| 11 |
+
const visibleRef = useRef(false);
|
| 12 |
+
const hoveredElementRef = useRef(null);
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
if (typeof window === 'undefined') return;
|
| 16 |
+
const isFinePointer = window.matchMedia('(pointer: fine)').matches;
|
| 17 |
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
| 18 |
+
|
| 19 |
+
if (!isFinePointer || prefersReducedMotion) {
|
| 20 |
+
return undefined;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
setEnabled(true);
|
| 24 |
+
const body = document.body;
|
| 25 |
+
body.classList.add('dot-cursor-enabled');
|
| 26 |
+
|
| 27 |
+
const update = () => {
|
| 28 |
+
frameRef.current = null;
|
| 29 |
+
const el = dotRef.current;
|
| 30 |
+
if (!el) return;
|
| 31 |
+
const { x, y } = latestPosRef.current;
|
| 32 |
+
const isActive = activeRef.current;
|
| 33 |
+
const isVisible = visibleRef.current;
|
| 34 |
+
// Use will-change for better performance
|
| 35 |
+
el.style.willChange = 'transform';
|
| 36 |
+
el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) scale(${isActive ? 2.2 : 1})`;
|
| 37 |
+
el.style.opacity = isVisible ? '1' : '0';
|
| 38 |
+
el.dataset.state = isActive ? 'active' : 'rest';
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
const requestUpdate = () => {
|
| 42 |
+
// Cancel any pending frame and schedule new one immediately
|
| 43 |
+
if (frameRef.current) {
|
| 44 |
+
cancelAnimationFrame(frameRef.current);
|
| 45 |
+
}
|
| 46 |
+
frameRef.current = requestAnimationFrame(update);
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const handleMouseMove = (event) => {
|
| 50 |
+
latestPosRef.current = { x: event.clientX, y: event.clientY };
|
| 51 |
+
if (!visibleRef.current) {
|
| 52 |
+
visibleRef.current = true;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const target = event.target;
|
| 56 |
+
const interactiveEl = target?.closest(INTERACTIVE_SELECTOR);
|
| 57 |
+
const isActive = Boolean(interactiveEl);
|
| 58 |
+
|
| 59 |
+
// Only update active state if it actually changed to prevent flickering
|
| 60 |
+
if (activeRef.current !== isActive) {
|
| 61 |
+
activeRef.current = isActive;
|
| 62 |
+
hoveredElementRef.current = interactiveEl;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Always update position immediately
|
| 66 |
+
requestUpdate();
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
const handleWindowMouseLeave = () => {
|
| 70 |
+
visibleRef.current = false;
|
| 71 |
+
activeRef.current = false;
|
| 72 |
+
hoveredElementRef.current = null;
|
| 73 |
+
requestUpdate();
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const handleWindowMouseEnter = () => {
|
| 77 |
+
visibleRef.current = true;
|
| 78 |
+
requestUpdate();
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
| 82 |
+
window.addEventListener('mouseenter', handleWindowMouseEnter, { passive: true });
|
| 83 |
+
window.addEventListener('mouseleave', handleWindowMouseLeave, { passive: true });
|
| 84 |
+
|
| 85 |
+
return () => {
|
| 86 |
+
if (frameRef.current) cancelAnimationFrame(frameRef.current);
|
| 87 |
+
window.removeEventListener('mousemove', handleMouseMove);
|
| 88 |
+
window.removeEventListener('mouseenter', handleWindowMouseEnter);
|
| 89 |
+
window.removeEventListener('mouseleave', handleWindowMouseLeave);
|
| 90 |
+
body.classList.remove('dot-cursor-enabled');
|
| 91 |
+
visibleRef.current = false;
|
| 92 |
+
activeRef.current = false;
|
| 93 |
+
hoveredElementRef.current = null;
|
| 94 |
+
};
|
| 95 |
+
}, []);
|
| 96 |
+
|
| 97 |
+
if (!enabled) {
|
| 98 |
+
return null;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return (
|
| 102 |
+
<div
|
| 103 |
+
ref={dotRef}
|
| 104 |
+
className="dot-cursor pointer-events-none fixed left-0 top-0 z-[9999] hidden h-3 w-3 rounded-full border border-transparent md:block"
|
| 105 |
+
style={{
|
| 106 |
+
transform: 'translate3d(-999px, -999px, 0)',
|
| 107 |
+
opacity: 0,
|
| 108 |
+
backgroundColor: 'rgba(211, 155, 35, 0.8)',
|
| 109 |
+
boxShadow: '0 0 20px -5px rgba(211, 155, 35, 0.45)',
|
| 110 |
+
transition: 'opacity 0.1s ease-out'
|
| 111 |
+
}}
|
| 112 |
+
aria-hidden
|
| 113 |
+
/>
|
| 114 |
+
);
|
| 115 |
+
}
|
| 116 |
+
|
src/components/Footer.jsx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
import ImageFlex from './ImageFlex.jsx';
|
| 4 |
+
|
| 5 |
+
export default function Footer() {
|
| 6 |
+
return (
|
| 7 |
+
<footer className="mt-24 bg-slate-950 text-white" role="contentinfo">
|
| 8 |
+
<div className="container grid grid-cols-1 gap-10 py-14 md:grid-cols-3">
|
| 9 |
+
<div>
|
| 10 |
+
<div className="flex items-center gap-3">
|
| 11 |
+
<ImageFlex base="/assets/logo-white" alt="Jade Infra logo" className="h-8 w-auto" />
|
| 12 |
+
<span className="text-lg font-bold">Jade Infra</span>
|
| 13 |
+
</div>
|
| 14 |
+
<p className="mt-4 max-w-sm text-slate-300">
|
| 15 |
+
Building residential and commercial projects with quality, safety, and on-time delivery.
|
| 16 |
+
{/* TODO: replace with official boilerplate from jadeinfra.in */}
|
| 17 |
+
</p>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div>
|
| 21 |
+
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-slate-200">
|
| 22 |
+
Company
|
| 23 |
+
</h3>
|
| 24 |
+
<ul className="space-y-2 text-slate-300">
|
| 25 |
+
<li><Link to="/about" className="hover:text-white">About</Link></li>
|
| 26 |
+
<li><Link to="/projects/development" className="hover:text-white">Projects</Link></li>
|
| 27 |
+
<li><Link to="/contact" className="hover:text-white">Contact</Link></li>
|
| 28 |
+
</ul>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div>
|
| 32 |
+
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-slate-200">
|
| 33 |
+
Contact
|
| 34 |
+
</h3>
|
| 35 |
+
<address className="not-italic text-slate-300">
|
| 36 |
+
Address:{' '}
|
| 37 |
+
<a
|
| 38 |
+
className="hover:text-white"
|
| 39 |
+
href="https://maps.app.goo.gl/4jSpBviv91E6DYCS9"
|
| 40 |
+
target="_blank"
|
| 41 |
+
rel="noopener noreferrer"
|
| 42 |
+
>
|
| 43 |
+
Click here to view our office location
|
| 44 |
+
</a>
|
| 45 |
+
<br />
|
| 46 |
+
Phone:{' '}
|
| 47 |
+
<a className="hover:text-white" href="tel:+919673009729">
|
| 48 |
+
+91 96730 09729
|
| 49 |
+
</a>
|
| 50 |
+
<br />
|
| 51 |
+
Email:{' '}
|
| 52 |
+
<a className="hover:text-white" href="mailto:jadeinfrapune@gmail.com">
|
| 53 |
+
jadeinfrapune@gmail.com
|
| 54 |
+
</a>
|
| 55 |
+
</address>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
</div>
|
| 60 |
+
<div className="border-t border-white/10">
|
| 61 |
+
<div className="container flex flex-col items-center justify-between gap-4 py-6 md:flex-row">
|
| 62 |
+
<p className="text-sm text-slate-400">
|
| 63 |
+
© {new Date().getFullYear()} Jade Infra. All rights reserved.
|
| 64 |
+
</p>
|
| 65 |
+
<nav className="flex items-center gap-4 text-sm text-slate-400">
|
| 66 |
+
<Link className="hover:text-white" to="/privacy">Privacy</Link>
|
| 67 |
+
<Link className="hover:text-white" to="/terms">Terms</Link>
|
| 68 |
+
</nav>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</footer>
|
| 72 |
+
);
|
| 73 |
+
}
|
src/components/Header.jsx
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState, useRef } from 'react';
|
| 2 |
+
import { Link, NavLink } from 'react-router-dom';
|
| 3 |
+
import clsx from 'clsx';
|
| 4 |
+
import ScrollProgress from './ScrollProgress.jsx';
|
| 5 |
+
import ImageFlex from './ImageFlex.jsx';
|
| 6 |
+
|
| 7 |
+
export default function Header() {
|
| 8 |
+
const [scrolled, setScrolled] = useState(false);
|
| 9 |
+
const [menuOpen, setMenuOpen] = useState(false);
|
| 10 |
+
const [projectsOpen, setProjectsOpen] = useState(false);
|
| 11 |
+
const [headerVisible, setHeaderVisible] = useState(false);
|
| 12 |
+
const [isHovered, setIsHovered] = useState(false);
|
| 13 |
+
const closeTimerRef = useRef(null);
|
| 14 |
+
const headerRef = useRef(null);
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
const onScroll = () => {
|
| 18 |
+
const scrollY = window.scrollY;
|
| 19 |
+
setScrolled(scrollY > 24);
|
| 20 |
+
// Show header when scrolled
|
| 21 |
+
setHeaderVisible(scrollY > 50);
|
| 22 |
+
};
|
| 23 |
+
onScroll();
|
| 24 |
+
window.addEventListener('scroll', onScroll, { passive: true });
|
| 25 |
+
return () => window.removeEventListener('scroll', onScroll);
|
| 26 |
+
}, []);
|
| 27 |
+
|
| 28 |
+
useEffect(() => {
|
| 29 |
+
// Show header on hover or when menu is open, regardless of scroll position
|
| 30 |
+
if (isHovered || menuOpen) {
|
| 31 |
+
setHeaderVisible(true);
|
| 32 |
+
} else {
|
| 33 |
+
// Only hide if not scrolled
|
| 34 |
+
const scrollY = window.scrollY;
|
| 35 |
+
if (scrollY <= 50) {
|
| 36 |
+
setHeaderVisible(false);
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}, [isHovered, menuOpen]);
|
| 40 |
+
|
| 41 |
+
useEffect(() => {
|
| 42 |
+
document.body.style.overflow = menuOpen ? 'hidden' : '';
|
| 43 |
+
}, [menuOpen]);
|
| 44 |
+
|
| 45 |
+
const navLink = (to, label) => (
|
| 46 |
+
<NavLink
|
| 47 |
+
to={to}
|
| 48 |
+
className={({ isActive }) =>
|
| 49 |
+
clsx(
|
| 50 |
+
'relative px-3 py-2 text-sm font-medium transition',
|
| 51 |
+
// Animated underline (temporary) from right -> left on hover
|
| 52 |
+
'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-1 after:h-[2px] after:bg-brand-600',
|
| 53 |
+
'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300',
|
| 54 |
+
'hover:after:origin-left hover:after:scale-x-100',
|
| 55 |
+
isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700'
|
| 56 |
+
)
|
| 57 |
+
}
|
| 58 |
+
>
|
| 59 |
+
{label}
|
| 60 |
+
</NavLink>
|
| 61 |
+
);
|
| 62 |
+
|
| 63 |
+
return (
|
| 64 |
+
<>
|
| 65 |
+
{/* Invisible hover area at top of page to reveal header */}
|
| 66 |
+
<div
|
| 67 |
+
className="fixed inset-x-0 top-0 z-40 h-4"
|
| 68 |
+
onMouseEnter={() => setIsHovered(true)}
|
| 69 |
+
onMouseLeave={() => setIsHovered(false)}
|
| 70 |
+
aria-hidden="true"
|
| 71 |
+
/>
|
| 72 |
+
<header
|
| 73 |
+
ref={headerRef}
|
| 74 |
+
onMouseEnter={() => setIsHovered(true)}
|
| 75 |
+
onMouseLeave={() => setIsHovered(false)}
|
| 76 |
+
className={clsx(
|
| 77 |
+
'fixed inset-x-0 top-0 z-50 bg-white transition-all duration-300',
|
| 78 |
+
scrolled ? 'shadow' : '',
|
| 79 |
+
headerVisible
|
| 80 |
+
? 'translate-y-0 opacity-100 pointer-events-auto'
|
| 81 |
+
: '-translate-y-full opacity-0 pointer-events-none'
|
| 82 |
+
)}
|
| 83 |
+
role="banner"
|
| 84 |
+
style={{
|
| 85 |
+
transform: headerVisible ? 'translateY(0)' : 'translateY(-100%)',
|
| 86 |
+
}}
|
| 87 |
+
>
|
| 88 |
+
<ScrollProgress />
|
| 89 |
+
<a href="#main-content" className="skip-link">
|
| 90 |
+
Skip to content
|
| 91 |
+
</a>
|
| 92 |
+
<div className="container flex items-center justify-between py-4">
|
| 93 |
+
<Link to="/" className="flex items-center gap-3" aria-label="Jade Infra home">
|
| 94 |
+
<ImageFlex
|
| 95 |
+
base="/assets/logo"
|
| 96 |
+
alt="Jade Infra logo"
|
| 97 |
+
className="h-12 md:h-14 w-auto -my-2 md:-my-3"
|
| 98 |
+
/>
|
| 99 |
+
<span className="sr-only">Jade Infra</span>
|
| 100 |
+
</Link>
|
| 101 |
+
|
| 102 |
+
<nav aria-label="Primary" className="hidden items-center gap-8 md:flex">
|
| 103 |
+
{navLink('/', 'Home')}
|
| 104 |
+
{navLink('/about', 'About')}
|
| 105 |
+
{/* Projects with dropdown (hover with grace period) */}
|
| 106 |
+
<div
|
| 107 |
+
className="relative"
|
| 108 |
+
onMouseEnter={() => {
|
| 109 |
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
| 110 |
+
setProjectsOpen(true);
|
| 111 |
+
}}
|
| 112 |
+
onMouseLeave={() => {
|
| 113 |
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
| 114 |
+
closeTimerRef.current = setTimeout(() => setProjectsOpen(false), 250);
|
| 115 |
+
}}
|
| 116 |
+
>
|
| 117 |
+
<NavLink
|
| 118 |
+
to="/projects/development"
|
| 119 |
+
className={({ isActive }) =>
|
| 120 |
+
clsx(
|
| 121 |
+
'px-3 py-2 text-sm font-medium transition',
|
| 122 |
+
isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700'
|
| 123 |
+
)
|
| 124 |
+
}
|
| 125 |
+
aria-haspopup="true"
|
| 126 |
+
aria-expanded={projectsOpen}
|
| 127 |
+
onFocus={() => setProjectsOpen(true)}
|
| 128 |
+
>
|
| 129 |
+
Projects
|
| 130 |
+
</NavLink>
|
| 131 |
+
<div
|
| 132 |
+
className={clsx(
|
| 133 |
+
'absolute left-0 top-full w-56 border border-slate-200 bg-white p-2 shadow-card',
|
| 134 |
+
'transition-opacity',
|
| 135 |
+
projectsOpen ? 'opacity-100 pointer-events-auto mt-2' : 'opacity-0 pointer-events-none mt-2'
|
| 136 |
+
)}
|
| 137 |
+
role="menu"
|
| 138 |
+
aria-label="Projects submenu"
|
| 139 |
+
>
|
| 140 |
+
<NavLink
|
| 141 |
+
to="/projects/development"
|
| 142 |
+
className={({ isActive }) =>
|
| 143 |
+
clsx(
|
| 144 |
+
'relative block rounded px-3 py-2 text-sm transition',
|
| 145 |
+
'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-0.5 after:h-[2px] after:bg-brand-600',
|
| 146 |
+
'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300',
|
| 147 |
+
'hover:after:origin-left hover:after:scale-x-100',
|
| 148 |
+
isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700'
|
| 149 |
+
)
|
| 150 |
+
}
|
| 151 |
+
role="menuitem"
|
| 152 |
+
>
|
| 153 |
+
Development
|
| 154 |
+
</NavLink>
|
| 155 |
+
<NavLink
|
| 156 |
+
to="/projects/construction"
|
| 157 |
+
className={({ isActive }) =>
|
| 158 |
+
clsx(
|
| 159 |
+
'relative block rounded px-3 py-2 text-sm transition',
|
| 160 |
+
'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-0.5 after:h-[2px] after:bg-brand-600',
|
| 161 |
+
'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300',
|
| 162 |
+
'hover:after:origin-left hover:after:scale-x-100',
|
| 163 |
+
isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700'
|
| 164 |
+
)
|
| 165 |
+
}
|
| 166 |
+
role="menuitem"
|
| 167 |
+
>
|
| 168 |
+
Project Contracting
|
| 169 |
+
</NavLink>
|
| 170 |
+
<NavLink
|
| 171 |
+
to="/projects/sra"
|
| 172 |
+
className={({ isActive }) =>
|
| 173 |
+
clsx(
|
| 174 |
+
'relative block rounded px-3 py-2 text-sm transition',
|
| 175 |
+
'after:content-[""] after:absolute after:left-3 after:right-3 after:-bottom-0.5 after:h-[2px] after:bg-brand-600',
|
| 176 |
+
'after:transform after:scale-x-0 after:origin-right after:transition-transform after:duration-300',
|
| 177 |
+
'hover:after:origin-left hover:after:scale-x-100',
|
| 178 |
+
isActive ? 'text-brand-700' : 'text-slate-700 hover:text-brand-700'
|
| 179 |
+
)
|
| 180 |
+
}
|
| 181 |
+
role="menuitem"
|
| 182 |
+
>
|
| 183 |
+
SRA
|
| 184 |
+
</NavLink>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
{navLink('/projects/redevelopment', 'Redevelopment')}
|
| 188 |
+
{navLink('/contact', 'Contact')}
|
| 189 |
+
</nav>
|
| 190 |
+
|
| 191 |
+
<div className="flex items-center gap-3">
|
| 192 |
+
<button
|
| 193 |
+
type="button"
|
| 194 |
+
aria-controls="mobile-menu"
|
| 195 |
+
aria-expanded={menuOpen}
|
| 196 |
+
aria-label="Toggle menu"
|
| 197 |
+
className="inline-flex h-10 w-10 items-center justify-center rounded md:hidden
|
| 198 |
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600"
|
| 199 |
+
onClick={() => setMenuOpen((v) => !v)}
|
| 200 |
+
>
|
| 201 |
+
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">
|
| 202 |
+
<path
|
| 203 |
+
d={menuOpen ? 'M6 18L18 6M6 6l12 12' : 'M3 6h18M3 12h18M3 18h18'}
|
| 204 |
+
stroke="currentColor"
|
| 205 |
+
strokeWidth="2"
|
| 206 |
+
strokeLinecap="round"
|
| 207 |
+
/>
|
| 208 |
+
</svg>
|
| 209 |
+
</button>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<div
|
| 214 |
+
id="mobile-menu"
|
| 215 |
+
className={clsx(
|
| 216 |
+
'md:hidden',
|
| 217 |
+
menuOpen ? 'block' : 'hidden'
|
| 218 |
+
)}
|
| 219 |
+
>
|
| 220 |
+
<div className="container space-y-1 pb-6">
|
| 221 |
+
<NavLink
|
| 222 |
+
to="/"
|
| 223 |
+
className="block rounded px-4 py-3 text-base hover:bg-slate-50"
|
| 224 |
+
onClick={() => setMenuOpen(false)}
|
| 225 |
+
>
|
| 226 |
+
Home
|
| 227 |
+
</NavLink>
|
| 228 |
+
<NavLink
|
| 229 |
+
to="/about"
|
| 230 |
+
className="block rounded px-4 py-3 text-base hover:bg-slate-50"
|
| 231 |
+
onClick={() => setMenuOpen(false)}
|
| 232 |
+
>
|
| 233 |
+
About
|
| 234 |
+
</NavLink>
|
| 235 |
+
<div>
|
| 236 |
+
<NavLink
|
| 237 |
+
to="/projects/development"
|
| 238 |
+
className="block rounded px-4 py-3 text-base hover:bg-slate-50"
|
| 239 |
+
onClick={() => setMenuOpen(false)}
|
| 240 |
+
>
|
| 241 |
+
Projects
|
| 242 |
+
</NavLink>
|
| 243 |
+
<div className="ml-4 mt-1 space-y-1">
|
| 244 |
+
<NavLink to="/projects/development" className="block rounded px-4 py-2 text-sm hover:bg-slate-50" onClick={() => setMenuOpen(false)}>Development</NavLink>
|
| 245 |
+
<NavLink to="/projects/construction" className="block rounded px-4 py-2 text-sm hover:bg-slate-50" onClick={() => setMenuOpen(false)}>Project Contracting</NavLink>
|
| 246 |
+
<NavLink to="/projects/sra" className="block rounded px-4 py-2 text-sm hover:bg-slate-50" onClick={() => setMenuOpen(false)}>SRA</NavLink>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
<NavLink
|
| 250 |
+
to="/projects/redevelopment"
|
| 251 |
+
className="block rounded px-4 py-3 text-base hover:bg-slate-50"
|
| 252 |
+
onClick={() => setMenuOpen(false)}
|
| 253 |
+
>
|
| 254 |
+
Redevelopment
|
| 255 |
+
</NavLink>
|
| 256 |
+
|
| 257 |
+
<NavLink
|
| 258 |
+
to="/contact"
|
| 259 |
+
className="block rounded px-4 py-3 text-base hover:bg-slate-50"
|
| 260 |
+
onClick={() => setMenuOpen(false)}
|
| 261 |
+
>
|
| 262 |
+
Contact
|
| 263 |
+
</NavLink>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
</header>
|
| 267 |
+
</>
|
| 268 |
+
);
|
| 269 |
+
}
|
src/components/Hero.jsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
import MagneticButton from './MagneticButton.jsx';
|
| 4 |
+
import ImageFlex from './ImageFlex.jsx';
|
| 5 |
+
|
| 6 |
+
export default function Hero() {
|
| 7 |
+
const mediaRef = useRef(null);
|
| 8 |
+
const videoRef = useRef(null);
|
| 9 |
+
const [mounted, setMounted] = useState(false);
|
| 10 |
+
const [videoLoaded, setVideoLoaded] = useState(false);
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
setMounted(true);
|
| 14 |
+
|
| 15 |
+
// Check if video loads successfully
|
| 16 |
+
if (videoRef.current) {
|
| 17 |
+
const handleLoadedData = () => setVideoLoaded(true);
|
| 18 |
+
const handleError = () => setVideoLoaded(false);
|
| 19 |
+
videoRef.current.addEventListener('loadeddata', handleLoadedData);
|
| 20 |
+
videoRef.current.addEventListener('error', handleError);
|
| 21 |
+
|
| 22 |
+
return () => {
|
| 23 |
+
if (videoRef.current) {
|
| 24 |
+
videoRef.current.removeEventListener('loadeddata', handleLoadedData);
|
| 25 |
+
videoRef.current.removeEventListener('error', handleError);
|
| 26 |
+
}
|
| 27 |
+
};
|
| 28 |
+
}
|
| 29 |
+
}, []);
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
| 33 |
+
const onScroll = () => {
|
| 34 |
+
const y = window.scrollY;
|
| 35 |
+
const el = videoLoaded ? videoRef.current : mediaRef.current;
|
| 36 |
+
if (!el) return;
|
| 37 |
+
// Subtle parallax translate
|
| 38 |
+
el.style.transform = `translateY(${Math.min(40, y * 0.15)}px) scale(1.05)`;
|
| 39 |
+
};
|
| 40 |
+
window.addEventListener('scroll', onScroll, { passive: true });
|
| 41 |
+
return () => window.removeEventListener('scroll', onScroll);
|
| 42 |
+
}, [videoLoaded]);
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<section
|
| 46 |
+
className="relative flex min-h-screen items-end overflow-hidden bg-slate-900 text-white mb-0"
|
| 47 |
+
aria-label="Hero"
|
| 48 |
+
style={{ zIndex: 1 }}
|
| 49 |
+
>
|
| 50 |
+
{/* Video background with image fallback */}
|
| 51 |
+
<video
|
| 52 |
+
ref={videoRef}
|
| 53 |
+
autoPlay
|
| 54 |
+
loop
|
| 55 |
+
muted
|
| 56 |
+
playsInline
|
| 57 |
+
className={`absolute inset-0 h-full w-full object-cover will-change-transform ${
|
| 58 |
+
videoLoaded ? 'opacity-100' : 'opacity-0'
|
| 59 |
+
}`}
|
| 60 |
+
style={{
|
| 61 |
+
transform: mounted ? 'scale(1.05)' : undefined,
|
| 62 |
+
filter: 'brightness(1.12) contrast(1.06)'
|
| 63 |
+
}}
|
| 64 |
+
aria-hidden="true"
|
| 65 |
+
>
|
| 66 |
+
<source src="/assets/hero-video.mp4" type="video/mp4" />
|
| 67 |
+
{/* TODO: replace with actual video path */}
|
| 68 |
+
</video>
|
| 69 |
+
<ImageFlex
|
| 70 |
+
base="/assets/hero" /* TODO: replace */
|
| 71 |
+
alt=""
|
| 72 |
+
className={`absolute inset-0 h-full w-full object-cover will-change-transform ${
|
| 73 |
+
videoLoaded ? 'opacity-0' : 'opacity-100'
|
| 74 |
+
}`}
|
| 75 |
+
style={{
|
| 76 |
+
transform: mounted ? 'scale(1.05)' : undefined,
|
| 77 |
+
filter: 'brightness(1.12) contrast(1.06)'
|
| 78 |
+
}}
|
| 79 |
+
ref={mediaRef}
|
| 80 |
+
/>
|
| 81 |
+
<div className="absolute inset-0 bg-slate-900/40" aria-hidden="true"></div>
|
| 82 |
+
|
| 83 |
+
{/* Overlay logo at top-right */}
|
| 84 |
+
<div className="absolute right-0 top-0 z-10">
|
| 85 |
+
<div className="rounded-bl-xl bg-white/30 px-3 py-2 shadow-lg backdrop-blur-md ring-1 ring-white/20 md:px-4 md:py-3">
|
| 86 |
+
<ImageFlex
|
| 87 |
+
base="/assets/logo"
|
| 88 |
+
alt="Jade Infra"
|
| 89 |
+
className="h-14 w-auto md:h-18 lg:h-22"
|
| 90 |
+
/>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
{/* Heading with equal left and bottom margins regardless of media aspect */}
|
| 95 |
+
<div className="absolute left-6 bottom-6 z-10 md:left-10 md:bottom-10 lg:left-16 lg:bottom-16">
|
| 96 |
+
<div className="max-w-3xl">
|
| 97 |
+
<h1 className="h1 text-white">Building Tomorrow with Quality and Trust</h1>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</section>
|
| 101 |
+
);
|
| 102 |
+
}
|
src/components/HeroOverlapGrid.jsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ImageFlex from './ImageFlex.jsx';
|
| 3 |
+
|
| 4 |
+
// Overlapping image grid that sits below the hero and overlaps upwards by ~1/3.
|
| 5 |
+
export default function HeroOverlapGrid({ count = 3 }) {
|
| 6 |
+
const items = Array.from({ length: 3 }, (_, i) => i + 1);
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<section aria-label="Featured previews" className="relative -mt-10 md:-mt-14 lg:-mt-16 z-20">
|
| 10 |
+
<div className="container">
|
| 11 |
+
<ul className="grid grid-cols-3 gap-4 sm:gap-6 md:gap-8 lg:gap-10">
|
| 12 |
+
{items.map((i) => (
|
| 13 |
+
<li key={i} className="overflow-hidden rounded-xl shadow-card bg-white">
|
| 14 |
+
<ImageFlex
|
| 15 |
+
base={`/assets/thumb-${i}`}
|
| 16 |
+
alt={`Preview ${i}`}
|
| 17 |
+
className="h-28 w-full object-cover md:h-40 lg:h-48"
|
| 18 |
+
/>
|
| 19 |
+
</li>
|
| 20 |
+
))}
|
| 21 |
+
</ul>
|
| 22 |
+
</div>
|
| 23 |
+
</section>
|
| 24 |
+
);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
src/components/ImageFlex.jsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
// Flexible image loader that tries extensions in order when an image fails.
|
| 4 |
+
// Usage: <ImageFlex base="/assets/hero" alt="..." className="..." />
|
| 5 |
+
// Optionally pass explicit candidates via srcCandidates=["/a.jpg","/a.png"].
|
| 6 |
+
export default function ImageFlex({
|
| 7 |
+
base,
|
| 8 |
+
exts = ['webp', 'jpg', 'jpeg', 'png'],
|
| 9 |
+
srcCandidates,
|
| 10 |
+
alt = '',
|
| 11 |
+
className = '',
|
| 12 |
+
style,
|
| 13 |
+
...rest
|
| 14 |
+
}) {
|
| 15 |
+
const candidates = useMemo(() => {
|
| 16 |
+
if (Array.isArray(srcCandidates) && srcCandidates.length > 0) return srcCandidates;
|
| 17 |
+
if (typeof base === 'string') return exts.map((e) => `${base}.${e}`);
|
| 18 |
+
return [];
|
| 19 |
+
}, [base, exts, srcCandidates]);
|
| 20 |
+
|
| 21 |
+
const [idx, setIdx] = useState(0);
|
| 22 |
+
const src = candidates[idx];
|
| 23 |
+
|
| 24 |
+
function handleError() {
|
| 25 |
+
if (idx < candidates.length - 1) setIdx(idx + 1);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
if (!src) return null;
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<img
|
| 32 |
+
src={src}
|
| 33 |
+
alt={alt}
|
| 34 |
+
className={className}
|
| 35 |
+
style={style}
|
| 36 |
+
onError={handleError}
|
| 37 |
+
{...rest}
|
| 38 |
+
/>
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
src/components/ImageSlider.jsx
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import ImageFlex from './ImageFlex.jsx';
|
| 3 |
+
|
| 4 |
+
export default function ImageSlider({ images, projectTitle }) {
|
| 5 |
+
const [currentIndex, setCurrentIndex] = useState(0);
|
| 6 |
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
| 7 |
+
const intervalRef = useRef(null);
|
| 8 |
+
|
| 9 |
+
// Auto-advance every 4 seconds
|
| 10 |
+
useEffect(() => {
|
| 11 |
+
if (images.length <= 1) return;
|
| 12 |
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
| 13 |
+
intervalRef.current = setInterval(() => {
|
| 14 |
+
setCurrentIndex((prev) => (prev + 1) % images.length);
|
| 15 |
+
}, 4000);
|
| 16 |
+
return () => {
|
| 17 |
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
| 18 |
+
};
|
| 19 |
+
}, [images.length]);
|
| 20 |
+
|
| 21 |
+
// Pause on hover/focus
|
| 22 |
+
const pauseAutoPlay = () => {
|
| 23 |
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
| 24 |
+
};
|
| 25 |
+
const resumeAutoPlay = () => {
|
| 26 |
+
if (images.length <= 1) return;
|
| 27 |
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
| 28 |
+
intervalRef.current = setInterval(() => {
|
| 29 |
+
setCurrentIndex((prev) => (prev + 1) % images.length);
|
| 30 |
+
}, 4000);
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const goToNext = () => {
|
| 34 |
+
pauseAutoPlay();
|
| 35 |
+
setCurrentIndex((prev) => (prev + 1) % images.length);
|
| 36 |
+
resumeAutoPlay();
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
const goToPrev = () => {
|
| 40 |
+
pauseAutoPlay();
|
| 41 |
+
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
|
| 42 |
+
resumeAutoPlay();
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const goToSlide = (index) => {
|
| 46 |
+
pauseAutoPlay();
|
| 47 |
+
setCurrentIndex(index);
|
| 48 |
+
resumeAutoPlay();
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const openFullscreen = () => {
|
| 52 |
+
setIsFullscreen(true);
|
| 53 |
+
pauseAutoPlay();
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
const closeFullscreen = () => {
|
| 57 |
+
setIsFullscreen(false);
|
| 58 |
+
resumeAutoPlay();
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
// Keyboard navigation in fullscreen (auto-play stays paused in fullscreen)
|
| 62 |
+
useEffect(() => {
|
| 63 |
+
if (!isFullscreen) return;
|
| 64 |
+
|
| 65 |
+
const handleKeyDown = (e) => {
|
| 66 |
+
if (e.key === 'Escape') {
|
| 67 |
+
setIsFullscreen(false);
|
| 68 |
+
resumeAutoPlay();
|
| 69 |
+
} else if (e.key === 'ArrowLeft') {
|
| 70 |
+
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
|
| 71 |
+
} else if (e.key === 'ArrowRight') {
|
| 72 |
+
setCurrentIndex((prev) => (prev + 1) % images.length);
|
| 73 |
+
}
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
window.addEventListener('keydown', handleKeyDown);
|
| 77 |
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 78 |
+
}, [isFullscreen, images.length]);
|
| 79 |
+
|
| 80 |
+
if (!images || images.length === 0) return null;
|
| 81 |
+
|
| 82 |
+
const currentImage = images[currentIndex];
|
| 83 |
+
|
| 84 |
+
return (
|
| 85 |
+
<>
|
| 86 |
+
<div
|
| 87 |
+
className="relative group overflow-hidden rounded-xl bg-slate-100"
|
| 88 |
+
onMouseEnter={pauseAutoPlay}
|
| 89 |
+
onMouseLeave={resumeAutoPlay}
|
| 90 |
+
>
|
| 91 |
+
<div className="aspect-video relative">
|
| 92 |
+
<ImageFlex
|
| 93 |
+
base={currentImage}
|
| 94 |
+
alt={`${projectTitle} - Image ${currentIndex + 1}`}
|
| 95 |
+
className="w-full h-full object-cover"
|
| 96 |
+
/>
|
| 97 |
+
{/* Click-to-fullscreen overlay */}
|
| 98 |
+
<button
|
| 99 |
+
type="button"
|
| 100 |
+
onClick={openFullscreen}
|
| 101 |
+
aria-label="View fullscreen"
|
| 102 |
+
className="absolute inset-0 cursor-zoom-in focus:outline-none"
|
| 103 |
+
/>
|
| 104 |
+
{/* Navigation arrows */}
|
| 105 |
+
{images.length > 1 && (
|
| 106 |
+
<>
|
| 107 |
+
<button
|
| 108 |
+
type="button"
|
| 109 |
+
onClick={goToPrev}
|
| 110 |
+
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-2
|
| 111 |
+
transition opacity-0 group-hover:opacity-100 focus:opacity-100"
|
| 112 |
+
aria-label="Previous image"
|
| 113 |
+
>
|
| 114 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 115 |
+
<path d="M15 18l-6-6 6-6" />
|
| 116 |
+
</svg>
|
| 117 |
+
</button>
|
| 118 |
+
<button
|
| 119 |
+
type="button"
|
| 120 |
+
onClick={goToNext}
|
| 121 |
+
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white rounded-full p-2
|
| 122 |
+
transition opacity-0 group-hover:opacity-100 focus:opacity-100"
|
| 123 |
+
aria-label="Next image"
|
| 124 |
+
>
|
| 125 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 126 |
+
<path d="M9 18l6-6-6-6" />
|
| 127 |
+
</svg>
|
| 128 |
+
</button>
|
| 129 |
+
</>
|
| 130 |
+
)}
|
| 131 |
+
{/* Fullscreen button */}
|
| 132 |
+
<button
|
| 133 |
+
type="button"
|
| 134 |
+
onClick={openFullscreen}
|
| 135 |
+
className="absolute bottom-4 right-4 bg-white/80 hover:bg-white rounded-full p-2
|
| 136 |
+
transition opacity-0 group-hover:opacity-100 focus:opacity-100"
|
| 137 |
+
aria-label="View fullscreen"
|
| 138 |
+
>
|
| 139 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 140 |
+
<path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" />
|
| 141 |
+
</svg>
|
| 142 |
+
</button>
|
| 143 |
+
{/* Slide indicators */}
|
| 144 |
+
{images.length > 1 && (
|
| 145 |
+
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
| 146 |
+
{images.map((_, i) => (
|
| 147 |
+
<button
|
| 148 |
+
key={i}
|
| 149 |
+
type="button"
|
| 150 |
+
onClick={() => goToSlide(i)}
|
| 151 |
+
className={`h-2 rounded-full transition ${
|
| 152 |
+
i === currentIndex ? 'w-8 bg-white' : 'w-2 bg-white/50 hover:bg-white/75'
|
| 153 |
+
}`}
|
| 154 |
+
aria-label={`Go to slide ${i + 1}`}
|
| 155 |
+
/>
|
| 156 |
+
))}
|
| 157 |
+
</div>
|
| 158 |
+
)}
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
{/* Fullscreen modal */}
|
| 163 |
+
{isFullscreen && (
|
| 164 |
+
<div
|
| 165 |
+
className="fixed inset-0 z-[100] bg-black/95 flex items-center justify-center p-4"
|
| 166 |
+
onClick={closeFullscreen}
|
| 167 |
+
>
|
| 168 |
+
<button
|
| 169 |
+
type="button"
|
| 170 |
+
onClick={closeFullscreen}
|
| 171 |
+
className="absolute top-4 right-4 text-white hover:text-gray-300 p-2"
|
| 172 |
+
aria-label="Close fullscreen"
|
| 173 |
+
>
|
| 174 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 175 |
+
<path d="M18 6L6 18M6 6l12 12" />
|
| 176 |
+
</svg>
|
| 177 |
+
</button>
|
| 178 |
+
<div className="relative max-w-7xl w-full h-full flex items-center" onClick={(e) => e.stopPropagation()}>
|
| 179 |
+
<button
|
| 180 |
+
type="button"
|
| 181 |
+
onClick={goToPrev}
|
| 182 |
+
className="absolute left-4 bg-white/20 hover:bg-white/30 text-white rounded-full p-3 z-10"
|
| 183 |
+
aria-label="Previous image"
|
| 184 |
+
>
|
| 185 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 186 |
+
<path d="M15 18l-6-6 6-6" />
|
| 187 |
+
</svg>
|
| 188 |
+
</button>
|
| 189 |
+
<div className="w-full h-full flex items-center justify-center">
|
| 190 |
+
<ImageFlex
|
| 191 |
+
base={currentImage}
|
| 192 |
+
alt={`${projectTitle} - Image ${currentIndex + 1}`}
|
| 193 |
+
className="max-w-full max-h-full object-contain"
|
| 194 |
+
/>
|
| 195 |
+
</div>
|
| 196 |
+
<button
|
| 197 |
+
type="button"
|
| 198 |
+
onClick={goToNext}
|
| 199 |
+
className="absolute right-4 bg-white/20 hover:bg-white/30 text-white rounded-full p-3 z-10"
|
| 200 |
+
aria-label="Next image"
|
| 201 |
+
>
|
| 202 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 203 |
+
<path d="M9 18l6-6-6-6" />
|
| 204 |
+
</svg>
|
| 205 |
+
</button>
|
| 206 |
+
{/* Fullscreen indicators */}
|
| 207 |
+
{images.length > 1 && (
|
| 208 |
+
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2">
|
| 209 |
+
{images.map((_, i) => (
|
| 210 |
+
<button
|
| 211 |
+
key={i}
|
| 212 |
+
type="button"
|
| 213 |
+
onClick={() => goToSlide(i)}
|
| 214 |
+
className={`h-2 rounded-full transition ${
|
| 215 |
+
i === currentIndex ? 'w-8 bg-white' : 'w-2 bg-white/50 hover:bg-white/75'
|
| 216 |
+
}`}
|
| 217 |
+
aria-label={`Go to slide ${i + 1}`}
|
| 218 |
+
/>
|
| 219 |
+
))}
|
| 220 |
+
</div>
|
| 221 |
+
)}
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
)}
|
| 225 |
+
</>
|
| 226 |
+
);
|
| 227 |
+
}
|
| 228 |
+
|
src/components/InteractiveBackground.jsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef } from 'react';
|
| 2 |
+
|
| 3 |
+
// Interactive background with flowing waves/spirals in brand colors.
|
| 4 |
+
// Renders on a canvas for performance and reacts to scroll and time.
|
| 5 |
+
export default function InteractiveBackground() {
|
| 6 |
+
const canvasRef = useRef(null);
|
| 7 |
+
const rafRef = useRef(0);
|
| 8 |
+
const timeRef = useRef(0);
|
| 9 |
+
const dimsRef = useRef({ w: 0, h: 0, dpr: 1 });
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
const canvas = canvasRef.current;
|
| 13 |
+
if (!canvas) return;
|
| 14 |
+
const ctx = canvas.getContext('2d');
|
| 15 |
+
|
| 16 |
+
const resize = () => {
|
| 17 |
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
| 18 |
+
const w = canvas.clientWidth;
|
| 19 |
+
const h = canvas.clientHeight;
|
| 20 |
+
canvas.width = Math.floor(w * dpr);
|
| 21 |
+
canvas.height = Math.floor(h * dpr);
|
| 22 |
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
| 23 |
+
dimsRef.current = { w, h, dpr };
|
| 24 |
+
};
|
| 25 |
+
resize();
|
| 26 |
+
const ro = new ResizeObserver(resize);
|
| 27 |
+
ro.observe(canvas);
|
| 28 |
+
|
| 29 |
+
const drawWaves = (t) => {
|
| 30 |
+
const { w, h } = dimsRef.current;
|
| 31 |
+
// transparent base to let page bg through; slight warm-cool wash
|
| 32 |
+
const wash = ctx.createLinearGradient(0, 0, w, h);
|
| 33 |
+
wash.addColorStop(0, 'rgba(255,247,237,0.55)'); // warm
|
| 34 |
+
wash.addColorStop(1, 'rgba(236,253,245,0.55)'); // cool
|
| 35 |
+
ctx.fillStyle = wash;
|
| 36 |
+
ctx.fillRect(0, 0, w, h);
|
| 37 |
+
|
| 38 |
+
// Parameters
|
| 39 |
+
const scrollY = window.scrollY || 0;
|
| 40 |
+
const baseSpeed = 0.0018;
|
| 41 |
+
const parallax = scrollY * 0.08;
|
| 42 |
+
|
| 43 |
+
// Brand colors
|
| 44 |
+
const green = '#007746';
|
| 45 |
+
const orange = '#d39b23';
|
| 46 |
+
|
| 47 |
+
// Helper to draw a sine wave ribbon
|
| 48 |
+
const ribbon = (phase, amp, freq, thickness, color, yOffset = 0) => {
|
| 49 |
+
ctx.save();
|
| 50 |
+
ctx.beginPath();
|
| 51 |
+
const midY = h * 0.55 + yOffset;
|
| 52 |
+
for (let x = 0; x <= w; x += 2) {
|
| 53 |
+
const prog = x / w;
|
| 54 |
+
const spiral = Math.sin(prog * 6.283 + phase) * 0.6; // spiral-ish modulation
|
| 55 |
+
const y =
|
| 56 |
+
midY +
|
| 57 |
+
Math.sin(x * freq + phase) * amp +
|
| 58 |
+
Math.cos((x * freq) / 2 + phase * 0.8) * (amp * 0.35) +
|
| 59 |
+
spiral * 16;
|
| 60 |
+
if (x === 0) ctx.moveTo(x, y);
|
| 61 |
+
else ctx.lineTo(x, y);
|
| 62 |
+
}
|
| 63 |
+
ctx.strokeStyle = color;
|
| 64 |
+
ctx.lineWidth = thickness;
|
| 65 |
+
ctx.globalAlpha = 0.55;
|
| 66 |
+
ctx.shadowColor = color;
|
| 67 |
+
ctx.shadowBlur = 16;
|
| 68 |
+
ctx.stroke();
|
| 69 |
+
ctx.restore();
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
// Green top ribbon
|
| 73 |
+
ribbon(t * 3 + parallax * 0.015, 24, 0.012, 6, hexToRgba(green, 0.55), -40);
|
| 74 |
+
// Orange middle ribbon
|
| 75 |
+
ribbon(t * 2 + parallax * 0.01, 28, 0.0105, 7, hexToRgba(orange, 0.55), 0);
|
| 76 |
+
// Green thin accent
|
| 77 |
+
ribbon(t * 4.2 + parallax * 0.02, 18, 0.018, 3, hexToRgba(green, 0.45), 32);
|
| 78 |
+
// Orange thin accent
|
| 79 |
+
ribbon(t * 5 + parallax * 0.018, 14, 0.02, 2, hexToRgba(orange, 0.4), -22);
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const render = () => {
|
| 83 |
+
const { w, h } = dimsRef.current;
|
| 84 |
+
if (w === 0 || h === 0) return;
|
| 85 |
+
const ctx = canvas.getContext('2d');
|
| 86 |
+
// Clear with transparency to stack with site background if any
|
| 87 |
+
ctx.clearRect(0, 0, w, h);
|
| 88 |
+
const t = (timeRef.current += 0.6); // time base for wave evolution
|
| 89 |
+
drawWaves(t * 0.001);
|
| 90 |
+
rafRef.current = requestAnimationFrame(render);
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
rafRef.current = requestAnimationFrame(render);
|
| 94 |
+
const onScroll = () => {
|
| 95 |
+
// allow scroll to affect next frame via parallax calc
|
| 96 |
+
};
|
| 97 |
+
window.addEventListener('scroll', onScroll, { passive: true });
|
| 98 |
+
return () => {
|
| 99 |
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
| 100 |
+
ro.disconnect();
|
| 101 |
+
window.removeEventListener('scroll', onScroll);
|
| 102 |
+
};
|
| 103 |
+
}, []);
|
| 104 |
+
|
| 105 |
+
return (
|
| 106 |
+
<div className="pointer-events-none fixed inset-0 -z-10" aria-hidden="true">
|
| 107 |
+
<canvas ref={canvasRef} className="h-full w-full" />
|
| 108 |
+
</div>
|
| 109 |
+
);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function hexToRgba(hex, a = 1) {
|
| 113 |
+
const clean = hex.replace('#', '');
|
| 114 |
+
const bigint = parseInt(clean, 16);
|
| 115 |
+
const r = (bigint >> 16) & 255;
|
| 116 |
+
const g = (bigint >> 8) & 255;
|
| 117 |
+
const b = bigint & 255;
|
| 118 |
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
src/components/MagneticButton.jsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef } from 'react';
|
| 2 |
+
|
| 3 |
+
export default function MagneticButton({ children, className = '', strength = 0.25, ...props }) {
|
| 4 |
+
const ref = useRef(null);
|
| 5 |
+
|
| 6 |
+
function onMove(e) {
|
| 7 |
+
const el = ref.current;
|
| 8 |
+
if (!el) return;
|
| 9 |
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
| 10 |
+
const rect = el.getBoundingClientRect();
|
| 11 |
+
const relX = e.clientX - rect.left - rect.width / 2;
|
| 12 |
+
const relY = e.clientY - rect.top - rect.height / 2;
|
| 13 |
+
el.style.transform = `translate(${relX * strength}px, ${relY * strength}px)`;
|
| 14 |
+
}
|
| 15 |
+
function onLeave() {
|
| 16 |
+
const el = ref.current;
|
| 17 |
+
if (!el) return;
|
| 18 |
+
el.style.transform = 'translate(0, 0)';
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<span
|
| 23 |
+
ref={ref}
|
| 24 |
+
className={className}
|
| 25 |
+
onMouseMove={onMove}
|
| 26 |
+
onMouseLeave={onLeave}
|
| 27 |
+
{...props}
|
| 28 |
+
>
|
| 29 |
+
{children}
|
| 30 |
+
</span>
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
src/components/MapEmbed.jsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
export default function MapEmbed({ address, title = 'Project location' }) {
|
| 4 |
+
if (!address) return null;
|
| 5 |
+
const src = `https://www.google.com/maps?q=${encodeURIComponent(address)}&output=embed`;
|
| 6 |
+
|
| 7 |
+
return (
|
| 8 |
+
<section className="mt-6" aria-labelledby="map-heading">
|
| 9 |
+
<h3 id="map-heading" className="h3 mb-3">Location</h3>
|
| 10 |
+
<div className="overflow-hidden rounded-xl border border-slate-200 bg-slate-50">
|
| 11 |
+
<iframe
|
| 12 |
+
title={title}
|
| 13 |
+
src={src}
|
| 14 |
+
loading="lazy"
|
| 15 |
+
referrerPolicy="no-referrer-when-downgrade"
|
| 16 |
+
className="h-64 w-full md:h-72"
|
| 17 |
+
/>
|
| 18 |
+
</div>
|
| 19 |
+
<p className="mt-2 text-xs text-slate-500">Based on project location: {address}</p>
|
| 20 |
+
</section>
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
src/components/ProjectCard.jsx
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef } from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
import ImageFlex from './ImageFlex.jsx';
|
| 4 |
+
|
| 5 |
+
export default function ProjectCard({ project }) {
|
| 6 |
+
const cardRef = useRef(null);
|
| 7 |
+
|
| 8 |
+
function getStatusClasses(status) {
|
| 9 |
+
if (!status) return 'bg-slate-100 text-slate-700';
|
| 10 |
+
switch (status) {
|
| 11 |
+
case 'Completed':
|
| 12 |
+
return 'bg-emerald-100 text-emerald-700';
|
| 13 |
+
case 'Ongoing':
|
| 14 |
+
return 'bg-amber-100 text-amber-700';
|
| 15 |
+
case 'Upcoming':
|
| 16 |
+
return 'bg-sky-100 text-sky-700';
|
| 17 |
+
default:
|
| 18 |
+
return 'bg-slate-100 text-slate-700';
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function onMove(e) {
|
| 23 |
+
const el = cardRef.current;
|
| 24 |
+
if (!el) return;
|
| 25 |
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
| 26 |
+
const rect = el.getBoundingClientRect();
|
| 27 |
+
const x = e.clientX - rect.left;
|
| 28 |
+
const y = e.clientY - rect.top;
|
| 29 |
+
const midX = rect.width / 2;
|
| 30 |
+
const midY = rect.height / 2;
|
| 31 |
+
const rotateX = ((y - midY) / midY) * -4;
|
| 32 |
+
const rotateY = ((x - midX) / midX) * 4;
|
| 33 |
+
el.style.transform = `perspective(800px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
|
| 34 |
+
}
|
| 35 |
+
function onLeave() {
|
| 36 |
+
const el = cardRef.current;
|
| 37 |
+
if (!el) return;
|
| 38 |
+
el.style.transform = 'perspective(800px) rotateX(0deg) rotateY(0deg)';
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const hasGallery = Array.isArray(project.gallery) && project.gallery.length > 0;
|
| 42 |
+
const primaryImage =
|
| 43 |
+
(typeof project.image === 'string' && project.image.replace(/\.(jpg|jpeg|png|webp)$/i, '')) || null;
|
| 44 |
+
const galleryImage = hasGallery ? project.gallery[0] : null;
|
| 45 |
+
const explicitBase =
|
| 46 |
+
typeof project.imageBase === 'string'
|
| 47 |
+
? project.imageBase.replace(/\.(jpg|jpeg|png|webp)$/i, '')
|
| 48 |
+
: null;
|
| 49 |
+
const slugFallback = !project.noDetail && project.slug ? `/assets/projects/${project.slug}/gallery-1` : null;
|
| 50 |
+
|
| 51 |
+
const imageBases = [
|
| 52 |
+
primaryImage,
|
| 53 |
+
galleryImage,
|
| 54 |
+
explicitBase,
|
| 55 |
+
slugFallback
|
| 56 |
+
].filter(Boolean);
|
| 57 |
+
|
| 58 |
+
const hasImage = imageBases.length > 0;
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
<article
|
| 62 |
+
ref={cardRef}
|
| 63 |
+
onMouseMove={onMove}
|
| 64 |
+
onMouseLeave={onLeave}
|
| 65 |
+
className={`relative card group overflow-hidden will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-xl before:bg-brand-600/0 before:blur before:transition before:duration-300 hover:before:bg-brand-600/10 ${
|
| 66 |
+
hasImage ? 'aspect-[3/4] min-h-[400px] md:min-h-[500px]' : 'h-full'
|
| 67 |
+
}`}
|
| 68 |
+
aria-labelledby={`proj-${project.slug}`}
|
| 69 |
+
>
|
| 70 |
+
{hasImage ? (
|
| 71 |
+
<>
|
| 72 |
+
<div className="relative h-full w-full bg-slate-200">
|
| 73 |
+
<ImageFlex
|
| 74 |
+
base={imageBases[0]}
|
| 75 |
+
srcCandidates={imageBases.flatMap((base) =>
|
| 76 |
+
['webp', 'jpg', 'jpeg', 'png'].map((ext) => `${base}.${ext}`)
|
| 77 |
+
)}
|
| 78 |
+
alt={project.title}
|
| 79 |
+
className="h-full w-full object-contain transition-transform group-hover:scale-[1.03]"
|
| 80 |
+
loading="lazy"
|
| 81 |
+
/>
|
| 82 |
+
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-900/80 via-slate-900/40 to-transparent"></div>
|
| 83 |
+
</div>
|
| 84 |
+
{/* Overlay text content */}
|
| 85 |
+
<div className="absolute bottom-0 left-0 right-0 p-5 text-white">
|
| 86 |
+
<h3 id={`proj-${project.slug}`} className="h3 text-white">
|
| 87 |
+
{project.noDetail ? (
|
| 88 |
+
<span>{project.title}</span>
|
| 89 |
+
) : (
|
| 90 |
+
<Link to={`/project/${project.slug}`} className="hover:underline">
|
| 91 |
+
{project.title}
|
| 92 |
+
</Link>
|
| 93 |
+
)}
|
| 94 |
+
</h3>
|
| 95 |
+
<p className="mt-1 text-sm text-white/90">{project.location}</p>
|
| 96 |
+
{project.projectSize && (
|
| 97 |
+
<p className="mt-1 text-sm font-medium text-white">
|
| 98 |
+
{project.projectSize}
|
| 99 |
+
</p>
|
| 100 |
+
)}
|
| 101 |
+
<div className="mt-4 flex items-center justify-between">
|
| 102 |
+
<span className="flex flex-wrap items-center gap-2">
|
| 103 |
+
{project.status && (
|
| 104 |
+
<span
|
| 105 |
+
className={`rounded px-2 py-1 text-xs font-medium ${getStatusClasses(project.status)}`}
|
| 106 |
+
aria-label={`Status: ${project.status}`}
|
| 107 |
+
>
|
| 108 |
+
{project.status}
|
| 109 |
+
</span>
|
| 110 |
+
)}
|
| 111 |
+
{(project.categories || []).slice(0, 2).map((c) => (
|
| 112 |
+
<span key={c} className="rounded bg-white/20 backdrop-blur-sm px-2 py-1 text-xs text-white">
|
| 113 |
+
{c}
|
| 114 |
+
</span>
|
| 115 |
+
))}
|
| 116 |
+
</span>
|
| 117 |
+
{!project.noDetail && (
|
| 118 |
+
<Link
|
| 119 |
+
to={`/project/${project.slug}`}
|
| 120 |
+
className="text-sm font-medium text-white underline-offset-2 hover:underline"
|
| 121 |
+
aria-label={`View ${project.title}`}
|
| 122 |
+
>
|
| 123 |
+
View details
|
| 124 |
+
</Link>
|
| 125 |
+
)}
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</>
|
| 129 |
+
) : (
|
| 130 |
+
<>
|
| 131 |
+
<div className="flex h-24 w-full items-center justify-center bg-gradient-to-br from-brand-50 via-white to-brand-100">
|
| 132 |
+
<svg
|
| 133 |
+
className="h-12 w-12 text-brand-600"
|
| 134 |
+
viewBox="0 0 24 24"
|
| 135 |
+
fill="none"
|
| 136 |
+
stroke="currentColor"
|
| 137 |
+
strokeWidth="1.5"
|
| 138 |
+
strokeLinecap="round"
|
| 139 |
+
strokeLinejoin="round"
|
| 140 |
+
aria-hidden="true"
|
| 141 |
+
>
|
| 142 |
+
<rect x="3" y="3" width="7" height="7" />
|
| 143 |
+
<rect x="14" y="3" width="7" height="7" />
|
| 144 |
+
<rect x="14" y="14" width="7" height="7" />
|
| 145 |
+
<rect x="3" y="14" width="7" height="7" />
|
| 146 |
+
</svg>
|
| 147 |
+
</div>
|
| 148 |
+
<div className="p-5">
|
| 149 |
+
<h3 id={`proj-${project.slug}`} className="h3">
|
| 150 |
+
{project.noDetail ? (
|
| 151 |
+
<span>{project.title}</span>
|
| 152 |
+
) : (
|
| 153 |
+
<Link to={`/project/${project.slug}`} className="hover:underline">
|
| 154 |
+
{project.title}
|
| 155 |
+
</Link>
|
| 156 |
+
)}
|
| 157 |
+
</h3>
|
| 158 |
+
<p className="mt-1 text-sm text-slate-600">{project.location}</p>
|
| 159 |
+
{project.projectSize && (
|
| 160 |
+
<p className="mt-1 text-sm font-medium text-brand-700">
|
| 161 |
+
{project.projectSize}
|
| 162 |
+
</p>
|
| 163 |
+
)}
|
| 164 |
+
<div className="mt-4 flex items-center justify-between">
|
| 165 |
+
<span className="flex flex-wrap items-center gap-2">
|
| 166 |
+
{project.status && (
|
| 167 |
+
<span
|
| 168 |
+
className={`rounded px-2 py-1 text-xs font-medium ${getStatusClasses(project.status)}`}
|
| 169 |
+
aria-label={`Status: ${project.status}`}
|
| 170 |
+
>
|
| 171 |
+
{project.status}
|
| 172 |
+
</span>
|
| 173 |
+
)}
|
| 174 |
+
{(project.categories || []).slice(0, 2).map((c) => (
|
| 175 |
+
<span key={c} className="rounded bg-slate-100 px-2 py-1 text-xs text-slate-700">
|
| 176 |
+
{c}
|
| 177 |
+
</span>
|
| 178 |
+
))}
|
| 179 |
+
</span>
|
| 180 |
+
{!project.noDetail && (
|
| 181 |
+
<Link
|
| 182 |
+
to={`/project/${project.slug}`}
|
| 183 |
+
className="text-sm font-medium text-brand-700 underline-offset-2 hover:underline"
|
| 184 |
+
aria-label={`View ${project.title}`}
|
| 185 |
+
>
|
| 186 |
+
View details
|
| 187 |
+
</Link>
|
| 188 |
+
)}
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</>
|
| 192 |
+
)}
|
| 193 |
+
</article>
|
| 194 |
+
);
|
| 195 |
+
}
|
src/components/Reveal.jsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export default function Reveal({ children, as: Tag = 'div', delay = 0, y = 16 }) {
|
| 4 |
+
const ref = useRef(null);
|
| 5 |
+
const [visible, setVisible] = useState(false);
|
| 6 |
+
|
| 7 |
+
useEffect(() => {
|
| 8 |
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
| 9 |
+
setVisible(true);
|
| 10 |
+
return;
|
| 11 |
+
}
|
| 12 |
+
const el = ref.current;
|
| 13 |
+
if (!el) return;
|
| 14 |
+
const obs = new IntersectionObserver(
|
| 15 |
+
(entries) => {
|
| 16 |
+
entries.forEach((e) => {
|
| 17 |
+
if (e.isIntersecting) {
|
| 18 |
+
setTimeout(() => setVisible(true), delay);
|
| 19 |
+
obs.disconnect();
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
},
|
| 23 |
+
{ threshold: 0.12 }
|
| 24 |
+
);
|
| 25 |
+
obs.observe(el);
|
| 26 |
+
return () => obs.disconnect();
|
| 27 |
+
}, [delay]);
|
| 28 |
+
|
| 29 |
+
const style = visible
|
| 30 |
+
? { transform: 'none', opacity: 1 }
|
| 31 |
+
: { transform: `translateY(${y}px)`, opacity: 0 };
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<Tag ref={ref} style={style} className="transition-all duration-700 ease-out will-change-transform">
|
| 35 |
+
{children}
|
| 36 |
+
</Tag>
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
src/components/ScrollProgress.jsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export default function ScrollProgress() {
|
| 4 |
+
const [progress, setProgress] = useState(0);
|
| 5 |
+
|
| 6 |
+
useEffect(() => {
|
| 7 |
+
const onScroll = () => {
|
| 8 |
+
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
| 9 |
+
const height =
|
| 10 |
+
document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
| 11 |
+
const pct = height > 0 ? Math.min(100, Math.max(0, (scrollTop / height) * 100)) : 0;
|
| 12 |
+
setProgress(pct);
|
| 13 |
+
};
|
| 14 |
+
onScroll();
|
| 15 |
+
window.addEventListener('scroll', onScroll, { passive: true });
|
| 16 |
+
window.addEventListener('resize', onScroll);
|
| 17 |
+
return () => {
|
| 18 |
+
window.removeEventListener('scroll', onScroll);
|
| 19 |
+
window.removeEventListener('resize', onScroll);
|
| 20 |
+
};
|
| 21 |
+
}, []);
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div aria-hidden="true" className="fixed inset-x-0 top-0 z-[60] h-0.5 bg-transparent">
|
| 25 |
+
<div
|
| 26 |
+
className="h-full bg-gradient-to-r from-brand-600 via-accent to-brand-600 transition-[width] duration-150"
|
| 27 |
+
style={{ width: `${progress}%` }}
|
| 28 |
+
/>
|
| 29 |
+
</div>
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
src/components/SectionIntro.jsx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import Reveal from './Reveal.jsx';
|
| 3 |
+
|
| 4 |
+
export default function SectionIntro({ sectionId }) {
|
| 5 |
+
const section = sectionId?.toLowerCase();
|
| 6 |
+
|
| 7 |
+
// Real Estate Development
|
| 8 |
+
if (section === 'development') {
|
| 9 |
+
return (
|
| 10 |
+
<Reveal>
|
| 11 |
+
<div className="mb-16 grid gap-10 md:grid-cols-2 md:items-center">
|
| 12 |
+
<div>
|
| 13 |
+
<div className="mb-4 inline-block rounded-full bg-brand-100 px-4 py-1.5 text-sm font-semibold text-brand-700">
|
| 14 |
+
Real Estate Development
|
| 15 |
+
</div>
|
| 16 |
+
<h2 className="h2 mb-4">Trusted Developers. Visionary Projects. Guaranteed Value.</h2>
|
| 17 |
+
<p className="lead mb-6">
|
| 18 |
+
For over 38 years, we haven't just built structures; we've cultivated trust and delivered lasting value.
|
| 19 |
+
As a leading real estate developer, we specialize in identifying prime <strong>Land Purchase</strong> opportunities
|
| 20 |
+
and executing end-to-end <strong>Property and Project Development</strong>.
|
| 21 |
+
</p>
|
| 22 |
+
<p className="mb-6 text-slate-700">
|
| 23 |
+
Our portfolio includes the successful delivery of <strong>25+ Residential and Commercial projects</strong>. Each
|
| 24 |
+
a testament to our commitment to quality, timely execution, and building vibrant communities. When you partner
|
| 25 |
+
with us, you are investing in a future where <strong>Value, Goodwill, and Trust</strong> are the foundation of
|
| 26 |
+
every square foot.
|
| 27 |
+
</p>
|
| 28 |
+
<div className="mb-6 space-y-3">
|
| 29 |
+
<h3 className="font-semibold text-slate-900">Our Expertise Includes:</h3>
|
| 30 |
+
<ul className="space-y-2 text-slate-700">
|
| 31 |
+
<li className="flex items-start gap-2">
|
| 32 |
+
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-brand-600"></span>
|
| 33 |
+
<span>Strategic Land Acquisition and Aggregation.</span>
|
| 34 |
+
</li>
|
| 35 |
+
<li className="flex items-start gap-2">
|
| 36 |
+
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-brand-600"></span>
|
| 37 |
+
<span>Concept-to-Completion Development of Premium Properties.</span>
|
| 38 |
+
</li>
|
| 39 |
+
<li className="flex items-start gap-2">
|
| 40 |
+
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-brand-600"></span>
|
| 41 |
+
<span>Joint Venture Partnerships for High-Yield Projects.</span>
|
| 42 |
+
</li>
|
| 43 |
+
</ul>
|
| 44 |
+
</div>
|
| 45 |
+
<p className="rounded-lg bg-brand-50 p-4 text-sm font-medium text-brand-800">
|
| 46 |
+
<strong>The Advantage:</strong> A proven record of transforming vision into profitable, high-quality real estate.
|
| 47 |
+
</p>
|
| 48 |
+
</div>
|
| 49 |
+
<div className="card overflow-hidden p-8 bg-gradient-to-br from-brand-50 to-brand-100">
|
| 50 |
+
<div className="space-y-4">
|
| 51 |
+
<div className="flex items-center justify-between rounded-lg bg-white p-4 shadow-sm">
|
| 52 |
+
<span className="text-sm text-slate-600">Years of Experience</span>
|
| 53 |
+
<span className="text-2xl font-bold text-brand-700">38+</span>
|
| 54 |
+
</div>
|
| 55 |
+
<div className="flex items-center justify-between rounded-lg bg-white p-4 shadow-sm">
|
| 56 |
+
<span className="text-sm text-slate-600">Projects Delivered</span>
|
| 57 |
+
<span className="text-2xl font-bold text-brand-700">25+</span>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="flex items-center justify-between rounded-lg bg-white p-4 shadow-sm">
|
| 60 |
+
<span className="text-sm text-slate-600">Focus Areas</span>
|
| 61 |
+
<span className="text-sm font-semibold text-brand-700">Residential & Commercial</span>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</Reveal>
|
| 67 |
+
);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Construction Project Contracting & Management
|
| 71 |
+
if (section === 'construction') {
|
| 72 |
+
return (
|
| 73 |
+
<Reveal>
|
| 74 |
+
<div className="mb-16 grid gap-10 md:grid-cols-2 md:items-center">
|
| 75 |
+
<div className="order-2 md:order-1">
|
| 76 |
+
<div className="card overflow-hidden p-8 bg-gradient-to-br from-brand-50 to-brand-100">
|
| 77 |
+
<div className="mb-6">
|
| 78 |
+
<div className="mb-2 text-5xl font-bold text-brand-700">70+</div>
|
| 79 |
+
<p className="text-sm text-slate-700">Projects Executed on Contract Basis</p>
|
| 80 |
+
</div>
|
| 81 |
+
<div className="h-px bg-brand-200 mb-6"></div>
|
| 82 |
+
<div className="space-y-3 text-sm">
|
| 83 |
+
<div className="flex items-center gap-2">
|
| 84 |
+
<svg className="h-5 w-5 text-brand-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 85 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 86 |
+
</svg>
|
| 87 |
+
<span>End-to-End Project Execution</span>
|
| 88 |
+
</div>
|
| 89 |
+
<div className="flex items-center gap-2">
|
| 90 |
+
<svg className="h-5 w-5 text-brand-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 91 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 92 |
+
</svg>
|
| 93 |
+
<span>Rigorous Project Management</span>
|
| 94 |
+
</div>
|
| 95 |
+
<div className="flex items-center gap-2">
|
| 96 |
+
<svg className="h-5 w-5 text-brand-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 97 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 98 |
+
</svg>
|
| 99 |
+
<span>Quality Assurance & Safety</span>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
<div className="order-1 md:order-2">
|
| 105 |
+
<div className="mb-4 inline-block rounded-full bg-brand-100 px-4 py-1.5 text-sm font-semibold text-brand-700">
|
| 106 |
+
Project Contracting & Management
|
| 107 |
+
</div>
|
| 108 |
+
<h2 className="h2 mb-4">Master Builders. Exceptional Execution. Project Delivery, Perfected.</h2>
|
| 109 |
+
<p className="lead mb-6">
|
| 110 |
+
Experience is the ultimate currency in construction, and after <strong>38+ years</strong> in the industry,
|
| 111 |
+
our experience is unparalleled. We are experts in comprehensive <strong>Construction Project Contracting & Management</strong>,
|
| 112 |
+
offering seamless execution for projects of any scale.
|
| 113 |
+
</p>
|
| 114 |
+
<p className="mb-6 text-slate-700">
|
| 115 |
+
Our credentials speak for themselves: we have successfully undertaken the execution of <strong>70+ projects on a contract basis</strong>.
|
| 116 |
+
This impressive <strong>Quantum of Work</strong> demonstrates not just our capacity, but the depth of our logistical prowess,
|
| 117 |
+
commitment to safety, and ability to deliver complex projects on time and within budget.
|
| 118 |
+
</p>
|
| 119 |
+
<div className="mb-6 space-y-3">
|
| 120 |
+
<h3 className="font-semibold text-slate-900">Services We Offer:</h3>
|
| 121 |
+
<ul className="space-y-2 text-slate-700">
|
| 122 |
+
<li className="flex items-start gap-2">
|
| 123 |
+
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-brand-600"></span>
|
| 124 |
+
<span>End-to-End Project Execution (EPC/Lump-Sum Contracts).</span>
|
| 125 |
+
</li>
|
| 126 |
+
<li className="flex items-start gap-2">
|
| 127 |
+
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-brand-600"></span>
|
| 128 |
+
<span>Rigorous Project Management and Site Supervision.</span>
|
| 129 |
+
</li>
|
| 130 |
+
<li className="flex items-start gap-2">
|
| 131 |
+
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-brand-600"></span>
|
| 132 |
+
<span>Quality Assurance and Safety Compliance.</span>
|
| 133 |
+
</li>
|
| 134 |
+
</ul>
|
| 135 |
+
</div>
|
| 136 |
+
<p className="rounded-lg bg-brand-50 p-4 text-sm font-medium text-brand-800">
|
| 137 |
+
<strong>Your Project in Expert Hands:</strong> Partner with a team whose three decades of expertise guarantees efficiency, precision, and reliable delivery.
|
| 138 |
+
</p>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</Reveal>
|
| 142 |
+
);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Redevelopment
|
| 146 |
+
if (section === 'redevelopment') {
|
| 147 |
+
return (
|
| 148 |
+
<Reveal>
|
| 149 |
+
<div className="mb-16 grid gap-10 md:grid-cols-2 md:items-center">
|
| 150 |
+
<div>
|
| 151 |
+
<div className="mb-4 inline-block rounded-full bg-brand-100 px-4 py-1.5 text-sm font-semibold text-brand-700">
|
| 152 |
+
Redevelopment
|
| 153 |
+
</div>
|
| 154 |
+
<h2 className="h2 mb-4">Reimagine Your Home. Secure Your Investment. The Seamless Redevelopment Solution.</h2>
|
| 155 |
+
<p className="lead mb-6">
|
| 156 |
+
Your old society building holds history, but <strong>Redevelopment</strong> holds the promise of a safer, modern,
|
| 157 |
+
and high-value future. We specialize in the complete <strong>Redevelopment of Existing Old Society Premises and Buildings</strong>,
|
| 158 |
+
turning aged structures into brand-new, architecturally sound, and significantly upgraded homes.
|
| 159 |
+
</p>
|
| 160 |
+
<p className="mb-6 text-slate-700">
|
| 161 |
+
We understand the complexities and sensitivities involved. Our process is transparent, prioritizing the needs and
|
| 162 |
+
seamless transition of all existing tenants and members.
|
| 163 |
+
</p>
|
| 164 |
+
<div className="mb-6 rounded-xl border-2 border-brand-200 bg-gradient-to-br from-brand-50 to-brand-100 p-6">
|
| 165 |
+
<h3 className="mb-4 font-semibold text-slate-900">The Momentum is Building:</h3>
|
| 166 |
+
<div className="grid gap-4 md:grid-cols-3">
|
| 167 |
+
<div className="text-center">
|
| 168 |
+
<div className="text-3xl font-bold text-brand-700">1</div>
|
| 169 |
+
<div className="mt-1 text-sm text-slate-600">Major Project Currently Ongoing</div>
|
| 170 |
+
</div>
|
| 171 |
+
<div className="text-center">
|
| 172 |
+
<div className="text-3xl font-bold text-brand-700">2</div>
|
| 173 |
+
<div className="mt-1 text-sm text-slate-600">Exciting New Projects Upcoming</div>
|
| 174 |
+
</div>
|
| 175 |
+
<div className="text-center">
|
| 176 |
+
<div className="text-3xl font-bold text-brand-700">+</div>
|
| 177 |
+
<div className="mt-1 text-sm text-slate-600">Many More in Pipeline</div>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
<p className="rounded-lg bg-brand-50 p-4 text-sm font-medium text-brand-800">
|
| 182 |
+
<strong>We are here to guide your society through every step</strong>—from legal approvals to handover. Don't just renovate, reimagine.
|
| 183 |
+
</p>
|
| 184 |
+
</div>
|
| 185 |
+
<div className="card overflow-hidden p-8 bg-gradient-to-br from-brand-50 to-brand-100">
|
| 186 |
+
<div className="space-y-4">
|
| 187 |
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
| 188 |
+
<div className="mb-2 flex items-center gap-2">
|
| 189 |
+
<div className="h-3 w-3 rounded-full bg-brand-600"></div>
|
| 190 |
+
<span className="text-sm font-semibold text-slate-900">Transparent Process</span>
|
| 191 |
+
</div>
|
| 192 |
+
<p className="mt-2 text-sm text-slate-600">Clear communication at every stage</p>
|
| 193 |
+
</div>
|
| 194 |
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
| 195 |
+
<div className="mb-2 flex items-center gap-2">
|
| 196 |
+
<div className="h-3 w-3 rounded-full bg-brand-600"></div>
|
| 197 |
+
<span className="text-sm font-semibold text-slate-900">Seamless Transition</span>
|
| 198 |
+
</div>
|
| 199 |
+
<p className="mt-2 text-sm text-slate-600">Minimal disruption to residents</p>
|
| 200 |
+
</div>
|
| 201 |
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
| 202 |
+
<div className="mb-2 flex items-center gap-2">
|
| 203 |
+
<div className="h-3 w-3 rounded-full bg-brand-600"></div>
|
| 204 |
+
<span className="text-sm font-semibold text-slate-900">Legal Expertise</span>
|
| 205 |
+
</div>
|
| 206 |
+
<p className="mt-2 text-sm text-slate-600">Complete guidance on approvals</p>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</Reveal>
|
| 212 |
+
);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// No intro for other sections (SRA, etc.)
|
| 216 |
+
return null;
|
| 217 |
+
}
|
| 218 |
+
|
src/components/ServicesGrid.jsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
import Reveal from './Reveal.jsx';
|
| 4 |
+
import ImageFlex from './ImageFlex.jsx';
|
| 5 |
+
|
| 6 |
+
const SERVICES = [
|
| 7 |
+
{
|
| 8 |
+
id: 'real-estate-development',
|
| 9 |
+
title: 'Real Estate Development',
|
| 10 |
+
to: '/projects/development',
|
| 11 |
+
description:
|
| 12 |
+
'Concept to completion across mixed‑use, residential, commercial, and retail.',
|
| 13 |
+
image: '/assets/cutouts/development.png' // TODO: replace with silhouette cutout
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
id: 'redevelopment',
|
| 17 |
+
title: 'Redevelopment',
|
| 18 |
+
to: '/projects/redevelopment',
|
| 19 |
+
description:
|
| 20 |
+
'Transforming existing assets through structural upgrades and modern amenities.',
|
| 21 |
+
image: '/assets/cutouts/redevelopment.png' // TODO: replace with silhouette cutout
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
id: 'construction',
|
| 25 |
+
title: 'Project Contracting',
|
| 26 |
+
to: '/projects/construction',
|
| 27 |
+
description:
|
| 28 |
+
'Full‑scope civil and structural execution with safety and quality controls.',
|
| 29 |
+
image: '/assets/cutouts/construction.png' // TODO: replace with silhouette cutout
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
id: 'project-management',
|
| 33 |
+
title: 'Project Management',
|
| 34 |
+
to: '/projects/construction',
|
| 35 |
+
description:
|
| 36 |
+
'Planning, procurement, vendor coordination, and reporting for delivery certainty.',
|
| 37 |
+
image: '/assets/cutouts/management.png' // TODO: replace with silhouette cutout
|
| 38 |
+
}
|
| 39 |
+
];
|
| 40 |
+
|
| 41 |
+
export default function ServicesGrid() {
|
| 42 |
+
return (
|
| 43 |
+
<section className="section" aria-labelledby="services-heading">
|
| 44 |
+
<div className="container">
|
| 45 |
+
<header className="mb-10 text-center">
|
| 46 |
+
<h2 id="services-heading" className="h2">Our Expertise</h2>
|
| 47 |
+
<p className="lead mt-3">
|
| 48 |
+
End‑to‑end construction services tailored to residential and commercial needs.
|
| 49 |
+
{/* TODO: replace with jadeinfra.in services summary */}
|
| 50 |
+
</p>
|
| 51 |
+
</header>
|
| 52 |
+
<ul className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4">
|
| 53 |
+
{SERVICES.map((s, i) => (
|
| 54 |
+
<li key={s.id}>
|
| 55 |
+
<Reveal delay={i * 80}>
|
| 56 |
+
<Link
|
| 57 |
+
to={s.to}
|
| 58 |
+
className="group relative block overflow-hidden rounded-2xl border border-slate-200 bg-white
|
| 59 |
+
transition transform hover:-translate-y-1 hover:shadow-lg hover:bg-brand-600 min-h-[300px]
|
| 60 |
+
before:pointer-events-none before:absolute before:inset-0 before:rounded-2xl before:bg-brand-600/0 before:blur before:transition before:duration-300 hover:before:bg-brand-600/10"
|
| 61 |
+
>
|
| 62 |
+
{/* Image cutout */}
|
| 63 |
+
<div
|
| 64 |
+
className="pointer-events-none absolute right-0 bottom-0 z-20 opacity-100"
|
| 65 |
+
aria-hidden="true"
|
| 66 |
+
>
|
| 67 |
+
<div className="relative h-44 w-28 md:w-32 lg:w-36">
|
| 68 |
+
<ImageFlex
|
| 69 |
+
base={s.image && s.image.replace(/\.(jpg|jpeg|png|webp)$/i, '')}
|
| 70 |
+
alt=""
|
| 71 |
+
className="absolute inset-0 h-full w-full object-contain"
|
| 72 |
+
/>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
<div className="relative z-10 p-8">
|
| 76 |
+
<h3 className="h3 relative z-10 transition-colors group-hover:text-white">{s.title}</h3>
|
| 77 |
+
<p className="mt-2 text-sm text-slate-600 relative z-10 transition-colors group-hover:text-white/90">
|
| 78 |
+
{s.description}
|
| 79 |
+
</p>
|
| 80 |
+
<span className="mt-5 inline-flex items-center text-sm font-medium text-brand-700 transition-colors
|
| 81 |
+
group-hover:text-white">
|
| 82 |
+
Learn more →
|
| 83 |
+
</span>
|
| 84 |
+
</div>
|
| 85 |
+
</Link>
|
| 86 |
+
</Reveal>
|
| 87 |
+
</li>
|
| 88 |
+
))}
|
| 89 |
+
</ul>
|
| 90 |
+
</div>
|
| 91 |
+
</section>
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
|
src/components/StatsCounter.jsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
export default function StatsCounter({ value, label, duration = 1200, suffix = '', accent = false, decimals = 0 }) {
|
| 4 |
+
const [display, setDisplay] = useState(0);
|
| 5 |
+
const elRef = useRef(null);
|
| 6 |
+
const startedRef = useRef(false);
|
| 7 |
+
|
| 8 |
+
useEffect(() => {
|
| 9 |
+
const el = elRef.current;
|
| 10 |
+
if (!el) return;
|
| 11 |
+
|
| 12 |
+
const onEnter = (entries) => {
|
| 13 |
+
entries.forEach((entry) => {
|
| 14 |
+
if (entry.isIntersecting && !startedRef.current) {
|
| 15 |
+
startedRef.current = true;
|
| 16 |
+
const start = performance.now();
|
| 17 |
+
const target = Number(value);
|
| 18 |
+
const step = (t) => {
|
| 19 |
+
const p = Math.min(1, (t - start) / duration);
|
| 20 |
+
const raw = p * target;
|
| 21 |
+
if (decimals > 0) {
|
| 22 |
+
setDisplay(Number(raw.toFixed(decimals)));
|
| 23 |
+
} else {
|
| 24 |
+
setDisplay(Math.floor(raw));
|
| 25 |
+
}
|
| 26 |
+
if (p < 1) requestAnimationFrame(step);
|
| 27 |
+
};
|
| 28 |
+
requestAnimationFrame(step);
|
| 29 |
+
}
|
| 30 |
+
});
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const obs = new IntersectionObserver(onEnter, { threshold: 0.4 });
|
| 34 |
+
obs.observe(el);
|
| 35 |
+
return () => obs.disconnect();
|
| 36 |
+
}, [value, duration]);
|
| 37 |
+
|
| 38 |
+
const formatNumber = (n) => {
|
| 39 |
+
if (decimals > 0) {
|
| 40 |
+
// Keep fixed decimals but still use locale for thousands if ever needed
|
| 41 |
+
return Number(n).toLocaleString(undefined, {
|
| 42 |
+
minimumFractionDigits: decimals,
|
| 43 |
+
maximumFractionDigits: decimals
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
return Number(n).toLocaleString();
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const counterContent = (
|
| 50 |
+
<span className="text-4xl font-extrabold text-brand-700">
|
| 51 |
+
{formatNumber(display)}{suffix}
|
| 52 |
+
</span>
|
| 53 |
+
);
|
| 54 |
+
|
| 55 |
+
return (
|
| 56 |
+
<div ref={elRef} className="text-center">
|
| 57 |
+
{accent ? (
|
| 58 |
+
<div className="inline-block rounded-full bg-accent/20 px-6 py-2 shadow-[0_0_18px_rgba(211,155,35,0.35)] backdrop-blur-sm">
|
| 59 |
+
{counterContent}
|
| 60 |
+
</div>
|
| 61 |
+
) : (
|
| 62 |
+
counterContent
|
| 63 |
+
)}
|
| 64 |
+
<div className="mt-3 text-sm text-slate-600">{label}</div>
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
src/components/YouTubeEmbed.jsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Extracts YouTube video ID from various URL formats
|
| 5 |
+
* Supports:
|
| 6 |
+
* - https://www.youtube.com/watch?v=VIDEO_ID
|
| 7 |
+
* - https://youtu.be/VIDEO_ID
|
| 8 |
+
* - https://www.youtube.com/embed/VIDEO_ID
|
| 9 |
+
* - Direct VIDEO_ID
|
| 10 |
+
*/
|
| 11 |
+
function extractVideoId(url) {
|
| 12 |
+
if (!url) return null;
|
| 13 |
+
|
| 14 |
+
// If it's already just an ID (no URL)
|
| 15 |
+
if (!url.includes('http') && !url.includes('youtube') && !url.includes('youtu.be')) {
|
| 16 |
+
return url;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Extract from various YouTube URL formats
|
| 20 |
+
const patterns = [
|
| 21 |
+
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
|
| 22 |
+
/^([a-zA-Z0-9_-]{11})$/ // Direct ID
|
| 23 |
+
];
|
| 24 |
+
|
| 25 |
+
for (const pattern of patterns) {
|
| 26 |
+
const match = url.match(pattern);
|
| 27 |
+
if (match && match[1]) {
|
| 28 |
+
return match[1];
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return null;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export default function YouTubeEmbed({ url, title = 'Project walkthrough video' }) {
|
| 36 |
+
const videoId = extractVideoId(url);
|
| 37 |
+
|
| 38 |
+
if (!videoId) {
|
| 39 |
+
return null;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const embedUrl = `https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1`;
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<section className="mt-8" aria-labelledby="walkthrough-heading">
|
| 46 |
+
<h2 id="walkthrough-heading" className="h3 mb-4">Walkthrough Video</h2>
|
| 47 |
+
<div className="relative aspect-video overflow-hidden rounded-xl bg-slate-100">
|
| 48 |
+
<iframe
|
| 49 |
+
src={embedUrl}
|
| 50 |
+
title={title}
|
| 51 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 52 |
+
allowFullScreen
|
| 53 |
+
className="absolute inset-0 w-full h-full"
|
| 54 |
+
loading="lazy"
|
| 55 |
+
/>
|
| 56 |
+
</div>
|
| 57 |
+
</section>
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
|
src/global/style.css
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Tailwind directives are in src/styles/global.css. This file uses plain CSS only. */
|
| 2 |
+
|
| 3 |
+
/* Base resets and variables */
|
| 4 |
+
:root {
|
| 5 |
+
--container-max: 1200px;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
html {
|
| 9 |
+
scroll-behavior: smooth;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* Keep body styling minimal here; Tailwind handles typography/colors */
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
.container-max {
|
| 16 |
+
max-width: var(--container-max);
|
| 17 |
+
margin-left: auto;
|
| 18 |
+
margin-right: auto;
|
| 19 |
+
padding-left: 1rem;
|
| 20 |
+
padding-right: 1rem;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* Class hooks are provided by Tailwind in src/styles/global.css */
|
src/main.jsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { createRoot } from 'react-dom/client';
|
| 3 |
+
import { BrowserRouter } from 'react-router-dom';
|
| 4 |
+
import App from './App.jsx';
|
| 5 |
+
import './styles/global.css';
|
| 6 |
+
|
| 7 |
+
const root = document.getElementById('root');
|
| 8 |
+
createRoot(root).render(
|
| 9 |
+
<React.StrictMode>
|
| 10 |
+
<BrowserRouter>
|
| 11 |
+
<App />
|
| 12 |
+
</BrowserRouter>
|
| 13 |
+
</React.StrictMode>
|
| 14 |
+
);
|
src/pages/About.jsx
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
import Reveal from '../components/Reveal.jsx';
|
| 3 |
+
import ImageFlex from '../components/ImageFlex.jsx';
|
| 4 |
+
|
| 5 |
+
function TimelineItem({ align = 'left', year, title, children, lineHeight = 0 }) {
|
| 6 |
+
const ref = useRef(null);
|
| 7 |
+
const yearRef = useRef(null);
|
| 8 |
+
const [visible, setVisible] = useState(false);
|
| 9 |
+
const [isEnlarged, setIsEnlarged] = useState(false);
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
const el = ref.current;
|
| 13 |
+
if (!el) return;
|
| 14 |
+
const obs = new IntersectionObserver(
|
| 15 |
+
([entry]) => {
|
| 16 |
+
if (entry.isIntersecting) {
|
| 17 |
+
setVisible(true);
|
| 18 |
+
obs.unobserve(entry.target);
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
{ threshold: 0.3 }
|
| 22 |
+
);
|
| 23 |
+
obs.observe(el);
|
| 24 |
+
return () => obs.disconnect();
|
| 25 |
+
}, []);
|
| 26 |
+
|
| 27 |
+
useEffect(() => {
|
| 28 |
+
const yearEl = yearRef.current;
|
| 29 |
+
const timelineEl = yearEl?.closest('[data-timeline-container]');
|
| 30 |
+
if (!yearEl || !timelineEl) return;
|
| 31 |
+
|
| 32 |
+
const updateEnlarged = () => {
|
| 33 |
+
const timelineRect = timelineEl.getBoundingClientRect();
|
| 34 |
+
const yearRect = yearEl.getBoundingClientRect();
|
| 35 |
+
// Calculate year badge position relative to timeline container top
|
| 36 |
+
const yearPositionFromTop = yearRect.top - timelineRect.top + (yearRect.height / 2);
|
| 37 |
+
|
| 38 |
+
// Check if the line has reached or passed the year badge center
|
| 39 |
+
const reached = lineHeight >= yearPositionFromTop;
|
| 40 |
+
setIsEnlarged(reached);
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
updateEnlarged();
|
| 44 |
+
// Also update on scroll/resize
|
| 45 |
+
window.addEventListener('scroll', updateEnlarged, { passive: true });
|
| 46 |
+
window.addEventListener('resize', updateEnlarged);
|
| 47 |
+
return () => {
|
| 48 |
+
window.removeEventListener('scroll', updateEnlarged);
|
| 49 |
+
window.removeEventListener('resize', updateEnlarged);
|
| 50 |
+
};
|
| 51 |
+
}, [lineHeight]);
|
| 52 |
+
|
| 53 |
+
const card = (
|
| 54 |
+
<div
|
| 55 |
+
ref={ref}
|
| 56 |
+
className={
|
| 57 |
+
'card relative bg-white p-6 md:p-8 transition ' +
|
| 58 |
+
(visible
|
| 59 |
+
? 'shadow-[0_12px_40px_rgba(0,0,0,0.08)] ring-1 ring-brand-600/20'
|
| 60 |
+
: 'shadow-card')
|
| 61 |
+
}
|
| 62 |
+
>
|
| 63 |
+
<h3 className="h3">{title}</h3>
|
| 64 |
+
<div className="mt-3 text-slate-700">{children}</div>
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
|
| 68 |
+
const yearBadge = (
|
| 69 |
+
<div className="sticky top-28" ref={yearRef}>
|
| 70 |
+
<div
|
| 71 |
+
className={`inline-flex items-center justify-center rounded-full bg-brand-600 px-3 py-1.5 text-sm font-semibold text-white shadow-lg transition-all duration-500 ease-out ${
|
| 72 |
+
isEnlarged ? 'scale-125 md:scale-150' : 'scale-100'
|
| 73 |
+
}`}
|
| 74 |
+
>
|
| 75 |
+
{year}
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
);
|
| 79 |
+
|
| 80 |
+
return (
|
| 81 |
+
<div className="relative grid grid-cols-1 items-start gap-4 md:grid-cols-2">
|
| 82 |
+
{align === 'left' ? (
|
| 83 |
+
<>
|
| 84 |
+
<div className="md:pr-8">{card}</div>
|
| 85 |
+
<div className="md:pl-8 md:text-left">{yearBadge}</div>
|
| 86 |
+
</>
|
| 87 |
+
) : (
|
| 88 |
+
<>
|
| 89 |
+
<div className="order-2 md:order-2 md:pl-8">{card}</div>
|
| 90 |
+
<div className="order-1 md:order-1 md:pr-8 md:text-right">{yearBadge}</div>
|
| 91 |
+
</>
|
| 92 |
+
)}
|
| 93 |
+
</div>
|
| 94 |
+
);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export default function About() {
|
| 98 |
+
const historyRef = useRef(null);
|
| 99 |
+
const [lineH, setLineH] = useState(0);
|
| 100 |
+
|
| 101 |
+
useEffect(() => {
|
| 102 |
+
const el = historyRef.current;
|
| 103 |
+
if (!el) return;
|
| 104 |
+
const onScroll = () => {
|
| 105 |
+
const rect = el.getBoundingClientRect();
|
| 106 |
+
const viewport = window.innerHeight || 0;
|
| 107 |
+
// progress of section revealed in viewport
|
| 108 |
+
const revealed = Math.min(rect.height, Math.max(0, viewport - rect.top));
|
| 109 |
+
// Keep a visual margin while scrolling, but allow completion near the end
|
| 110 |
+
const buffer = 120; // px margin from bottom during scroll
|
| 111 |
+
const threshold = rect.height - buffer;
|
| 112 |
+
const grow =
|
| 113 |
+
revealed >= threshold
|
| 114 |
+
? rect.height // complete the line when near the end
|
| 115 |
+
: Math.max(0, revealed - buffer);
|
| 116 |
+
setLineH(grow);
|
| 117 |
+
};
|
| 118 |
+
onScroll();
|
| 119 |
+
window.addEventListener('scroll', onScroll, { passive: true });
|
| 120 |
+
window.addEventListener('resize', onScroll);
|
| 121 |
+
return () => {
|
| 122 |
+
window.removeEventListener('scroll', onScroll);
|
| 123 |
+
window.removeEventListener('resize', onScroll);
|
| 124 |
+
};
|
| 125 |
+
}, []);
|
| 126 |
+
|
| 127 |
+
return (
|
| 128 |
+
<article className="section" aria-labelledby="about-heading">
|
| 129 |
+
<div className="container">
|
| 130 |
+
<Reveal>
|
| 131 |
+
<div className="mb-4 inline-block rounded-full bg-brand-100 px-4 py-1.5 text-sm font-semibold text-brand-700">
|
| 132 |
+
Building Trust Since 1987
|
| 133 |
+
</div>
|
| 134 |
+
<h1 id="about-heading" className="h2 mb-8">About Jade Infra</h1>
|
| 135 |
+
</Reveal>
|
| 136 |
+
|
| 137 |
+
<div className="grid gap-10 md:grid-cols-2 md:items-center">
|
| 138 |
+
<Reveal>
|
| 139 |
+
<div className="space-y-6">
|
| 140 |
+
<p className="text-lg leading-relaxed text-slate-700">
|
| 141 |
+
For over three decades, Jade Infra has been a cornerstone of the infrastructure and real estate landscape.
|
| 142 |
+
Founded on principles of integrity and engineering excellence, we have grown from a specialized construction
|
| 143 |
+
contractor to a holistic developer, manager, and redeveloper of commercial and residential assets.
|
| 144 |
+
</p>
|
| 145 |
+
<p className="text-lg leading-relaxed text-slate-700">
|
| 146 |
+
With over <strong className="text-brand-700">75 successful projects</strong> (25+ developed, 70+ contracted) to our name,
|
| 147 |
+
we pride ourselves on a proven track record of delivering quality, guaranteeing value, and fostering long-term
|
| 148 |
+
trust with every stakeholder.
|
| 149 |
+
</p>
|
| 150 |
+
<div className="grid grid-cols-3 gap-4 pt-4">
|
| 151 |
+
<div className="text-center rounded-lg bg-brand-50 p-4">
|
| 152 |
+
<div className="text-3xl font-bold text-brand-700">75+</div>
|
| 153 |
+
<div className="mt-1 text-xs text-slate-600">Total Projects</div>
|
| 154 |
+
</div>
|
| 155 |
+
<div className="text-center rounded-lg bg-brand-50 p-4">
|
| 156 |
+
<div className="text-3xl font-bold text-brand-700">38+</div>
|
| 157 |
+
<div className="mt-1 text-xs text-slate-600">Years</div>
|
| 158 |
+
</div>
|
| 159 |
+
<div className="text-center rounded-lg bg-brand-50 p-4">
|
| 160 |
+
<div className="text-3xl font-bold text-brand-700">25+</div>
|
| 161 |
+
<div className="mt-1 text-xs text-slate-600">Developed</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
</Reveal>
|
| 166 |
+
<Reveal delay={120}>
|
| 167 |
+
<div className="card overflow-hidden shadow-lg">
|
| 168 |
+
<ImageFlex
|
| 169 |
+
base="/assets/rbtojade"
|
| 170 |
+
alt="Jade Infra - Building Trust Since 1987"
|
| 171 |
+
className="h-96 w-full object-cover"
|
| 172 |
+
/>
|
| 173 |
+
</div>
|
| 174 |
+
</Reveal>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
{/* Our History */}
|
| 178 |
+
<section className="mt-16" aria-labelledby="history-heading">
|
| 179 |
+
<div className="container">
|
| 180 |
+
<Reveal>
|
| 181 |
+
<header className="mb-12 text-center">
|
| 182 |
+
<h2 id="history-heading" className="h2">Our History</h2>
|
| 183 |
+
<p className="lead mt-2">From R.B. Constructions to Jade Infra</p>
|
| 184 |
+
</header>
|
| 185 |
+
</Reveal>
|
| 186 |
+
|
| 187 |
+
<div className="relative pb-20" ref={historyRef} data-timeline-container>
|
| 188 |
+
{/* Vertical timeline line with scroll growth */}
|
| 189 |
+
<div
|
| 190 |
+
className="pointer-events-none absolute left-1/2 top-0 bottom-16 -translate-x-1/2 w-[3px] bg-brand-600/15"
|
| 191 |
+
aria-hidden="true"
|
| 192 |
+
>
|
| 193 |
+
<div
|
| 194 |
+
className="absolute left-0 top-0 h-0 w-full rounded-full bg-brand-600/70 transition-[height] duration-300 ease-out will-change-[height] shadow-[0_0_10px_rgba(0,119,70,0.35)]"
|
| 195 |
+
style={{ height: `${lineH}px` }}
|
| 196 |
+
/>
|
| 197 |
+
</div>
|
| 198 |
+
<div className="space-y-10">
|
| 199 |
+
<Reveal>
|
| 200 |
+
<TimelineItem align="left" year="1987" title="The Foundation: R.B. Constructions" lineHeight={lineH}>
|
| 201 |
+
<p>
|
| 202 |
+
The story of Jade Infra is a journey spanning over three decades, built on a foundation of hard work,
|
| 203 |
+
meticulous growth, and a dedicated workforce. Our legacy began when Mr. Bharat B. Jain, drawing on
|
| 204 |
+
experience gained since 1983 with M/S. Kumar Group (now Kumar Properties), incorporated the proprietorship
|
| 205 |
+
firm R.B. Constructions, Civil Engineers & Contractors. Since its inception, the company established a
|
| 206 |
+
reputation for overseeing the execution of contracting works with dedication and hard work, successfully
|
| 207 |
+
transitioning into a reputable civil contractor and developer.
|
| 208 |
+
</p>
|
| 209 |
+
</TimelineItem>
|
| 210 |
+
</Reveal>
|
| 211 |
+
<Reveal delay={80}>
|
| 212 |
+
<TimelineItem align="right" year="1995" title="Growth and Expansion" lineHeight={lineH}>
|
| 213 |
+
<p>
|
| 214 |
+
The journey of growth continued when Mr. Jayesh Jain, a Diploma holder in Civil Engineering, joined
|
| 215 |
+
the venture after several years as a Site Supervisor and Project Incharge. Working together under R.B.
|
| 216 |
+
Constructions and various sister firms, the brothers collectively made a significant mark in the real
|
| 217 |
+
estate and property development sector for over 25 years. Mr. Jayesh Jain's technical expertise ensured
|
| 218 |
+
a consistent focus on precision, quality, and maintaining high standards in execution.
|
| 219 |
+
</p>
|
| 220 |
+
</TimelineItem>
|
| 221 |
+
</Reveal>
|
| 222 |
+
<Reveal delay={120}>
|
| 223 |
+
<TimelineItem align="left" year="2012" title="A New Identity: Jade Infra" lineHeight={lineH}>
|
| 224 |
+
<p>
|
| 225 |
+
To keep pace with changing times and skylines while honoring its established legacy, a new firm was
|
| 226 |
+
established: Jade Infra. This new identity was born as Mr. Ritesh Jain, a Bachelor of Architecture,
|
| 227 |
+
joined the business. Having worked as an architect in Mumbai, Mr. Ritesh Jain's arrival initiated a
|
| 228 |
+
complete branding makeover, bringing a modernist outlook and focusing on design, legal, and financial aspects.
|
| 229 |
+
</p>
|
| 230 |
+
</TimelineItem>
|
| 231 |
+
</Reveal>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</section>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
{/* Our Brand Story */}
|
| 239 |
+
<section className="mt-16" aria-labelledby="brand-story-heading">
|
| 240 |
+
<div className="container">
|
| 241 |
+
<Reveal>
|
| 242 |
+
<header className="mb-8 text-center">
|
| 243 |
+
<h2 id="brand-story-heading" className="h2">Our Brand Story</h2>
|
| 244 |
+
<p className="lead mt-2">Adapting the Legacy</p>
|
| 245 |
+
</header>
|
| 246 |
+
</Reveal>
|
| 247 |
+
<Reveal>
|
| 248 |
+
<div className="card p-6 md:p-10 bg-white shadow-card">
|
| 249 |
+
<p className="text-slate-700">
|
| 250 |
+
The transition to <strong>Jade Infra</strong> was driven by the principles of
|
| 251 |
+
<strong> Adapt</strong>, <strong> Design Driven</strong>, <strong> Meticulous</strong>,
|
| 252 |
+
<strong> Approachable</strong>, and <strong> Socially Responsible</strong>. Our new identity
|
| 253 |
+
integrates the prudence, simplicity, and confidence developed over time. We honor the legacy of
|
| 254 |
+
R.B. Constructions while focusing on continual up‑gradation in infrastructure, innovative design,
|
| 255 |
+
and in‑depth market study.
|
| 256 |
+
</p>
|
| 257 |
+
<p className="mt-4 text-slate-700">
|
| 258 |
+
Today, with a strong foundation, ample site experience, and practical knowledge, Jade Infra focuses
|
| 259 |
+
on <strong>Turn‑Key Projects & Development</strong>, with a vision to be one of the finest real estate
|
| 260 |
+
and property developers and a torchbearer for making Pune a “Smart Pune.”
|
| 261 |
+
</p>
|
| 262 |
+
</div>
|
| 263 |
+
</Reveal>
|
| 264 |
+
</div>
|
| 265 |
+
</section>
|
| 266 |
+
|
| 267 |
+
{/* Our Leadership */}
|
| 268 |
+
<div className="mt-20">
|
| 269 |
+
<Reveal>
|
| 270 |
+
<header className="mb-10 text-center">
|
| 271 |
+
<h2 className="h2">Our Leadership</h2>
|
| 272 |
+
<p className="lead mt-3">A foundation of experience and vision</p>
|
| 273 |
+
</header>
|
| 274 |
+
</Reveal>
|
| 275 |
+
<div className="space-y-10">
|
| 276 |
+
{/* Bharat B. Jain (align left) */}
|
| 277 |
+
<Reveal>
|
| 278 |
+
<article className="overflow-hidden rounded-2xl border border-slate-200 bg-gradient-to-br from-brand-50 to-white shadow-card md:mr-auto">
|
| 279 |
+
<div className="flex flex-col md:flex-row md:items-center">
|
| 280 |
+
<div className="md:w-[38%] flex justify-center md:justify-start p-6 md:pl-10 md:pr-0">
|
| 281 |
+
<div className="relative w-full max-w-xs">
|
| 282 |
+
<div className="absolute -top-8 -left-6 hidden h-40 w-40 rounded-full bg-brand-200/60 blur-3xl md:block"></div>
|
| 283 |
+
<ImageFlex
|
| 284 |
+
base="/assets/leadership/bharat"
|
| 285 |
+
alt="Mr. Bharat B. Jain"
|
| 286 |
+
className="relative z-10 mx-auto max-h-64 w-auto object-contain"
|
| 287 |
+
/>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
<div className="relative md:w-[62%] md:pl-10 md:pr-20 md:py-10 p-6">
|
| 291 |
+
<div className="mb-2 inline-block rounded-full bg-brand-100 px-3 py-1 text-xs font-semibold text-brand-700">Finance & Strategy</div>
|
| 292 |
+
<h3 className="h3">Mr. Bharat B. Jain</h3>
|
| 293 |
+
<p className="text-sm font-semibold text-brand-700">The Founder & Financial Strategist</p>
|
| 294 |
+
<p className="mt-3 text-slate-700">
|
| 295 |
+
Since founding the predecessor firm in 1987, Mr. Bharat B. Jain has been the cornerstone of the
|
| 296 |
+
organization's success. Drawing on early experience from 1983, he has transitioned from a dedicated
|
| 297 |
+
Civil Contractor to a robust Real Estate Developer.
|
| 298 |
+
</p>
|
| 299 |
+
<p className="mt-2 text-slate-700">
|
| 300 |
+
Today, he strategically manages Legal, Finance, and Accounts, ensuring fiscal strength and regulatory
|
| 301 |
+
compliance across all ongoing projects. He is the ultimate guardian of the company's value and goodwill.
|
| 302 |
+
</p>
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
</article>
|
| 306 |
+
</Reveal>
|
| 307 |
+
|
| 308 |
+
{/* Jayesh B. Jain (align right) */}
|
| 309 |
+
<Reveal delay={80}>
|
| 310 |
+
<article className="overflow-hidden rounded-2xl border border-slate-200 bg-gradient-to-br from-brand-50 to-white shadow-card md:ml-auto">
|
| 311 |
+
<div className="flex flex-col md:flex-row md:items-center">
|
| 312 |
+
<div className="order-2 md:order-2 md:w-[38%] flex justify-center md:justify-end p-6 md:pr-10 md:pl-0">
|
| 313 |
+
<div className="relative w-full max-w-xs">
|
| 314 |
+
<div className="absolute -top-6 -right-6 hidden h-40 w-40 rounded-full bg-brand-200/60 blur-3xl md:block"></div>
|
| 315 |
+
<ImageFlex
|
| 316 |
+
base="/assets/leadership/jayesh"
|
| 317 |
+
alt="Mr. Jayesh B. Jain"
|
| 318 |
+
className="relative z-10 mx-auto max-h-64 w-auto object-contain"
|
| 319 |
+
/>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
<div className="order-1 md:order-1 md:w-[62%] md:pr-20 md:pl-10 md:py-10 p-6">
|
| 323 |
+
<div className="mb-2 inline-block rounded-full bg-brand-100 px-3 py-1 text-xs font-semibold text-brand-700">Execution & Quality</div>
|
| 324 |
+
<h3 className="h3">Mr. Jayesh B. Jain</h3>
|
| 325 |
+
<p className="text-sm font-semibold text-brand-700">The Technical Head & Execution Master</p>
|
| 326 |
+
<p className="mt-3 text-slate-700">
|
| 327 |
+
Joining the leadership in 1995, Mr. Jayesh B. Jain provides the technical mastery that defines our
|
| 328 |
+
construction quality. Being a civil engineer, he is renowned for precision and practical know‑how.
|
| 329 |
+
</p>
|
| 330 |
+
<p className="mt-2 text-slate-700">
|
| 331 |
+
He is highly experienced in the single‑handed execution of civil works, ensuring each project
|
| 332 |
+
is delivered with the highest quality standards and technical integrity, on time and to specification.
|
| 333 |
+
</p>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
</article>
|
| 337 |
+
</Reveal>
|
| 338 |
+
|
| 339 |
+
{/* Ritesh B. Jain (align left again) */}
|
| 340 |
+
<Reveal delay={160}>
|
| 341 |
+
<article className="overflow-hidden rounded-2xl border border-slate-200 bg-gradient-to-br from-brand-50 to-white shadow-card md:mr-auto">
|
| 342 |
+
<div className="flex flex-col md:flex-row md:items-center">
|
| 343 |
+
<div className="md:w-[38%] flex justify-center md:justify-start p-6 md:pl-10 md:pr-0">
|
| 344 |
+
<div className="relative w-full max-w-xs">
|
| 345 |
+
<div className="absolute -top-6 -left-6 hidden h-40 w-40 rounded-full bg-brand-200/60 blur-3xl md:block"></div>
|
| 346 |
+
<ImageFlex
|
| 347 |
+
base="/assets/leadership/ritesh"
|
| 348 |
+
alt="Mr. Ritesh B. Jain"
|
| 349 |
+
className="relative z-10 mx-auto max-h-64 w-auto object-contain"
|
| 350 |
+
/>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
<div className="md:w-[62%] md:pl-10 md:pr-20 md:py-10 p-6">
|
| 354 |
+
<div className="mb-2 inline-block rounded-full bg-brand-100 px-3 py-1 text-xs font-semibold text-brand-700">Design & Brand</div>
|
| 355 |
+
<h3 className="h3">Mr. Ritesh B. Jain</h3>
|
| 356 |
+
<p className="text-sm font-semibold text-brand-700">The Visionary & Design Strategist</p>
|
| 357 |
+
<p className="mt-3 text-slate-700">
|
| 358 |
+
Mr. Ritesh B. Jain, B.Arch, brings a modern, aesthetic, and forward‑thinking perspective since 2012.
|
| 359 |
+
After experience as an Architect in Mumbai, he spearheaded the rebranding and transition to Jade Infra.
|
| 360 |
+
</p>
|
| 361 |
+
<p className="mt-2 text-slate-700">
|
| 362 |
+
His expertise spans Design, Aesthetics, and Branding, while also overseeing aspects of Legal and Finance,
|
| 363 |
+
ensuring developments are structurally sound, visually appealing, and strategically aligned with market needs.
|
| 364 |
+
</p>
|
| 365 |
+
</div>
|
| 366 |
+
</div>
|
| 367 |
+
</article>
|
| 368 |
+
</Reveal>
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
+
|
| 372 |
+
{/* Vision, Mission & Values (moved to end) */}
|
| 373 |
+
<section className="mt-20" aria-labelledby="mvv-heading">
|
| 374 |
+
<div className="container">
|
| 375 |
+
<Reveal>
|
| 376 |
+
<header className="mb-12 text-center">
|
| 377 |
+
<h2 id="mvv-heading" className="h2">Vision, Mission & Values</h2>
|
| 378 |
+
<p className="lead mt-3">The principles that guide everything we do</p>
|
| 379 |
+
</header>
|
| 380 |
+
</Reveal>
|
| 381 |
+
<div className="grid gap-8 md:grid-cols-3">
|
| 382 |
+
<Reveal>
|
| 383 |
+
<section className="card p-8 bg-white hover:shadow-lg transition-shadow" aria-labelledby="vision-title">
|
| 384 |
+
<div className="mb-4 flex items-center gap-3">
|
| 385 |
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-brand-100">
|
| 386 |
+
<svg className="h-6 w-6 text-brand-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 387 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 388 |
+
<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" />
|
| 389 |
+
</svg>
|
| 390 |
+
</div>
|
| 391 |
+
<h2 id="vision-title" className="h3">Vision</h2>
|
| 392 |
+
</div>
|
| 393 |
+
<p className="mt-4 leading-relaxed text-slate-700">
|
| 394 |
+
To be the leading and most trusted integrated real estate and infrastructure group, known for transforming
|
| 395 |
+
urban landscapes and consistently setting the benchmark for quality, sustainability, and value creation
|
| 396 |
+
in every project we undertake.
|
| 397 |
+
</p>
|
| 398 |
+
</section>
|
| 399 |
+
</Reveal>
|
| 400 |
+
<Reveal delay={80}>
|
| 401 |
+
<section className="card p-8 bg-white hover:shadow-lg transition-shadow" aria-labelledby="mission-title">
|
| 402 |
+
<div className="mb-4 flex items-center gap-3">
|
| 403 |
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-slate-900">
|
| 404 |
+
<svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 405 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
| 406 |
+
</svg>
|
| 407 |
+
</div>
|
| 408 |
+
<h2 id="mission-title" className="h3">Mission</h2>
|
| 409 |
+
</div>
|
| 410 |
+
<p className="mt-4 leading-relaxed text-slate-700">
|
| 411 |
+
To deliver superior value across all our services, from strategic Land Development and large-scale
|
| 412 |
+
Construction Management to community-focused Redevelopment. We achieve this by committing to engineering
|
| 413 |
+
precision, financial transparency, and timely delivery, ensuring the security and satisfaction of our
|
| 414 |
+
clients and partners.
|
| 415 |
+
</p>
|
| 416 |
+
</section>
|
| 417 |
+
</Reveal>
|
| 418 |
+
<Reveal delay={160}>
|
| 419 |
+
<section className="card p-8 bg-white hover:shadow-lg transition-shadow" aria-labelledby="values-title">
|
| 420 |
+
<div className="mb-4 flex items-center gap-3">
|
| 421 |
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-accent/10">
|
| 422 |
+
<svg className="h-6 w-6 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 423 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
| 424 |
+
</svg>
|
| 425 |
+
</div>
|
| 426 |
+
<h2 id="values-title" className="h3">Values</h2>
|
| 427 |
+
</div>
|
| 428 |
+
<div className="mt-4 space-y-3">
|
| 429 |
+
<div className="flex items-start gap-2">
|
| 430 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 431 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 432 |
+
<strong className="text-slate-900">Integrity:</strong> Operating with complete honesty and transparency to build mutual trust.
|
| 433 |
+
</p>
|
| 434 |
+
</div>
|
| 435 |
+
<div className="flex items-start gap-2">
|
| 436 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 437 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 438 |
+
<strong className="text-slate-900">Engineering Excellence:</strong> Committing to the highest standards of quality and safety in every structure.
|
| 439 |
+
</p>
|
| 440 |
+
</div>
|
| 441 |
+
<div className="flex items-start gap-2">
|
| 442 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 443 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 444 |
+
<strong className="text-slate-900">Value Creation:</strong> Maximizing return on investment for all stakeholders.
|
| 445 |
+
</p>
|
| 446 |
+
</div>
|
| 447 |
+
<div className="flex items-start gap-2">
|
| 448 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 449 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 450 |
+
<strong className="text-slate-900">Accountability:</strong> Delivering reliably on deadlines and budgets with unwavering ownership.
|
| 451 |
+
</p>
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
</section>
|
| 455 |
+
</Reveal>
|
| 456 |
+
</div>
|
| 457 |
+
</div>
|
| 458 |
+
</section>
|
| 459 |
+
</article>
|
| 460 |
+
);
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
|
src/pages/Contact.jsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ContactForm from '../components/ContactForm.jsx';
|
| 3 |
+
|
| 4 |
+
export default function Contact() {
|
| 5 |
+
return (
|
| 6 |
+
<section className="section" aria-labelledby="contact-heading">
|
| 7 |
+
<div className="container grid gap-10 md:grid-cols-2">
|
| 8 |
+
<header>
|
| 9 |
+
<h1 id="contact-heading" className="h2">Contact Us</h1>
|
| 10 |
+
<p className="lead mt-3">
|
| 11 |
+
Reach out for project enquiries, partnerships, or general information.
|
| 12 |
+
</p>
|
| 13 |
+
<div className="mt-6 rounded-lg bg-slate-50 p-6">
|
| 14 |
+
<h2 className="h3">Office</h2>
|
| 15 |
+
<address className="mt-2 not-italic text-slate-700">
|
| 16 |
+
Address:{' '}
|
| 17 |
+
<a
|
| 18 |
+
className="text-brand-700 hover:underline"
|
| 19 |
+
href="https://maps.app.goo.gl/4jSpBviv91E6DYCS9"
|
| 20 |
+
target="_blank"
|
| 21 |
+
rel="noopener noreferrer"
|
| 22 |
+
>
|
| 23 |
+
Click here to view our office location
|
| 24 |
+
</a>
|
| 25 |
+
<br />
|
| 26 |
+
Phone: <a className="text-brand-700" href="tel:+919673009729">+91 96730 09729</a>
|
| 27 |
+
<br />
|
| 28 |
+
Email: <a className="text-brand-700" href="mailto:jadeinfrapune@gmail.com">jadeinfrapune@gmail.com</a>
|
| 29 |
+
</address>
|
| 30 |
+
</div>
|
| 31 |
+
</header>
|
| 32 |
+
<div className="card p-6">
|
| 33 |
+
<ContactForm />
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</section>
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
src/pages/Home.jsx
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo } from 'react';
|
| 2 |
+
import Hero from '../components/Hero.jsx';
|
| 3 |
+
import Reveal from '../components/Reveal.jsx';
|
| 4 |
+
import ServicesGrid from '../components/ServicesGrid.jsx';
|
| 5 |
+
import StatsCounter from '../components/StatsCounter.jsx';
|
| 6 |
+
import ProjectCard from '../components/ProjectCard.jsx';
|
| 7 |
+
import ImageFlex from '../components/ImageFlex.jsx';
|
| 8 |
+
import { Link } from 'react-router-dom';
|
| 9 |
+
import { ALL_PROJECTS } from './Projects.jsx';
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Algorithm to select featured projects:
|
| 13 |
+
* 1. Prioritize ongoing projects, if they don't fill all slots, add recently completed projects
|
| 14 |
+
* 2. If ongoing projects exceed slots, prioritize by project size (sq.ft.)
|
| 15 |
+
* 3. Exclude Construction section projects
|
| 16 |
+
* 4. Return exactly the requested count
|
| 17 |
+
*/
|
| 18 |
+
function getFeaturedProjects(count = 3) {
|
| 19 |
+
// Filter out Construction section projects
|
| 20 |
+
const eligible = ALL_PROJECTS.filter((p) => p.section?.toLowerCase() !== 'construction');
|
| 21 |
+
|
| 22 |
+
// Separate Ongoing and Completed projects
|
| 23 |
+
const ongoing = eligible.filter((p) => p.status === 'Ongoing');
|
| 24 |
+
const completed = eligible.filter((p) => p.status === 'Completed');
|
| 25 |
+
|
| 26 |
+
// Helper to extract numeric value from projectSize (e.g., "50,000 sq.ft." -> 50000)
|
| 27 |
+
const getSizeValue = (projectSize) => {
|
| 28 |
+
if (!projectSize) return 0;
|
| 29 |
+
const match = projectSize.toString().replace(/,/g, '').match(/(\d+)/);
|
| 30 |
+
return match ? parseInt(match[1], 10) : 0;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
// Sort Ongoing by project size (biggest first)
|
| 34 |
+
const sortedOngoing = [...ongoing].sort((a, b) => {
|
| 35 |
+
const sizeA = getSizeValue(a.projectSize);
|
| 36 |
+
const sizeB = getSizeValue(b.projectSize);
|
| 37 |
+
return sizeB - sizeA; // Descending order
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
// Sort Completed by completion date (most recent first)
|
| 41 |
+
const sortedCompleted = [...completed].sort((a, b) => {
|
| 42 |
+
const dateA = a.completionDate ? parseInt(a.completionDate, 10) : 0;
|
| 43 |
+
const dateB = b.completionDate ? parseInt(b.completionDate, 10) : 0;
|
| 44 |
+
return dateB - dateA; // Descending order (most recent first)
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
// Select featured projects
|
| 48 |
+
const featured = [];
|
| 49 |
+
|
| 50 |
+
// First, add ongoing projects (up to count)
|
| 51 |
+
featured.push(...sortedOngoing.slice(0, count));
|
| 52 |
+
|
| 53 |
+
// If we need more, add recently completed projects
|
| 54 |
+
if (featured.length < count) {
|
| 55 |
+
const remaining = count - featured.length;
|
| 56 |
+
featured.push(...sortedCompleted.slice(0, remaining));
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return featured.slice(0, count);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export default function Home() {
|
| 63 |
+
// Get featured projects using the algorithm
|
| 64 |
+
const featuredProjects = useMemo(() => getFeaturedProjects(3), []);
|
| 65 |
+
|
| 66 |
+
return (
|
| 67 |
+
<>
|
| 68 |
+
<Hero />
|
| 69 |
+
|
| 70 |
+
{/* Featured Projects - overlapping hero banner */}
|
| 71 |
+
<section className="section relative -mt-10 md:-mt-14 lg:-mt-16 z-20" aria-labelledby="featured-heading">
|
| 72 |
+
<div className="container">
|
| 73 |
+
<header className="mb-10 text-center">
|
| 74 |
+
<h2 id="featured-heading" className="h2">Featured Projects</h2>
|
| 75 |
+
<p className="lead mt-3">
|
| 76 |
+
A snapshot of our recent residential and commercial work.
|
| 77 |
+
</p>
|
| 78 |
+
</header>
|
| 79 |
+
<ul className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
| 80 |
+
{featuredProjects.map((p, i) => (
|
| 81 |
+
<li key={p.slug}>
|
| 82 |
+
<Reveal delay={i * 80}>
|
| 83 |
+
<ProjectCard project={p} />
|
| 84 |
+
</Reveal>
|
| 85 |
+
</li>
|
| 86 |
+
))}
|
| 87 |
+
</ul>
|
| 88 |
+
</div>
|
| 89 |
+
</section>
|
| 90 |
+
|
| 91 |
+
<section className="section" aria-labelledby="stats-heading">
|
| 92 |
+
<div className="container">
|
| 93 |
+
<Reveal>
|
| 94 |
+
<header className="mb-10 text-center">
|
| 95 |
+
<h2 id="stats-heading" className="h2">Our Track Record</h2>
|
| 96 |
+
<p className="lead mt-3">Numbers that reflect our commitment and scale.</p>
|
| 97 |
+
</header>
|
| 98 |
+
</Reveal>
|
| 99 |
+
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
| 100 |
+
<Reveal delay={0}>
|
| 101 |
+
<StatsCounter value={75} label="Projects Delivered" suffix="+" accent />
|
| 102 |
+
</Reveal>
|
| 103 |
+
<Reveal delay={100}>
|
| 104 |
+
<StatsCounter value={6} label="Ongoing Projects" accent />
|
| 105 |
+
</Reveal>
|
| 106 |
+
<Reveal delay={200}>
|
| 107 |
+
<StatsCounter value={38} label="Years of Experience" suffix="+" accent />
|
| 108 |
+
</Reveal>
|
| 109 |
+
<Reveal delay={300}>
|
| 110 |
+
<StatsCounter value={3.8} label="sq.ft. delivered" suffix="+ Million" accent decimals={1} />
|
| 111 |
+
</Reveal>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</section>
|
| 115 |
+
|
| 116 |
+
<section className="section relative z-10 bg-gradient-to-b from-white to-slate-50" aria-labelledby="about-home-heading" style={{ marginTop: 0 }}>
|
| 117 |
+
<div className="container">
|
| 118 |
+
<Reveal>
|
| 119 |
+
<div className="mb-4 inline-block rounded-full bg-brand-100 px-4 py-1.5 text-sm font-semibold text-brand-700">
|
| 120 |
+
Building Trust Since 1987
|
| 121 |
+
</div>
|
| 122 |
+
<h2 id="about-home-heading" className="h2 mb-6">About Jade Infra</h2>
|
| 123 |
+
</Reveal>
|
| 124 |
+
<div className="grid items-center gap-10 md:grid-cols-2">
|
| 125 |
+
<Reveal>
|
| 126 |
+
<p className="text-lg leading-relaxed text-slate-700">
|
| 127 |
+
For over three decades, Jade Infra has been a cornerstone of the infrastructure and real estate landscape.
|
| 128 |
+
Founded on principles of integrity and engineering excellence, we have grown from a specialized construction
|
| 129 |
+
contractor to a holistic developer, manager, and redeveloper of commercial and residential assets.
|
| 130 |
+
</p>
|
| 131 |
+
<p className="mt-4 text-lg leading-relaxed text-slate-700">
|
| 132 |
+
With over <strong className="text-brand-700">75 successful projects</strong> (25+ developed, 70+ contracted) to our name,
|
| 133 |
+
we pride ourselves on a proven track record of delivering quality, guaranteeing value, and fostering long-term
|
| 134 |
+
trust with every stakeholder.
|
| 135 |
+
</p>
|
| 136 |
+
<div className="mt-6">
|
| 137 |
+
<Link to="/about" className="btn btn-primary">Learn more</Link>
|
| 138 |
+
</div>
|
| 139 |
+
</Reveal>
|
| 140 |
+
<Reveal delay={120}>
|
| 141 |
+
<div className="card overflow-hidden shadow-lg">
|
| 142 |
+
<ImageFlex
|
| 143 |
+
base="/assets/rbtojade"
|
| 144 |
+
alt="Jade Infra - Building Trust Since 1987"
|
| 145 |
+
className="h-96 w-full object-cover"
|
| 146 |
+
/>
|
| 147 |
+
</div>
|
| 148 |
+
</Reveal>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</section>
|
| 152 |
+
|
| 153 |
+
<section className="section bg-slate-50" aria-labelledby="bvv-heading">
|
| 154 |
+
<div className="container">
|
| 155 |
+
<header className="mb-12 text-center">
|
| 156 |
+
<h2 id="bvv-heading" className="h2">Vision, Mission & Values</h2>
|
| 157 |
+
<p className="lead mt-3">The principles that guide everything we do</p>
|
| 158 |
+
</header>
|
| 159 |
+
<div className="grid gap-8 md:grid-cols-3">
|
| 160 |
+
<Reveal>
|
| 161 |
+
<section className="card p-8 bg-white hover:shadow-lg transition-shadow" aria-labelledby="vision-title">
|
| 162 |
+
<div className="mb-4 flex items-center gap-3">
|
| 163 |
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-brand-100">
|
| 164 |
+
<svg className="h-6 w-6 text-brand-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 165 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 166 |
+
<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" />
|
| 167 |
+
</svg>
|
| 168 |
+
</div>
|
| 169 |
+
<h3 id="vision-title" className="h3">Vision</h3>
|
| 170 |
+
</div>
|
| 171 |
+
<p className="mt-4 leading-relaxed text-slate-700">
|
| 172 |
+
To be the leading and most trusted integrated real estate and infrastructure group, known for transforming
|
| 173 |
+
urban landscapes and consistently setting the benchmark for quality, sustainability, and value creation
|
| 174 |
+
in every project we undertake.
|
| 175 |
+
</p>
|
| 176 |
+
</section>
|
| 177 |
+
</Reveal>
|
| 178 |
+
<Reveal delay={80}>
|
| 179 |
+
<section className="card p-8 bg-white hover:shadow-lg transition-shadow" aria-labelledby="mission-title">
|
| 180 |
+
<div className="mb-4 flex items-center gap-3">
|
| 181 |
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-slate-900">
|
| 182 |
+
<svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 183 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
| 184 |
+
</svg>
|
| 185 |
+
</div>
|
| 186 |
+
<h3 id="mission-title" className="h3">Mission</h3>
|
| 187 |
+
</div>
|
| 188 |
+
<p className="mt-4 leading-relaxed text-slate-700">
|
| 189 |
+
To deliver superior value across all our services, from strategic Land Development and large-scale
|
| 190 |
+
Construction Management to community-focused Redevelopment. We achieve this by committing to engineering
|
| 191 |
+
precision, financial transparency, and timely delivery, ensuring the security and satisfaction of our
|
| 192 |
+
clients and partners.
|
| 193 |
+
</p>
|
| 194 |
+
</section>
|
| 195 |
+
</Reveal>
|
| 196 |
+
<Reveal delay={160}>
|
| 197 |
+
<section className="card p-8 bg-white hover:shadow-lg transition-shadow" aria-labelledby="values-title">
|
| 198 |
+
<div className="mb-4 flex items-center gap-3">
|
| 199 |
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-accent/10">
|
| 200 |
+
<svg className="h-6 w-6 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 201 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
| 202 |
+
</svg>
|
| 203 |
+
</div>
|
| 204 |
+
<h3 id="values-title" className="h3">Values</h3>
|
| 205 |
+
</div>
|
| 206 |
+
<div className="mt-4 space-y-3">
|
| 207 |
+
<div className="flex items-start gap-2">
|
| 208 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 209 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 210 |
+
<strong className="text-slate-900">Integrity:</strong> Operating with complete honesty and transparency to build mutual trust.
|
| 211 |
+
</p>
|
| 212 |
+
</div>
|
| 213 |
+
<div className="flex items-start gap-2">
|
| 214 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 215 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 216 |
+
<strong className="text-slate-900">Engineering Excellence:</strong> Committing to the highest standards of quality and safety in every structure.
|
| 217 |
+
</p>
|
| 218 |
+
</div>
|
| 219 |
+
<div className="flex items-start gap-2">
|
| 220 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 221 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 222 |
+
<strong className="text-slate-900">Value Creation:</strong> Maximizing return on investment for all stakeholders.
|
| 223 |
+
</p>
|
| 224 |
+
</div>
|
| 225 |
+
<div className="flex items-start gap-2">
|
| 226 |
+
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand-600"></span>
|
| 227 |
+
<p className="text-sm leading-relaxed text-slate-700">
|
| 228 |
+
<strong className="text-slate-900">Accountability:</strong> Delivering reliably on deadlines and budgets with unwavering ownership.
|
| 229 |
+
</p>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
</section>
|
| 233 |
+
</Reveal>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</section>
|
| 237 |
+
|
| 238 |
+
<ServicesGrid />
|
| 239 |
+
|
| 240 |
+
<section className="section" aria-labelledby="cta-heading">
|
| 241 |
+
<div className="container grid items-center gap-8 rounded-2xl bg-brand-700 px-8 py-12 text-white md:grid-cols-2">
|
| 242 |
+
<Reveal>
|
| 243 |
+
<div>
|
| 244 |
+
<h2 id="cta-heading" className="h2 text-white">Collaborate with us</h2>
|
| 245 |
+
<p className="mt-2 text-white/90">
|
| 246 |
+
Tell us about your project and we'll get back within 7 business days.
|
| 247 |
+
</p>
|
| 248 |
+
</div>
|
| 249 |
+
</Reveal>
|
| 250 |
+
<Reveal delay={150}>
|
| 251 |
+
<div className="text-right">
|
| 252 |
+
<Link to="/contact" className="btn btn-ghost border border-white/30">
|
| 253 |
+
Contact Us
|
| 254 |
+
</Link>
|
| 255 |
+
</div>
|
| 256 |
+
</Reveal>
|
| 257 |
+
</div>
|
| 258 |
+
</section>
|
| 259 |
+
</>
|
| 260 |
+
);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
|
src/pages/ProjectDetail.jsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useState } from 'react';
|
| 2 |
+
import { useParams, Link } from 'react-router-dom';
|
| 3 |
+
import { ALL_PROJECTS } from './Projects.jsx';
|
| 4 |
+
import ImageSlider from '../components/ImageSlider.jsx';
|
| 5 |
+
import YouTubeEmbed from '../components/YouTubeEmbed.jsx';
|
| 6 |
+
import MapEmbed from '../components/MapEmbed.jsx';
|
| 7 |
+
|
| 8 |
+
export default function ProjectDetail() {
|
| 9 |
+
const { slug } = useParams();
|
| 10 |
+
const project = ALL_PROJECTS.find((p) => p.slug === slug);
|
| 11 |
+
const [detectedGallery, setDetectedGallery] = useState([]);
|
| 12 |
+
|
| 13 |
+
// Auto-detect gallery images if not provided in data
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
if (!project) return;
|
| 16 |
+
if (project.gallery && project.gallery.length > 0) return;
|
| 17 |
+
|
| 18 |
+
const maxToCheck = 12; // check up to 12 images by convention
|
| 19 |
+
const bases = Array.from({ length: maxToCheck }, (_, i) => `/assets/projects/${project.slug}/gallery-${i + 1}`);
|
| 20 |
+
|
| 21 |
+
const extensions = ['webp', 'jpg', 'jpeg', 'png'];
|
| 22 |
+
const existsCache = new Set();
|
| 23 |
+
|
| 24 |
+
const checks = bases.map((base) => {
|
| 25 |
+
return new Promise((resolve) => {
|
| 26 |
+
let settled = false;
|
| 27 |
+
for (const ext of extensions) {
|
| 28 |
+
const img = new Image();
|
| 29 |
+
img.onload = () => {
|
| 30 |
+
if (!settled) {
|
| 31 |
+
existsCache.add(base);
|
| 32 |
+
settled = true;
|
| 33 |
+
resolve(true);
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
img.onerror = () => {
|
| 37 |
+
// ignore
|
| 38 |
+
};
|
| 39 |
+
img.src = `${base}.${ext}`;
|
| 40 |
+
}
|
| 41 |
+
// Give 1.5s for loads; then resolve
|
| 42 |
+
setTimeout(() => {
|
| 43 |
+
if (!settled) resolve(false);
|
| 44 |
+
}, 1500);
|
| 45 |
+
});
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
Promise.all(checks).then(() => {
|
| 49 |
+
setDetectedGallery(Array.from(existsCache));
|
| 50 |
+
});
|
| 51 |
+
}, [project]);
|
| 52 |
+
|
| 53 |
+
if (!project) {
|
| 54 |
+
return (
|
| 55 |
+
<section className="section">
|
| 56 |
+
<div className="container text-center">
|
| 57 |
+
<h1 className="h2">Project not found</h1>
|
| 58 |
+
<p className="mt-2 text-slate-600">The project you’re looking for doesn’t exist.</p>
|
| 59 |
+
<Link to="/" className="btn btn-primary mt-6">Go home</Link>
|
| 60 |
+
</div>
|
| 61 |
+
</section>
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
if (project.noDetail) {
|
| 66 |
+
return (
|
| 67 |
+
<section className="section">
|
| 68 |
+
<div className="container text-center">
|
| 69 |
+
<h1 className="h2">{project.title}</h1>
|
| 70 |
+
<p className="mt-2 text-slate-600">
|
| 71 |
+
Detailed information for this contract project is not available online yet.
|
| 72 |
+
</p>
|
| 73 |
+
<Link
|
| 74 |
+
to={`/projects/${project.section?.toLowerCase() || 'development'}`}
|
| 75 |
+
className="btn btn-primary mt-6"
|
| 76 |
+
>
|
| 77 |
+
Back to {project.section || 'Projects'}
|
| 78 |
+
</Link>
|
| 79 |
+
</div>
|
| 80 |
+
</section>
|
| 81 |
+
);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
return (
|
| 85 |
+
<article className="section" aria-labelledby="project-title">
|
| 86 |
+
<div className="container">
|
| 87 |
+
<nav className="mb-6 text-sm text-slate-600" aria-label="Breadcrumb">
|
| 88 |
+
<ol className="flex items-center gap-2">
|
| 89 |
+
<li><Link className="hover:underline" to="/">Home</Link></li>
|
| 90 |
+
<li>/</li>
|
| 91 |
+
<li><Link className="hover:underline" to={`/projects/${project.section?.toLowerCase()}`}>{project.section}</Link></li>
|
| 92 |
+
<li>/</li>
|
| 93 |
+
<li aria-current="page" className="text-slate-900 font-medium">{project.title}</li>
|
| 94 |
+
</ol>
|
| 95 |
+
</nav>
|
| 96 |
+
|
| 97 |
+
<header>
|
| 98 |
+
<h1 id="project-title" className="h2">{project.title}</h1>
|
| 99 |
+
<p className="mt-1 text-slate-600">
|
| 100 |
+
{project.location} • {(project.categories || []).join(' + ')}
|
| 101 |
+
</p>
|
| 102 |
+
</header>
|
| 103 |
+
|
| 104 |
+
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-3">
|
| 105 |
+
<div className="md:col-span-2">
|
| 106 |
+
{/* Image Slider */}
|
| 107 |
+
{(() => {
|
| 108 |
+
const images = (project.gallery && project.gallery.length > 0) ? project.gallery : detectedGallery;
|
| 109 |
+
if (images && images.length > 0) {
|
| 110 |
+
return <ImageSlider images={images} projectTitle={project.title} />;
|
| 111 |
+
}
|
| 112 |
+
return (
|
| 113 |
+
<div className="overflow-hidden rounded-xl bg-slate-100 aspect-video flex items-center justify-center">
|
| 114 |
+
<p className="text-slate-400">No images available</p>
|
| 115 |
+
</div>
|
| 116 |
+
);
|
| 117 |
+
})()}
|
| 118 |
+
|
| 119 |
+
<section className="mt-6">
|
| 120 |
+
<h2 className="h3">Overview</h2>
|
| 121 |
+
<p className="mt-2 text-slate-700">
|
| 122 |
+
{project.description}
|
| 123 |
+
</p>
|
| 124 |
+
</section>
|
| 125 |
+
|
| 126 |
+
{/* Walkthrough Video - Only show if YouTube link is provided */}
|
| 127 |
+
{project.youtubeUrl && (
|
| 128 |
+
<YouTubeEmbed url={project.youtubeUrl} title={`${project.title} - Walkthrough Video`} />
|
| 129 |
+
)}
|
| 130 |
+
</div>
|
| 131 |
+
<aside className="card p-6">
|
| 132 |
+
<h2 className="h3">Key Details</h2>
|
| 133 |
+
<ul className="mt-3 space-y-2 text-sm text-slate-700">
|
| 134 |
+
<li><strong>Location:</strong> {project.location}</li>
|
| 135 |
+
{project.projectSize && (
|
| 136 |
+
<li><strong>Project Size:</strong> {project.projectSize}</li>
|
| 137 |
+
)}
|
| 138 |
+
<li><strong>Status:</strong> {project.status || 'REPLACE_ME'}</li>
|
| 139 |
+
{project.status === 'Upcoming' ? (
|
| 140 |
+
<li><strong>Planned Commencement Date:</strong> {project.plannedCommencement || 'REPLACE_ME'}</li>
|
| 141 |
+
) : (
|
| 142 |
+
<li><strong>Commencement Date:</strong> {project.commencement || 'REPLACE_ME'}</li>
|
| 143 |
+
)}
|
| 144 |
+
{project.status === 'Completed' && (
|
| 145 |
+
<li><strong>Completion Date:</strong> {project.completionDate || 'REPLACE_ME'}</li>
|
| 146 |
+
)}
|
| 147 |
+
{project.status === 'Ongoing' && (
|
| 148 |
+
<li><strong>Completion as per RERA:</strong> {project.reraCompletion || 'REPLACE_ME'}</li>
|
| 149 |
+
)}
|
| 150 |
+
{(() => {
|
| 151 |
+
const commenceYear = project.commencement ? parseInt(project.commencement) : null;
|
| 152 |
+
if (commenceYear && commenceYear >= 2021 && (project.status === 'Completed' || project.status === 'Ongoing')) {
|
| 153 |
+
return <li><strong>MAHARERA No.:</strong> {project.reraNo || 'REPLACE_ME'}</li>;
|
| 154 |
+
}
|
| 155 |
+
return null;
|
| 156 |
+
})()}
|
| 157 |
+
</ul>
|
| 158 |
+
{project.status === 'Ongoing' && (
|
| 159 |
+
<div className="mt-6">
|
| 160 |
+
<Link to="/contact" className="btn btn-primary w-full">Enquire Now</Link>
|
| 161 |
+
</div>
|
| 162 |
+
)}
|
| 163 |
+
</aside>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
{/* Map below details/enquire */}
|
| 167 |
+
<div className="mt-6 md:mt-10">
|
| 168 |
+
<MapEmbed address={project.location} title={`${project.title} location`} />
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</article>
|
| 172 |
+
);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
|
src/pages/Projects.jsx
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useState } from 'react';
|
| 2 |
+
import { useLocation } from 'react-router-dom';
|
| 3 |
+
import ProjectCard from '../components/ProjectCard.jsx';
|
| 4 |
+
import Reveal from '../components/Reveal.jsx';
|
| 5 |
+
|
| 6 |
+
export const ALL_PROJECTS = [
|
| 7 |
+
{
|
| 8 |
+
slug: 'siddhesh-yashoda',
|
| 9 |
+
title: 'Siddhesh Yashoda',
|
| 10 |
+
location: 'Kothrud, Pune',
|
| 11 |
+
categories: ['Residential'],
|
| 12 |
+
section: 'Redevelopment',
|
| 13 |
+
status: 'Ongoing',
|
| 14 |
+
commencement: '2023',
|
| 15 |
+
reraCompletion: '2026',
|
| 16 |
+
reraNo: 'P52100054124',
|
| 17 |
+
image: '/assets/projects/siddhesh-yashoda/card',
|
| 18 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 19 |
+
description:
|
| 20 |
+
'A Redevelopment Project comprising of 2BHK & 3BHK flats. MAHARera No.: P52100054124 (2023–2026).',
|
| 21 |
+
// Gallery images - array of image paths (without extension, ImageFlex will handle .webp, .jpg, .jpeg, .png)
|
| 22 |
+
gallery: [
|
| 23 |
+
'/assets/projects/siddhesh-yashoda/gallery-1',
|
| 24 |
+
'/assets/projects/siddhesh-yashoda/gallery-2',
|
| 25 |
+
'/assets/projects/siddhesh-yashoda/gallery-3',
|
| 26 |
+
'/assets/projects/siddhesh-yashoda/gallery-4',
|
| 27 |
+
'/assets/projects/siddhesh-yashoda/gallery-5',
|
| 28 |
+
'/assets/projects/siddhesh-yashoda/gallery-6'
|
| 29 |
+
],
|
| 30 |
+
projectSize: '67,400 sq.ft.',
|
| 31 |
+
// YouTube video URL or ID - supports full URLs or just the video ID
|
| 32 |
+
// Examples: 'https://www.youtube.com/watch?v=VIDEO_ID' or 'https://youtu.be/VIDEO_ID' or just 'VIDEO_ID'
|
| 33 |
+
youtubeUrl: '' // TODO: Add YouTube link if available
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
slug: 'jito-nagar',
|
| 37 |
+
title: 'JITO Nagar',
|
| 38 |
+
location: 'Kondhwa Khurd, Pune',
|
| 39 |
+
categories: ['Residential'],
|
| 40 |
+
section: 'Development',
|
| 41 |
+
status: 'Ongoing',
|
| 42 |
+
commencement: '2022',
|
| 43 |
+
reraCompletion: '2026',
|
| 44 |
+
reraNo: 'P52100047920',
|
| 45 |
+
image: '/assets/projects/jito-nagar/card',
|
| 46 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 47 |
+
projectSize: '210,000 sq.ft.',
|
| 48 |
+
description:
|
| 49 |
+
'PMAY project with 477 compact 1BHK flats in association with JITO Pune Chapter. MAHARera No.: P52100047920 (2022–2026).',
|
| 50 |
+
// Gallery images for JITO Nagar
|
| 51 |
+
gallery: [
|
| 52 |
+
'/assets/projects/jito-nagar/gallery-1',
|
| 53 |
+
'/assets/projects/jito-nagar/gallery-2',
|
| 54 |
+
'/assets/projects/jito-nagar/gallery-3',
|
| 55 |
+
'/assets/projects/jito-nagar/gallery-4',
|
| 56 |
+
'/assets/projects/jito-nagar/gallery-5',
|
| 57 |
+
'/assets/projects/jito-nagar/gallery-6',
|
| 58 |
+
'/assets/projects/jito-nagar/gallery-7',
|
| 59 |
+
]
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
slug: 'synergy',
|
| 63 |
+
title: 'Synergy',
|
| 64 |
+
location: 'Salisbury Park, Pune',
|
| 65 |
+
categories: ['Residential'],
|
| 66 |
+
section: 'Construction',
|
| 67 |
+
status: 'Completed',
|
| 68 |
+
commencement: '2021',
|
| 69 |
+
completionDate: '2023',
|
| 70 |
+
reraNo: 'REPLACE_ME',
|
| 71 |
+
image: '/assets/projects/synergy/card',
|
| 72 |
+
projectSize: '136,000 sq.ft.',
|
| 73 |
+
description: 'Residential construction project developed by Raviraj Realty (2021–2023).'
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
slug: 'eisha-pearl',
|
| 77 |
+
title: 'Eisha Pearl',
|
| 78 |
+
location: 'Gangadham, Kondhwa Khurd, Pune',
|
| 79 |
+
categories: ['Residential'],
|
| 80 |
+
section: 'Development',
|
| 81 |
+
status: 'Completed',
|
| 82 |
+
commencement: '2010',
|
| 83 |
+
completionDate: '2018',
|
| 84 |
+
image: '/assets/projects/eisha-pearl/card',
|
| 85 |
+
description:
|
| 86 |
+
'Residential scheme of ~550 flats (1/2/3 BHK), central open space ~50,000 sq.ft., clubhouse, Jain Temple, and modern amenities (2010–2018).',
|
| 87 |
+
projectSize: '500,000 sq.ft.'
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
slug: 'vardhamanpura-phase-1-2',
|
| 91 |
+
title: 'Vardhamanpura Phase 1 & 2',
|
| 92 |
+
location: 'Gultekdi, Market Yard, Pune',
|
| 93 |
+
categories: ['Residential'],
|
| 94 |
+
section: 'Development',
|
| 95 |
+
status: 'Completed',
|
| 96 |
+
commencement: '2008',
|
| 97 |
+
completionDate: '2016',
|
| 98 |
+
image: '/assets/projects/vardhamanpura/card',
|
| 99 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 100 |
+
projectSize: '800,000 sq.ft.',
|
| 101 |
+
description:
|
| 102 |
+
'460 flats with 2/3/4 BHK luxurious homes; Jain Temple and amenities. Phase 1 (2008–2012), Phase 2 (2014–2016).'
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
slug: 'highway-bliss',
|
| 106 |
+
title: 'Highway Bliss',
|
| 107 |
+
location: 'Ambegaon Budruk, Pune',
|
| 108 |
+
categories: ['Mixed Use'],
|
| 109 |
+
section: 'Development',
|
| 110 |
+
status: 'Completed',
|
| 111 |
+
commencement: '2014',
|
| 112 |
+
completionDate: '2017',
|
| 113 |
+
image: '/assets/projects/highway-bliss/card',
|
| 114 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 115 |
+
projectSize: '114,000 sq.ft.',
|
| 116 |
+
description:
|
| 117 |
+
'3 wings, 129 flats with commercial spaces (2014–2017). 1, 1.5 & 2 BHK homes in a well-connected serene setting.'
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
slug: 'suyog-isha-hills',
|
| 121 |
+
title: 'Suyog Isha Hills',
|
| 122 |
+
location: 'Bhilar, Mahabaleshwar',
|
| 123 |
+
categories: ['Residential'],
|
| 124 |
+
section: 'Development',
|
| 125 |
+
status: 'Completed',
|
| 126 |
+
completionDate: '2008',
|
| 127 |
+
image: '/assets/projects/suyog-isha-hills/card',
|
| 128 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 129 |
+
projectSize: '290,000 sq.ft.',
|
| 130 |
+
description: 'Studio apartments on a sloping site with scenic valley view (Completed 2008).'
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
slug: 'isha-emerald-phase-1-2',
|
| 134 |
+
title: 'Isha Emerald Phase 1 & 2',
|
| 135 |
+
location: 'Lullanagar–Bibwewadi Road, Market Yard, Pune',
|
| 136 |
+
categories: ['Residential'],
|
| 137 |
+
section: 'Development',
|
| 138 |
+
status: 'Completed',
|
| 139 |
+
commencement: '2006',
|
| 140 |
+
completionDate: '2009',
|
| 141 |
+
image: '/assets/projects/isha-emerald/card',
|
| 142 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 143 |
+
projectSize: '288,000 sq.ft.',
|
| 144 |
+
description:
|
| 145 |
+
'Phase 1: 18 bungalow plots + 172 flats. Phase 2 (E-Building): 22 super luxurious flats. Jain Temple, clubhouse, landscaped area (2006–2009).'
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
slug: 'isha-sangam',
|
| 149 |
+
title: 'Isha Sangam',
|
| 150 |
+
location: 'Lullanagar–Bibwewadi Road, Market Yard, Pune',
|
| 151 |
+
categories: ['Residential'],
|
| 152 |
+
section: 'Development',
|
| 153 |
+
status: 'Completed',
|
| 154 |
+
image: '/assets/projects/isha-sangam/card',
|
| 155 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 156 |
+
projectSize: '70,000 sq.ft.',
|
| 157 |
+
description:
|
| 158 |
+
'Residential project of 56 flats in a fast-growing area with strong city connectivity.'
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
slug: 'antara',
|
| 162 |
+
title: 'Antara',
|
| 163 |
+
location: 'Nanded City, Sinhagad Road, Pune',
|
| 164 |
+
categories: ['Residential'],
|
| 165 |
+
section: 'Construction',
|
| 166 |
+
status: 'Ongoing',
|
| 167 |
+
image: '/assets/projects/antara/gallery-1',
|
| 168 |
+
gallery: ['/assets/projects/antara/gallery-1'],
|
| 169 |
+
projectSize: '26,000 sq.ft.',
|
| 170 |
+
description: 'Residential construction project developed by Nanded City Development & Construction Company Limited.'
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
slug: 'siddhi-new-kusum',
|
| 174 |
+
title: 'Siddhi New Kusum',
|
| 175 |
+
location: 'Shankar Sheth Road, Pune',
|
| 176 |
+
categories: ['Residential'],
|
| 177 |
+
section: 'Construction',
|
| 178 |
+
status: 'Ongoing',
|
| 179 |
+
image: '/assets/projects/siddhi-new-kusum/gallery-1',
|
| 180 |
+
gallery: ['/assets/projects/siddhi-new-kusum/gallery-1'],
|
| 181 |
+
projectSize: '135,000 sq.ft.',
|
| 182 |
+
description: 'Residential construction project by Bhairav Promoters Unit-4.'
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
slug: 'kumar-parv',
|
| 186 |
+
title: 'KUMAR PARV',
|
| 187 |
+
location: 'Moshi, PCMC',
|
| 188 |
+
categories: ['Residential'],
|
| 189 |
+
section: 'Construction',
|
| 190 |
+
status: 'Ongoing',
|
| 191 |
+
image: '/assets/projects/kumar-parv/gallery-1',
|
| 192 |
+
gallery: ['/assets/projects/kumar-parv/gallery-1'],
|
| 193 |
+
projectSize: '82,000 sq.ft.',
|
| 194 |
+
description: 'Residential construction by Kumar Properties Life Space Pvt. Ltd.'
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
slug: 'anmol',
|
| 198 |
+
title: 'ANMOL',
|
| 199 |
+
location: 'Salisbury Park, Pune',
|
| 200 |
+
categories: ['Residential'],
|
| 201 |
+
section: 'Construction',
|
| 202 |
+
status: 'Ongoing',
|
| 203 |
+
image: '/assets/projects/anmol/gallery-1',
|
| 204 |
+
gallery: ['/assets/projects/anmol/gallery-1'],
|
| 205 |
+
projectSize: '56,600 sq.ft.',
|
| 206 |
+
description: 'Residential construction by Builtwell Constructions.'
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
slug: 'nityanand-apartment',
|
| 210 |
+
title: 'Nityanand Apartment',
|
| 211 |
+
location: '592, Budhwar Peth, Pune',
|
| 212 |
+
categories: ['Residential'],
|
| 213 |
+
section: 'Construction',
|
| 214 |
+
status: 'Completed',
|
| 215 |
+
commencement: '1998',
|
| 216 |
+
completionDate: '1999',
|
| 217 |
+
projectSize: '9,000 sq.ft.',
|
| 218 |
+
description:
|
| 219 |
+
'Contract works for Parmar Bhandari & Associates at 592, Budhwar Peth (Aug 1998 - Dec 1999).',
|
| 220 |
+
noDetail: true
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
slug: 'wadki-warehouse',
|
| 224 |
+
title: 'Warehouse',
|
| 225 |
+
location: 'Wadki Nala, Fursungi Road, Pune',
|
| 226 |
+
categories: ['Commercial'],
|
| 227 |
+
section: 'Construction',
|
| 228 |
+
status: 'Completed',
|
| 229 |
+
commencement: '1998',
|
| 230 |
+
completionDate: '1999',
|
| 231 |
+
projectSize: '16,000 sq.ft.',
|
| 232 |
+
description:
|
| 233 |
+
'Logistics facility delivered for Ashok Warehousing Corp at Wadki Nala (Aug 1998 - Sept 1999).',
|
| 234 |
+
noDetail: true
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
slug: 'twin-towers-wanawadi',
|
| 238 |
+
title: 'Twin Towers',
|
| 239 |
+
location: 'Wanawadi, Pune',
|
| 240 |
+
categories: ['Residential'],
|
| 241 |
+
section: 'Construction',
|
| 242 |
+
status: 'Completed',
|
| 243 |
+
commencement: '1998',
|
| 244 |
+
completionDate: '2002',
|
| 245 |
+
projectSize: '18,000 sq.ft.',
|
| 246 |
+
description:
|
| 247 |
+
'Turnkey structural execution for S. S. Builder at Wanawadi (May 1998 - Aug 2002).',
|
| 248 |
+
noDetail: true
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
slug: 'maxim-pharmaceutical-lab',
|
| 252 |
+
title: 'Pharmaceutical Lab',
|
| 253 |
+
location: 'Alandi - Markal Road, Pune',
|
| 254 |
+
categories: ['Commercial'],
|
| 255 |
+
section: 'Construction',
|
| 256 |
+
status: 'Completed',
|
| 257 |
+
commencement: '1998',
|
| 258 |
+
completionDate: '1999',
|
| 259 |
+
projectSize: '13,000 sq.ft.',
|
| 260 |
+
description:
|
| 261 |
+
'Specialised laboratory facilities constructed for Maxim Pharmacy at Markal (Mar 1998 - Feb 1999).',
|
| 262 |
+
noDetail: true
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
slug: 'arihant-palace',
|
| 266 |
+
title: 'Arihant Palace',
|
| 267 |
+
location: 'Market Yard, Pune',
|
| 268 |
+
categories: ['Residential'],
|
| 269 |
+
section: 'Construction',
|
| 270 |
+
status: 'Completed',
|
| 271 |
+
commencement: '1998',
|
| 272 |
+
completionDate: '2000',
|
| 273 |
+
projectSize: '64,000 sq.ft.',
|
| 274 |
+
description:
|
| 275 |
+
'High-rise residential development for Arihant Co-Op. Housing Society (Feb 1998 - Dec 2000).',
|
| 276 |
+
noDetail: true
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
slug: 'legend-classic',
|
| 280 |
+
title: 'Legend Classic',
|
| 281 |
+
location: 'Kothrud, Pune',
|
| 282 |
+
categories: ['Residential'],
|
| 283 |
+
section: 'Construction',
|
| 284 |
+
status: 'Completed',
|
| 285 |
+
commencement: '1998',
|
| 286 |
+
completionDate: '2001',
|
| 287 |
+
projectSize: '31,240 sq.ft.',
|
| 288 |
+
description:
|
| 289 |
+
'Premium residences constructed for Legend Builders in Kothrud (Jul 1998 - Feb 2001).',
|
| 290 |
+
noDetail: true
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
slug: 'ratnadeep-chambers',
|
| 294 |
+
title: 'Ratnadeep Chambers',
|
| 295 |
+
location: '512, Ganesh Peth, Pune',
|
| 296 |
+
categories: ['Commercial'],
|
| 297 |
+
section: 'Construction',
|
| 298 |
+
status: 'Completed',
|
| 299 |
+
commencement: '2001',
|
| 300 |
+
completionDate: '2001',
|
| 301 |
+
projectSize: '9,000 sq.ft.',
|
| 302 |
+
description:
|
| 303 |
+
'Commercial chambers for Parmar Bhandari Associates delivered within four months (Jan 2001 - Apr 2001).',
|
| 304 |
+
noDetail: true
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
slug: 'siddhivinayak-vihar',
|
| 308 |
+
title: 'Siddhivinayak Vihar',
|
| 309 |
+
location: 'Handewadi Road, Hadapsar, Pune',
|
| 310 |
+
categories: ['Residential'],
|
| 311 |
+
section: 'Construction',
|
| 312 |
+
status: 'Completed',
|
| 313 |
+
commencement: '1999',
|
| 314 |
+
completionDate: '2001',
|
| 315 |
+
projectSize: '110,000 sq.ft.',
|
| 316 |
+
description:
|
| 317 |
+
'Large-scale residential enclave for Siddhivinayak Construction (Jun 1999 - Oct 2001).',
|
| 318 |
+
noDetail: true
|
| 319 |
+
},
|
| 320 |
+
{
|
| 321 |
+
slug: 'regent-park',
|
| 322 |
+
title: 'Regent Park',
|
| 323 |
+
location: '30, Gidney Park, Pune',
|
| 324 |
+
categories: ['Residential'],
|
| 325 |
+
section: 'Construction',
|
| 326 |
+
status: 'Completed',
|
| 327 |
+
commencement: '2000',
|
| 328 |
+
completionDate: '2002',
|
| 329 |
+
projectSize: '32,000 sq.ft.',
|
| 330 |
+
description:
|
| 331 |
+
'Residential towers for Bhawani Construction Co. in Gidney Park (Aug 2000 - Nov 2002).',
|
| 332 |
+
noDetail: true
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
slug: 'vidya-sahkari-bank',
|
| 336 |
+
title: 'Vidya Sahkari Bank',
|
| 337 |
+
location: 'Near Ganraj Hotel, Bajirao Road, Pune',
|
| 338 |
+
categories: ['Commercial'],
|
| 339 |
+
section: 'Construction',
|
| 340 |
+
status: 'Completed',
|
| 341 |
+
commencement: '1999',
|
| 342 |
+
completionDate: '2001',
|
| 343 |
+
projectSize: '15,000 sq.ft.',
|
| 344 |
+
description:
|
| 345 |
+
'Bank headquarters completed for Vidya Sahkari Bank Limited (Nov 1999 - Nov 2001).',
|
| 346 |
+
noDetail: true
|
| 347 |
+
},
|
| 348 |
+
{
|
| 349 |
+
slug: 'pratik-apartment',
|
| 350 |
+
title: 'Pratik Apartment',
|
| 351 |
+
location: '314, Rasta Peth, Pune',
|
| 352 |
+
categories: ['Residential'],
|
| 353 |
+
section: 'Construction',
|
| 354 |
+
status: 'Completed',
|
| 355 |
+
commencement: '2000',
|
| 356 |
+
completionDate: '2002',
|
| 357 |
+
projectSize: '11,000 sq.ft.',
|
| 358 |
+
description:
|
| 359 |
+
'Residential block executed for Oswal Construction at Rasta Peth (Oct 2000 - May 2002).',
|
| 360 |
+
noDetail: true
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
slug: 'kumar-manor',
|
| 364 |
+
title: 'Kumar Manor',
|
| 365 |
+
location: 'Rasta Peth, Pune',
|
| 366 |
+
categories: ['Residential'],
|
| 367 |
+
section: 'Construction',
|
| 368 |
+
status: 'Completed',
|
| 369 |
+
commencement: '2002',
|
| 370 |
+
completionDate: '2004',
|
| 371 |
+
projectSize: '48,000 sq.ft.',
|
| 372 |
+
description:
|
| 373 |
+
'Premium apartments delivered for Ashwamegh Constructions and Kumar Builders (2002 - 2004).',
|
| 374 |
+
noDetail: true
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
slug: 'khopkar-heights',
|
| 378 |
+
title: 'Khopkar Heights',
|
| 379 |
+
location: 'Quarter Gate, Pune',
|
| 380 |
+
categories: ['Residential'],
|
| 381 |
+
section: 'Construction',
|
| 382 |
+
status: 'Completed',
|
| 383 |
+
commencement: '2000',
|
| 384 |
+
completionDate: '2003',
|
| 385 |
+
projectSize: '32,000 sq.ft.',
|
| 386 |
+
description:
|
| 387 |
+
'Multi-storey residences for Nagpal Constructions in Quarter Gate (2000 - 2003).',
|
| 388 |
+
noDetail: true
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
slug: 'tanmay-apartments',
|
| 392 |
+
title: 'Tanmay Apartments',
|
| 393 |
+
location: 'Fatima Nagar, Pune',
|
| 394 |
+
categories: ['Residential'],
|
| 395 |
+
section: 'Construction',
|
| 396 |
+
status: 'Completed',
|
| 397 |
+
commencement: '1999',
|
| 398 |
+
completionDate: '2001',
|
| 399 |
+
projectSize: '18,000 sq.ft.',
|
| 400 |
+
description: 'Residential project contracted for S. S. Builders in Fatima Nagar.',
|
| 401 |
+
noDetail: true
|
| 402 |
+
},
|
| 403 |
+
{
|
| 404 |
+
slug: 'archies-court',
|
| 405 |
+
title: "Archie's Court",
|
| 406 |
+
location: 'Shankar Sheth Road, Pune',
|
| 407 |
+
categories: ['Commercial'],
|
| 408 |
+
section: 'Construction',
|
| 409 |
+
status: 'Completed',
|
| 410 |
+
commencement: '1999',
|
| 411 |
+
completionDate: '2001',
|
| 412 |
+
projectSize: '16,000 sq.ft.',
|
| 413 |
+
description: 'Commercial development delivered for Achal Constructions on Shankar Sheth Road.',
|
| 414 |
+
noDetail: true
|
| 415 |
+
},
|
| 416 |
+
{
|
| 417 |
+
slug: 'radiant-paradise',
|
| 418 |
+
title: 'Radiant Paradise',
|
| 419 |
+
location: 'Salunke Vihar Road, Kondhwa, Pune',
|
| 420 |
+
categories: ['Residential'],
|
| 421 |
+
section: 'Construction',
|
| 422 |
+
status: 'Completed',
|
| 423 |
+
commencement: '2001',
|
| 424 |
+
completionDate: '2004',
|
| 425 |
+
projectSize: '189,000 sq.ft.',
|
| 426 |
+
description:
|
| 427 |
+
'Large township style development executed for Radiant Builders at Salunke Vihar Road.',
|
| 428 |
+
noDetail: true
|
| 429 |
+
},
|
| 430 |
+
{
|
| 431 |
+
slug: 'kwality-garden',
|
| 432 |
+
title: 'Kwality Garden',
|
| 433 |
+
location: 'NIBM Road, Kondhwa, Pune',
|
| 434 |
+
categories: ['Residential'],
|
| 435 |
+
section: 'Construction',
|
| 436 |
+
status: 'Completed',
|
| 437 |
+
commencement: '2000',
|
| 438 |
+
completionDate: '2003',
|
| 439 |
+
projectSize: '72,000 sq.ft.',
|
| 440 |
+
description: 'Garden themed residential enclave for Kwality Builders at NIBM Road.',
|
| 441 |
+
noDetail: true
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
slug: 'kwality-empress',
|
| 445 |
+
title: 'Kwality Empress',
|
| 446 |
+
location: 'St. Patricks Town, Uday Baug, Pune',
|
| 447 |
+
categories: ['Residential'],
|
| 448 |
+
section: 'Construction',
|
| 449 |
+
status: 'Completed',
|
| 450 |
+
commencement: '1999',
|
| 451 |
+
completionDate: '2001',
|
| 452 |
+
projectSize: '23,000 sq.ft.',
|
| 453 |
+
description: 'Premium residences for Kwality Builders at St. Patricks Town, Uday Baug.',
|
| 454 |
+
noDetail: true
|
| 455 |
+
},
|
| 456 |
+
{
|
| 457 |
+
slug: 'kumar-sadan',
|
| 458 |
+
title: 'Kumar Sadan',
|
| 459 |
+
location: '444, Somwar Peth, Pune',
|
| 460 |
+
categories: ['Residential'],
|
| 461 |
+
section: 'Construction',
|
| 462 |
+
status: 'Completed',
|
| 463 |
+
commencement: '1998',
|
| 464 |
+
completionDate: '2000',
|
| 465 |
+
projectSize: '35,000 sq.ft.',
|
| 466 |
+
description: 'Urban residential project delivered for Kumar Builders at Somwar Peth.',
|
| 467 |
+
noDetail: true
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
slug: 'nivedita-gardens',
|
| 471 |
+
title: 'Nivedita Gardens',
|
| 472 |
+
location: 'NIBM Road, Kondhwa, Pune',
|
| 473 |
+
categories: ['Residential'],
|
| 474 |
+
section: 'Construction',
|
| 475 |
+
status: 'Completed',
|
| 476 |
+
commencement: '1999',
|
| 477 |
+
completionDate: '2001',
|
| 478 |
+
projectSize: '19,000 sq.ft.',
|
| 479 |
+
description: 'Garden residences executed for Nivedita Builders at NIBM Road.',
|
| 480 |
+
noDetail: true
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
slug: 'kumar-kunj',
|
| 484 |
+
title: 'Kumar Kunj',
|
| 485 |
+
location: 'Fatima Nagar, Pune',
|
| 486 |
+
categories: ['Residential'],
|
| 487 |
+
section: 'Construction',
|
| 488 |
+
status: 'Completed',
|
| 489 |
+
commencement: '2000',
|
| 490 |
+
completionDate: '2003',
|
| 491 |
+
projectSize: '130,000 sq.ft.',
|
| 492 |
+
description: 'High-density housing for Kumar and Gaikwad at Fatima Nagar.',
|
| 493 |
+
noDetail: true
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
slug: 'leela-heights',
|
| 497 |
+
title: 'Leela Heights',
|
| 498 |
+
location: 'Bhawani Peth, Pune',
|
| 499 |
+
categories: ['Residential'],
|
| 500 |
+
section: 'Construction',
|
| 501 |
+
status: 'Completed',
|
| 502 |
+
commencement: '1999',
|
| 503 |
+
completionDate: '2001',
|
| 504 |
+
projectSize: '28,000 sq.ft.',
|
| 505 |
+
description: 'Residential tower opposite Sathe Biscuits in Bhawani Peth.',
|
| 506 |
+
noDetail: true
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
slug: 'nagpals-camp',
|
| 510 |
+
title: "Nagpal's",
|
| 511 |
+
location: 'Near Fashion Street, Camp, Pune',
|
| 512 |
+
categories: ['Residential'],
|
| 513 |
+
section: 'Construction',
|
| 514 |
+
status: 'Completed',
|
| 515 |
+
commencement: '1999',
|
| 516 |
+
completionDate: '2001',
|
| 517 |
+
projectSize: '22,000 sq.ft.',
|
| 518 |
+
description: 'Contracting works for Nagpal Construction Pvt Ltd near Fashion Street.',
|
| 519 |
+
noDetail: true
|
| 520 |
+
},
|
| 521 |
+
{
|
| 522 |
+
slug: 'radhika-empress',
|
| 523 |
+
title: 'Radhika Empress',
|
| 524 |
+
location: 'Wanawadi, Pune',
|
| 525 |
+
categories: ['Residential'],
|
| 526 |
+
section: 'Construction',
|
| 527 |
+
status: 'Completed',
|
| 528 |
+
commencement: '2000',
|
| 529 |
+
completionDate: '2002',
|
| 530 |
+
projectSize: '42,000 sq.ft.',
|
| 531 |
+
description: 'Lifestyle residences executed for Ranka Shah Associates in Wanawadi.',
|
| 532 |
+
noDetail: true
|
| 533 |
+
},
|
| 534 |
+
{
|
| 535 |
+
slug: 'radhika-empire',
|
| 536 |
+
title: 'Radhika Empire',
|
| 537 |
+
location: 'Wanawadi, Pune',
|
| 538 |
+
categories: ['Residential'],
|
| 539 |
+
section: 'Construction',
|
| 540 |
+
status: 'Completed',
|
| 541 |
+
commencement: '2000',
|
| 542 |
+
completionDate: '2002',
|
| 543 |
+
projectSize: '28,000 sq.ft.',
|
| 544 |
+
description: 'Complementary residential block for Ranka Shah Associates in Wanawadi.',
|
| 545 |
+
noDetail: true
|
| 546 |
+
},
|
| 547 |
+
{
|
| 548 |
+
slug: 'siddhivinayak-prastha',
|
| 549 |
+
title: 'Siddhivinayak Prastha',
|
| 550 |
+
location: 'Akurdi, Pune',
|
| 551 |
+
categories: ['Residential'],
|
| 552 |
+
section: 'Construction',
|
| 553 |
+
status: 'Completed',
|
| 554 |
+
commencement: '2001',
|
| 555 |
+
completionDate: '2004',
|
| 556 |
+
projectSize: '100,000 sq.ft.',
|
| 557 |
+
description: 'Township scale project executed for Riddhi Developers at Akurdi.',
|
| 558 |
+
noDetail: true
|
| 559 |
+
},
|
| 560 |
+
{
|
| 561 |
+
slug: 'gulmohar-habitat',
|
| 562 |
+
title: 'Gulmohar Habitat',
|
| 563 |
+
location: 'Fatima Nagar, Pune',
|
| 564 |
+
categories: ['Residential'],
|
| 565 |
+
section: 'Construction',
|
| 566 |
+
status: 'Completed',
|
| 567 |
+
commencement: '2000',
|
| 568 |
+
completionDate: '2003',
|
| 569 |
+
projectSize: '50,000 sq.ft.',
|
| 570 |
+
description: 'Habitation-focused development for Gulmohar Associates at Fatima Nagar.',
|
| 571 |
+
noDetail: true
|
| 572 |
+
},
|
| 573 |
+
{
|
| 574 |
+
slug: 'nakoda-bhairav-dharamshala',
|
| 575 |
+
title: 'Nakoda Bhairav Dharamshala',
|
| 576 |
+
location: 'Alandi, Pune',
|
| 577 |
+
categories: ['Mixed Use'],
|
| 578 |
+
section: 'Construction',
|
| 579 |
+
status: 'Completed',
|
| 580 |
+
commencement: '1999',
|
| 581 |
+
completionDate: '2001',
|
| 582 |
+
projectSize: '28,000 sq.ft.',
|
| 583 |
+
description: 'Community dharamshala constructed for Nakoda Trust in Alandi.',
|
| 584 |
+
noDetail: true
|
| 585 |
+
},
|
| 586 |
+
{
|
| 587 |
+
slug: 'vardhaman-heights',
|
| 588 |
+
title: 'Vardhaman Heights',
|
| 589 |
+
location: 'Bajirao Road, Pune',
|
| 590 |
+
categories: ['Residential'],
|
| 591 |
+
section: 'Construction',
|
| 592 |
+
status: 'Completed',
|
| 593 |
+
commencement: '2001',
|
| 594 |
+
completionDate: '2003',
|
| 595 |
+
projectSize: '42,000 sq.ft.',
|
| 596 |
+
description: 'High-rise residences delivered for Vardhaman Associates at Bajirao Road.',
|
| 597 |
+
noDetail: true
|
| 598 |
+
},
|
| 599 |
+
{
|
| 600 |
+
slug: 'silver-plaza',
|
| 601 |
+
title: 'Silver Plaza',
|
| 602 |
+
location: 'Fatima Nagar, Pune',
|
| 603 |
+
categories: ['Commercial'],
|
| 604 |
+
section: 'Construction',
|
| 605 |
+
status: 'Completed',
|
| 606 |
+
commencement: '1999',
|
| 607 |
+
completionDate: '2001',
|
| 608 |
+
projectSize: '12,000 sq.ft.',
|
| 609 |
+
description: 'Neighbourhood commercial plaza for Mahavir Promoters & Builders.',
|
| 610 |
+
noDetail: true
|
| 611 |
+
},
|
| 612 |
+
{
|
| 613 |
+
slug: 'silver-apartment',
|
| 614 |
+
title: 'Silver Apartment',
|
| 615 |
+
location: 'Hadapsar, Pune',
|
| 616 |
+
categories: ['Residential'],
|
| 617 |
+
section: 'Construction',
|
| 618 |
+
status: 'Completed',
|
| 619 |
+
commencement: '1999',
|
| 620 |
+
completionDate: '2001',
|
| 621 |
+
projectSize: '24,000 sq.ft.',
|
| 622 |
+
description: 'Residential tower for Mahavir Promoters & Builders at Hadapsar.',
|
| 623 |
+
noDetail: true
|
| 624 |
+
},
|
| 625 |
+
{
|
| 626 |
+
slug: 'silver-terrace',
|
| 627 |
+
title: 'Silver Terrace',
|
| 628 |
+
location: 'Fatima Nagar, Pune',
|
| 629 |
+
categories: ['Residential'],
|
| 630 |
+
section: 'Construction',
|
| 631 |
+
status: 'Completed',
|
| 632 |
+
commencement: '1999',
|
| 633 |
+
completionDate: '2001',
|
| 634 |
+
projectSize: '14,000 sq.ft.',
|
| 635 |
+
description: 'Boutique residences for Mahavir Promoters & Builders at Fatima Nagar.',
|
| 636 |
+
noDetail: true
|
| 637 |
+
},
|
| 638 |
+
{
|
| 639 |
+
slug: 'silver-chambers',
|
| 640 |
+
title: 'Silver Chambers',
|
| 641 |
+
location: 'Hadapsar, Pune',
|
| 642 |
+
categories: ['Commercial'],
|
| 643 |
+
section: 'Construction',
|
| 644 |
+
status: 'Completed',
|
| 645 |
+
commencement: '1999',
|
| 646 |
+
completionDate: '2001',
|
| 647 |
+
projectSize: '12,000 sq.ft.',
|
| 648 |
+
description: 'Commercial complex delivered for Mahavir Promoters & Builders at Hadapsar.',
|
| 649 |
+
noDetail: true
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
slug: 'silver-heights',
|
| 653 |
+
title: 'Silver Heights',
|
| 654 |
+
location: 'Fatima Nagar, Pune',
|
| 655 |
+
categories: ['Residential'],
|
| 656 |
+
section: 'Construction',
|
| 657 |
+
status: 'Completed',
|
| 658 |
+
commencement: '1999',
|
| 659 |
+
completionDate: '2001',
|
| 660 |
+
projectSize: '16,000 sq.ft.',
|
| 661 |
+
description: 'Mid-rise residential block for Mahavir Promoters & Builders at Fatima Nagar.',
|
| 662 |
+
noDetail: true
|
| 663 |
+
},
|
| 664 |
+
{
|
| 665 |
+
slug: 'legend-park',
|
| 666 |
+
title: 'Legend Park',
|
| 667 |
+
location: 'Kondhwa, Pune',
|
| 668 |
+
categories: ['Residential'],
|
| 669 |
+
section: 'Construction',
|
| 670 |
+
status: 'Completed',
|
| 671 |
+
commencement: '2000',
|
| 672 |
+
completionDate: '2003',
|
| 673 |
+
projectSize: '28,000 sq.ft.',
|
| 674 |
+
description: 'Signature residences executed for Legend Builders at Kondhwa.',
|
| 675 |
+
noDetail: true
|
| 676 |
+
},
|
| 677 |
+
{
|
| 678 |
+
slug: 'neeta-corner',
|
| 679 |
+
title: 'Neeta Corner',
|
| 680 |
+
location: 'Bhawani Peth, Pune',
|
| 681 |
+
categories: ['Commercial'],
|
| 682 |
+
section: 'Construction',
|
| 683 |
+
status: 'Completed',
|
| 684 |
+
commencement: '1999',
|
| 685 |
+
completionDate: '2001',
|
| 686 |
+
projectSize: '14,000 sq.ft.',
|
| 687 |
+
description: 'Corner commercial building executed for Kumar & Co. at Bhawani Peth.',
|
| 688 |
+
noDetail: true
|
| 689 |
+
},
|
| 690 |
+
{
|
| 691 |
+
slug: 'saraswati-apartment',
|
| 692 |
+
title: 'Saraswati Apartment',
|
| 693 |
+
location: 'Wanawadi, Pune',
|
| 694 |
+
categories: ['Residential'],
|
| 695 |
+
section: 'Construction',
|
| 696 |
+
status: 'Completed',
|
| 697 |
+
commencement: '1999',
|
| 698 |
+
completionDate: '2001',
|
| 699 |
+
projectSize: '22,000 sq.ft.',
|
| 700 |
+
description: 'Residential project delivered for Kumar & Co. in Wanawadi.',
|
| 701 |
+
noDetail: true
|
| 702 |
+
},
|
| 703 |
+
{
|
| 704 |
+
slug: 'kg-mansion',
|
| 705 |
+
title: 'K. G. Mansion',
|
| 706 |
+
location: 'Apte Road, Pune',
|
| 707 |
+
categories: ['Residential'],
|
| 708 |
+
section: 'Construction',
|
| 709 |
+
status: 'Completed',
|
| 710 |
+
commencement: '1999',
|
| 711 |
+
completionDate: '2001',
|
| 712 |
+
projectSize: '14,000 sq.ft.',
|
| 713 |
+
description: 'Premium residences executed for Kumar Gaikwad Associates at Apte Road.',
|
| 714 |
+
noDetail: true
|
| 715 |
+
},
|
| 716 |
+
{
|
| 717 |
+
slug: 'hotel-coronet',
|
| 718 |
+
title: 'Hotel Coronet',
|
| 719 |
+
location: 'Apte Road, Pune',
|
| 720 |
+
categories: ['Commercial'],
|
| 721 |
+
section: 'Construction',
|
| 722 |
+
status: 'Completed',
|
| 723 |
+
commencement: '2002',
|
| 724 |
+
completionDate: '2004',
|
| 725 |
+
projectSize: '24,000 sq.ft.',
|
| 726 |
+
description: 'Hospitality project constructed for Mr. V. Agarwal on Apte Road.',
|
| 727 |
+
noDetail: true
|
| 728 |
+
},
|
| 729 |
+
{
|
| 730 |
+
slug: 'hotel-orbett',
|
| 731 |
+
title: 'Hotel Orbett',
|
| 732 |
+
location: 'Apte Road, Pune',
|
| 733 |
+
categories: ['Commercial'],
|
| 734 |
+
section: 'Construction',
|
| 735 |
+
status: 'Completed',
|
| 736 |
+
commencement: '2002',
|
| 737 |
+
completionDate: '2004',
|
| 738 |
+
projectSize: '26,000 sq.ft.',
|
| 739 |
+
description: 'Upscale hotel delivered for Mr. B. Agarwal at Apte Road.',
|
| 740 |
+
noDetail: true
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
slug: 'silver-homes',
|
| 744 |
+
title: 'Silver Homes',
|
| 745 |
+
location: 'Shivaji Nagar, Pune',
|
| 746 |
+
categories: ['Residential'],
|
| 747 |
+
section: 'Construction',
|
| 748 |
+
status: 'Completed',
|
| 749 |
+
commencement: '1999',
|
| 750 |
+
completionDate: '2001',
|
| 751 |
+
projectSize: '15,000 sq.ft.',
|
| 752 |
+
description: 'Residential enclave for Mahavir Promoters & Builders at Shivaji Nagar.',
|
| 753 |
+
noDetail: true
|
| 754 |
+
},
|
| 755 |
+
{
|
| 756 |
+
slug: 'wakad-sra',
|
| 757 |
+
title: 'Wakad SRA',
|
| 758 |
+
location: 'Wakad–Tathawade, Pune',
|
| 759 |
+
categories: ['Mixed Use'],
|
| 760 |
+
section: 'SRA',
|
| 761 |
+
status: 'Upcoming',
|
| 762 |
+
plannedCommencement: 'REPLACE_ME',
|
| 763 |
+
image: '/assets/projects/wakad-sra/card',
|
| 764 |
+
projectSize: '1,500,000 sq.ft.',
|
| 765 |
+
description: 'Mixed-use SRA project by Jai Enterprises (Upcoming).'
|
| 766 |
+
},
|
| 767 |
+
{
|
| 768 |
+
slug: 'vrindavan-condominium',
|
| 769 |
+
title: 'Vrindavan Condominium',
|
| 770 |
+
location: 'Shankar Sheth Road, Pune',
|
| 771 |
+
categories: ['Residential', 'Retail'],
|
| 772 |
+
section: 'Redevelopment',
|
| 773 |
+
status: 'Upcoming',
|
| 774 |
+
plannedCommencement: 'June 2026',
|
| 775 |
+
image: '/assets/projects/vrindavan-condominium/card',
|
| 776 |
+
projectSize: '259,000 sq.ft.',
|
| 777 |
+
description: 'Redevelopment with residential and retail components (Planned June 2026).'
|
| 778 |
+
},
|
| 779 |
+
{
|
| 780 |
+
slug: 'kashewadi-sra',
|
| 781 |
+
title: 'Kashewadi SRA',
|
| 782 |
+
location: 'Pune',
|
| 783 |
+
categories: ['Residential'],
|
| 784 |
+
section: 'SRA',
|
| 785 |
+
status: 'Completed',
|
| 786 |
+
completionDate: '2008',
|
| 787 |
+
image: '/assets/projects/kashewadi-sra/card',
|
| 788 |
+
projectSize: '190,000 sq.ft.',
|
| 789 |
+
description: 'SRA residential project by Saubhagyalakshmi Developers (Completed 2008).'
|
| 790 |
+
},
|
| 791 |
+
{
|
| 792 |
+
slug: 'eisha-gems',
|
| 793 |
+
title: 'Eisha Gems',
|
| 794 |
+
location: 'Pune',
|
| 795 |
+
categories: ['Residential'],
|
| 796 |
+
section: 'Development',
|
| 797 |
+
status: 'Completed',
|
| 798 |
+
completionDate: '2014',
|
| 799 |
+
image: '/assets/projects/eisha-gems/card',
|
| 800 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 801 |
+
projectSize: '25,000 sq.ft.',
|
| 802 |
+
description: 'Residential development by Jain Ashapuri Developers – Unit 2 (Completed 2014).'
|
| 803 |
+
},
|
| 804 |
+
{
|
| 805 |
+
slug: 'isha-vastu',
|
| 806 |
+
title: 'Isha Vastu',
|
| 807 |
+
location: 'Pune',
|
| 808 |
+
categories: ['Residential'],
|
| 809 |
+
section: 'Development',
|
| 810 |
+
status: 'Completed',
|
| 811 |
+
completionDate: '2007',
|
| 812 |
+
image: '/assets/projects/isha-vastu/card',
|
| 813 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 814 |
+
projectSize: '16,000 sq.ft.',
|
| 815 |
+
description: 'Residential development by Vastu Estate (Completed 2007).'
|
| 816 |
+
},
|
| 817 |
+
{
|
| 818 |
+
slug: 'modak-prasad',
|
| 819 |
+
title: 'Modak Prasad',
|
| 820 |
+
location: 'Pune',
|
| 821 |
+
categories: ['Residential'],
|
| 822 |
+
section: 'Development',
|
| 823 |
+
status: 'Completed',
|
| 824 |
+
completionDate: '2005',
|
| 825 |
+
image: '/assets/projects/modak-prasad/card',
|
| 826 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 827 |
+
projectSize: '18,000 sq.ft.',
|
| 828 |
+
description: 'Residential development by Gadre & Associates (Completed 2005).'
|
| 829 |
+
},
|
| 830 |
+
{
|
| 831 |
+
slug: 'namo-vihar',
|
| 832 |
+
title: 'Namo Vihar',
|
| 833 |
+
location: 'Pune',
|
| 834 |
+
categories: ['Residential'],
|
| 835 |
+
section: 'Development',
|
| 836 |
+
status: 'Completed',
|
| 837 |
+
completionDate: '2003',
|
| 838 |
+
image: '/assets/projects/namo-vihar/card',
|
| 839 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 840 |
+
projectSize: '145,000 sq.ft.',
|
| 841 |
+
description: 'Residential development by Namo Promoters & Builders (Completed 2003).'
|
| 842 |
+
},
|
| 843 |
+
{
|
| 844 |
+
slug: 'namo-residency',
|
| 845 |
+
title: 'Namo Residency',
|
| 846 |
+
location: 'Pune',
|
| 847 |
+
categories: ['Residential'],
|
| 848 |
+
section: 'Development',
|
| 849 |
+
status: 'Completed',
|
| 850 |
+
completionDate: '2006',
|
| 851 |
+
image: '/assets/projects/namo-residency/card',
|
| 852 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 853 |
+
projectSize: '85,000 sq.ft.',
|
| 854 |
+
description: 'Residential development by Namo Developers & Builders (Completed 2006).'
|
| 855 |
+
},
|
| 856 |
+
{
|
| 857 |
+
slug: 'namo-prastha',
|
| 858 |
+
title: 'Namo Prastha',
|
| 859 |
+
location: 'Pune',
|
| 860 |
+
categories: ['Residential'],
|
| 861 |
+
section: 'Development',
|
| 862 |
+
status: 'Completed',
|
| 863 |
+
completionDate: '2004',
|
| 864 |
+
image: '/assets/projects/namo-prastha/card',
|
| 865 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 866 |
+
projectSize: '15,000 sq.ft.',
|
| 867 |
+
description: 'Residential development by Namo Promoters & Builders (Completed 2004).'
|
| 868 |
+
},
|
| 869 |
+
{
|
| 870 |
+
slug: 'isha-chambers',
|
| 871 |
+
title: 'Isha Chambers',
|
| 872 |
+
location: 'Pune',
|
| 873 |
+
categories: ['Residential'],
|
| 874 |
+
section: 'Development',
|
| 875 |
+
status: 'Completed',
|
| 876 |
+
completionDate: '2006',
|
| 877 |
+
image: '/assets/projects/isha-chambers/card',
|
| 878 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 879 |
+
projectSize: '21,000 sq.ft.',
|
| 880 |
+
description: 'Residential development by Jain Ashapuri Promoters & Builders (Completed 2006).'
|
| 881 |
+
},
|
| 882 |
+
{
|
| 883 |
+
slug: 'isha-heights',
|
| 884 |
+
title: 'Isha Heights',
|
| 885 |
+
location: 'Pune',
|
| 886 |
+
categories: ['Residential'],
|
| 887 |
+
section: 'Development',
|
| 888 |
+
status: 'Completed',
|
| 889 |
+
completionDate: '2005',
|
| 890 |
+
image: '/assets/projects/isha-heights/card',
|
| 891 |
+
projectSize: 'REPLACE_ME sq.ft. <!-- TODO: replace -->',
|
| 892 |
+
projectSize: '36,000 sq.ft.',
|
| 893 |
+
description: 'Residential development by Jain Ashapuri Promoters & Builders (Completed 2005).'
|
| 894 |
+
},
|
| 895 |
+
{
|
| 896 |
+
slug: 'vastu-sadan',
|
| 897 |
+
title: 'Vastu Sadan',
|
| 898 |
+
location: 'Pune',
|
| 899 |
+
categories: ['Residential'],
|
| 900 |
+
section: 'Development',
|
| 901 |
+
status: 'Completed',
|
| 902 |
+
completionDate: '2004',
|
| 903 |
+
projectSize: '9,000 sq.ft.',
|
| 904 |
+
description: 'Residential development by Vastu Developers (Completed 2004).',
|
| 905 |
+
noDetail: true
|
| 906 |
+
},
|
| 907 |
+
{
|
| 908 |
+
slug: 'isha-tower',
|
| 909 |
+
title: 'Isha Tower',
|
| 910 |
+
location: 'Pune',
|
| 911 |
+
categories: ['Residential'],
|
| 912 |
+
section: 'Development',
|
| 913 |
+
status: 'Completed',
|
| 914 |
+
completionDate: '2004',
|
| 915 |
+
projectSize: '21,000 sq.ft.',
|
| 916 |
+
description: 'Residential development by Jain Promoters & Builders (Completed 2004).',
|
| 917 |
+
noDetail: true
|
| 918 |
+
},
|
| 919 |
+
{
|
| 920 |
+
slug: 'vastu-puram',
|
| 921 |
+
title: 'Vastu Puram',
|
| 922 |
+
location: 'Pune',
|
| 923 |
+
categories: ['Residential'],
|
| 924 |
+
section: 'Development',
|
| 925 |
+
status: 'Completed',
|
| 926 |
+
completionDate: '2003',
|
| 927 |
+
projectSize: '16,000 sq.ft.',
|
| 928 |
+
description: 'Residential development by Vastu Promoters & Developers (Completed 2003).',
|
| 929 |
+
noDetail: true
|
| 930 |
+
},
|
| 931 |
+
{
|
| 932 |
+
slug: 'jayesh-apartment',
|
| 933 |
+
title: 'Jayesh Apartment',
|
| 934 |
+
location: 'Pune',
|
| 935 |
+
categories: ['Residential'],
|
| 936 |
+
section: 'Development',
|
| 937 |
+
status: 'Completed',
|
| 938 |
+
completionDate: '1995',
|
| 939 |
+
projectSize: '17,000 sq.ft.',
|
| 940 |
+
description: 'Residential development by R. B. Constructions (Completed 1995).',
|
| 941 |
+
noDetail: true
|
| 942 |
+
}
|
| 943 |
+
];
|
| 944 |
+
|
| 945 |
+
const CATEGORIES = ['All', 'Residential', 'Commercial', 'Retail', 'Mixed Use'];
|
| 946 |
+
const STATUSES = ['All', 'Completed', 'Ongoing', 'Upcoming'];
|
| 947 |
+
const SECTIONS = ['Redevelopment', 'Construction', 'Development'];
|
| 948 |
+
|
| 949 |
+
export default function Projects() {
|
| 950 |
+
const [query, setQuery] = useState('');
|
| 951 |
+
const [filter, setFilter] = useState('All');
|
| 952 |
+
const [statusFilter, setStatusFilter] = useState('All');
|
| 953 |
+
const location = useLocation();
|
| 954 |
+
const sectionParam = new URLSearchParams(location.search).get('section');
|
| 955 |
+
|
| 956 |
+
const data = useMemo(() => {
|
| 957 |
+
return ALL_PROJECTS.filter((p) => {
|
| 958 |
+
const matchesCategory =
|
| 959 |
+
filter === 'All' || (Array.isArray(p.categories) && p.categories.includes(filter));
|
| 960 |
+
const matchesStatus = statusFilter === 'All' || p.status === statusFilter;
|
| 961 |
+
const q = query.trim().toLowerCase();
|
| 962 |
+
const matchesQuery =
|
| 963 |
+
!q ||
|
| 964 |
+
p.title.toLowerCase().includes(q) ||
|
| 965 |
+
p.location.toLowerCase().includes(q) ||
|
| 966 |
+
p.description.toLowerCase().includes(q);
|
| 967 |
+
return matchesCategory && matchesStatus && matchesQuery;
|
| 968 |
+
});
|
| 969 |
+
}, [filter, statusFilter, query]);
|
| 970 |
+
|
| 971 |
+
// Scroll to requested section if provided
|
| 972 |
+
useEffect(() => {
|
| 973 |
+
if (sectionParam) {
|
| 974 |
+
const id = `sec-${sectionParam}`;
|
| 975 |
+
const el = document.getElementById(id);
|
| 976 |
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 977 |
+
}
|
| 978 |
+
}, [sectionParam]);
|
| 979 |
+
|
| 980 |
+
return (
|
| 981 |
+
<section className="section" aria-labelledby="projects-heading">
|
| 982 |
+
<div className="container">
|
| 983 |
+
<header className="mb-8 text-center">
|
| 984 |
+
<h1 id="projects-heading" className="h2">Projects</h1>
|
| 985 |
+
<p className="lead mt-3">
|
| 986 |
+
Explore our residential and commercial portfolio.
|
| 987 |
+
{/* TODO: replace with official project list and photos from jadeinfra.in */}
|
| 988 |
+
</p>
|
| 989 |
+
</header>
|
| 990 |
+
|
| 991 |
+
<div className="mb-6 grid grid-cols-1 gap-3 md:grid-cols-4">
|
| 992 |
+
<div className="md:col-span-2">
|
| 993 |
+
<label htmlFor="search" className="sr-only">Search</label>
|
| 994 |
+
<input
|
| 995 |
+
id="search"
|
| 996 |
+
placeholder="Search projects"
|
| 997 |
+
value={query}
|
| 998 |
+
onChange={(e) => setQuery(e.target.value)}
|
| 999 |
+
className="w-full rounded-md border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-600"
|
| 1000 |
+
/>
|
| 1001 |
+
</div>
|
| 1002 |
+
<div className="md:col-span-1">
|
| 1003 |
+
<label htmlFor="category-filter" className="block text-sm font-medium text-slate-700">Category</label>
|
| 1004 |
+
<select
|
| 1005 |
+
id="category-filter"
|
| 1006 |
+
value={filter}
|
| 1007 |
+
onChange={(e) => setFilter(e.target.value)}
|
| 1008 |
+
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-600"
|
| 1009 |
+
>
|
| 1010 |
+
{CATEGORIES.map((c) => (
|
| 1011 |
+
<option key={c} value={c}>{c}</option>
|
| 1012 |
+
))}
|
| 1013 |
+
</select>
|
| 1014 |
+
</div>
|
| 1015 |
+
<div className="md:col-span-1">
|
| 1016 |
+
<label htmlFor="status-filter" className="block text-sm font-medium text-slate-700">Status</label>
|
| 1017 |
+
<select
|
| 1018 |
+
id="status-filter"
|
| 1019 |
+
value={statusFilter}
|
| 1020 |
+
onChange={(e) => setStatusFilter(e.target.value)}
|
| 1021 |
+
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-600"
|
| 1022 |
+
>
|
| 1023 |
+
{STATUSES.map((s) => (
|
| 1024 |
+
<option key={s} value={s}>{s}</option>
|
| 1025 |
+
))}
|
| 1026 |
+
</select>
|
| 1027 |
+
</div>
|
| 1028 |
+
</div>
|
| 1029 |
+
|
| 1030 |
+
|
| 1031 |
+
|
| 1032 |
+
{SECTIONS.map((sec) => {
|
| 1033 |
+
const items = data.filter((p) => p.section === sec);
|
| 1034 |
+
if (items.length === 0) return null;
|
| 1035 |
+
return (
|
| 1036 |
+
<section key={sec} className="mt-10 first:mt-0" aria-labelledby={`sec-${sec}`}>
|
| 1037 |
+
<div className="mb-6 flex items-center justify-between">
|
| 1038 |
+
<h2 id={`sec-${sec}`} className="h3">{sec}</h2>
|
| 1039 |
+
<div className="h-px flex-1 ml-6 bg-slate-200"></div>
|
| 1040 |
+
</div>
|
| 1041 |
+
<ul className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
| 1042 |
+
{items.map((p, i) => (
|
| 1043 |
+
<li key={p.slug}>
|
| 1044 |
+
<Reveal delay={i * 60}>
|
| 1045 |
+
<ProjectCard project={p} />
|
| 1046 |
+
</Reveal>
|
| 1047 |
+
</li>
|
| 1048 |
+
))}
|
| 1049 |
+
</ul>
|
| 1050 |
+
</section>
|
| 1051 |
+
);
|
| 1052 |
+
})}
|
| 1053 |
+
</div>
|
| 1054 |
+
</section>
|
| 1055 |
+
);
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
|
src/pages/ProjectsSection.jsx
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo, useState } from 'react';
|
| 2 |
+
import { useParams } from 'react-router-dom';
|
| 3 |
+
import { ALL_PROJECTS } from './Projects.jsx';
|
| 4 |
+
import ProjectCard from '../components/ProjectCard.jsx';
|
| 5 |
+
import Reveal from '../components/Reveal.jsx';
|
| 6 |
+
import SectionIntro from '../components/SectionIntro.jsx';
|
| 7 |
+
|
| 8 |
+
const CATEGORIES = ['All', 'Residential', 'Commercial', 'Retail', 'Mixed Use'];
|
| 9 |
+
const STATUSES = ['All', 'Completed', 'Ongoing', 'Upcoming'];
|
| 10 |
+
|
| 11 |
+
export default function ProjectsSection() {
|
| 12 |
+
const { sectionId } = useParams();
|
| 13 |
+
const sectionTitle =
|
| 14 |
+
sectionId?.toLowerCase() === 'sra'
|
| 15 |
+
? 'SRA'
|
| 16 |
+
: sectionId?.toLowerCase() === 'construction'
|
| 17 |
+
? 'Project Contracting'
|
| 18 |
+
: sectionId?.charAt(0).toUpperCase() + sectionId?.slice(1).toLowerCase();
|
| 19 |
+
|
| 20 |
+
const [query, setQuery] = useState('');
|
| 21 |
+
const [filter, setFilter] = useState('All');
|
| 22 |
+
const [statusFilter, setStatusFilter] = useState('All');
|
| 23 |
+
|
| 24 |
+
const data = useMemo(() => {
|
| 25 |
+
return ALL_PROJECTS.filter((p) => {
|
| 26 |
+
const section = p.section?.toLowerCase();
|
| 27 |
+
const current = sectionId?.toLowerCase();
|
| 28 |
+
const matchesSection =
|
| 29 |
+
current === 'construction'
|
| 30 |
+
? ['construction', 'development', 'redevelopment'].includes(section)
|
| 31 |
+
: section === current;
|
| 32 |
+
const matchesCategory =
|
| 33 |
+
filter === 'All' || (Array.isArray(p.categories) && p.categories.includes(filter));
|
| 34 |
+
const matchesStatus = statusFilter === 'All' || p.status === statusFilter;
|
| 35 |
+
const q = query.trim().toLowerCase();
|
| 36 |
+
const matchesQuery =
|
| 37 |
+
!q ||
|
| 38 |
+
p.title.toLowerCase().includes(q) ||
|
| 39 |
+
p.location.toLowerCase().includes(q) ||
|
| 40 |
+
p.description.toLowerCase().includes(q);
|
| 41 |
+
return matchesSection && matchesCategory && matchesStatus && matchesQuery;
|
| 42 |
+
});
|
| 43 |
+
}, [sectionId, filter, statusFilter, query]);
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<section className="section" aria-labelledby="projects-section-heading">
|
| 47 |
+
<div className="container">
|
| 48 |
+
<header className="mb-8 text-center">
|
| 49 |
+
<h1 id="projects-section-heading" className="h2">{sectionTitle}</h1>
|
| 50 |
+
<p className="lead mt-3">Explore our {sectionTitle === 'SRA' ? 'SRA' : sectionTitle?.toLowerCase()} portfolio.</p>
|
| 51 |
+
</header>
|
| 52 |
+
|
| 53 |
+
<SectionIntro sectionId={sectionId} />
|
| 54 |
+
|
| 55 |
+
<div className="mb-8 card p-6 bg-gradient-to-br from-slate-50 to-white border-slate-200">
|
| 56 |
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
| 57 |
+
{/* Search Input */}
|
| 58 |
+
<div className="md:col-span-2">
|
| 59 |
+
<label htmlFor="search" className="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
| 60 |
+
<svg className="h-4 w-4 text-brand-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 61 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
| 62 |
+
</svg>
|
| 63 |
+
Search Projects
|
| 64 |
+
</label>
|
| 65 |
+
<div className="relative">
|
| 66 |
+
<input
|
| 67 |
+
id="search"
|
| 68 |
+
placeholder="Search by name, location, or description..."
|
| 69 |
+
value={query}
|
| 70 |
+
onChange={(e) => setQuery(e.target.value)}
|
| 71 |
+
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-3 pl-10 shadow-sm transition-all focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
|
| 72 |
+
/>
|
| 73 |
+
<svg className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 74 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
| 75 |
+
</svg>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
{/* Category Filter */}
|
| 80 |
+
<div className="md:col-span-1">
|
| 81 |
+
<label htmlFor="category-filter" className="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
| 82 |
+
<svg className="h-4 w-4 text-brand-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 83 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
| 84 |
+
</svg>
|
| 85 |
+
Category
|
| 86 |
+
</label>
|
| 87 |
+
<div className="relative">
|
| 88 |
+
<select
|
| 89 |
+
id="category-filter"
|
| 90 |
+
value={filter}
|
| 91 |
+
onChange={(e) => setFilter(e.target.value)}
|
| 92 |
+
className="w-full appearance-none rounded-lg border border-slate-300 bg-white px-4 py-3 pr-10 shadow-sm transition-all focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
|
| 93 |
+
>
|
| 94 |
+
{CATEGORIES.map((c) => (
|
| 95 |
+
<option key={c} value={c}>{c}</option>
|
| 96 |
+
))}
|
| 97 |
+
</select>
|
| 98 |
+
<svg className="pointer-events-none absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 99 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
| 100 |
+
</svg>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
{/* Status Filter */}
|
| 105 |
+
<div className="md:col-span-1">
|
| 106 |
+
<label htmlFor="status-filter" className="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
| 107 |
+
<svg className="h-4 w-4 text-brand-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 108 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 109 |
+
</svg>
|
| 110 |
+
Status
|
| 111 |
+
</label>
|
| 112 |
+
<div className="relative">
|
| 113 |
+
<select
|
| 114 |
+
id="status-filter"
|
| 115 |
+
value={statusFilter}
|
| 116 |
+
onChange={(e) => setStatusFilter(e.target.value)}
|
| 117 |
+
className="w-full appearance-none rounded-lg border border-slate-300 bg-white px-4 py-3 pr-10 shadow-sm transition-all focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
|
| 118 |
+
>
|
| 119 |
+
{STATUSES.map((s) => (
|
| 120 |
+
<option key={s} value={s}>{s}</option>
|
| 121 |
+
))}
|
| 122 |
+
</select>
|
| 123 |
+
<svg className="pointer-events-none absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 124 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
| 125 |
+
</svg>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
{(() => {
|
| 134 |
+
const ORDER = ['Ongoing', 'Upcoming', 'Completed'];
|
| 135 |
+
const buckets = (statusFilter === 'All' ? ORDER : [statusFilter]).map((s) => ({
|
| 136 |
+
status: s,
|
| 137 |
+
items: data.filter((p) => p.status === s)
|
| 138 |
+
}));
|
| 139 |
+
|
| 140 |
+
const any = buckets.some((b) => b.items.length > 0);
|
| 141 |
+
if (!any) return <p className="text-slate-600">No projects found.</p>;
|
| 142 |
+
|
| 143 |
+
return buckets.map((b) => {
|
| 144 |
+
if (b.items.length === 0) return null;
|
| 145 |
+
return (
|
| 146 |
+
<section key={b.status} className="mt-10 first:mt-0" aria-labelledby={`status-${b.status}`}>
|
| 147 |
+
<div className="mb-6 flex items-center justify-between">
|
| 148 |
+
<h2 id={`status-${b.status}`} className="h3">{b.status}</h2>
|
| 149 |
+
<div className="h-px flex-1 ml-6 bg-slate-200"></div>
|
| 150 |
+
</div>
|
| 151 |
+
{(() => {
|
| 152 |
+
const itemsSorted = [...b.items].sort((a, b) => {
|
| 153 |
+
const aHas = (Array.isArray(a.gallery) && a.gallery.length > 0) || !!a.image;
|
| 154 |
+
const bHas = (Array.isArray(b.gallery) && b.gallery.length > 0) || !!b.image;
|
| 155 |
+
// true first
|
| 156 |
+
if (aHas === bHas) return 0;
|
| 157 |
+
return aHas ? -1 : 1;
|
| 158 |
+
});
|
| 159 |
+
return (
|
| 160 |
+
<ul className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
| 161 |
+
{itemsSorted.map((p, i) => (
|
| 162 |
+
<li key={p.slug}>
|
| 163 |
+
<Reveal delay={i * 60}>
|
| 164 |
+
<ProjectCard project={p} />
|
| 165 |
+
</Reveal>
|
| 166 |
+
</li>
|
| 167 |
+
))}
|
| 168 |
+
</ul>
|
| 169 |
+
);
|
| 170 |
+
})()}
|
| 171 |
+
</section>
|
| 172 |
+
);
|
| 173 |
+
});
|
| 174 |
+
})()}
|
| 175 |
+
</div>
|
| 176 |
+
</section>
|
| 177 |
+
);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
|
src/pages/ServiceDetail.jsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { useParams } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const SERVICE_COPY = {
|
| 5 |
+
foundations: {
|
| 6 |
+
title: 'Foundations & Civil Works',
|
| 7 |
+
body:
|
| 8 |
+
'We execute excavation, piling, and reinforced concrete foundations with strict QA/QC, ensuring ' +
|
| 9 |
+
'long‑term performance and safety across soil conditions. <!-- TODO: replace with official copy -->'
|
| 10 |
+
},
|
| 11 |
+
structural: {
|
| 12 |
+
title: 'Structural Construction',
|
| 13 |
+
body:
|
| 14 |
+
'RCC frames, steel structures, slabs and masonry delivered with safety-first practices and ' +
|
| 15 |
+
'conformance to codes. <!-- TODO: replace with official copy -->'
|
| 16 |
+
},
|
| 17 |
+
interiors: {
|
| 18 |
+
title: 'Interiors & Fit‑outs',
|
| 19 |
+
body:
|
| 20 |
+
'Turnkey interiors: MEP coordination, partitioning, flooring, carpentry, and finishing for office and ' +
|
| 21 |
+
'residential projects. <!-- TODO: replace with official copy -->'
|
| 22 |
+
},
|
| 23 |
+
'project-management': {
|
| 24 |
+
title: 'Project Management',
|
| 25 |
+
body:
|
| 26 |
+
'Integrated planning, procurement, vendor management, and reporting for on‑time, on‑budget delivery. ' +
|
| 27 |
+
'<!-- TODO: replace with official copy -->'
|
| 28 |
+
}
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
export default function ServiceDetail() {
|
| 32 |
+
const { id } = useParams();
|
| 33 |
+
const svc = SERVICE_COPY[id] || {
|
| 34 |
+
title: 'Service',
|
| 35 |
+
body: 'Details coming soon. <!-- TODO: replace -->'
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<article className="section" aria-labelledby="service-heading">
|
| 40 |
+
<div className="container grid items-start gap-8 md:grid-cols-3">
|
| 41 |
+
<header className="md:col-span-2">
|
| 42 |
+
<h1 id="service-heading" className="h2">{svc.title}</h1>
|
| 43 |
+
<p className="mt-4 text-slate-700">{svc.body}</p>
|
| 44 |
+
</header>
|
| 45 |
+
<aside className="card p-6">
|
| 46 |
+
<h2 className="h3">Talk to our team</h2>
|
| 47 |
+
<p className="mt-2 text-sm text-slate-600">
|
| 48 |
+
Share your requirements and we will recommend the right scope.
|
| 49 |
+
</p>
|
| 50 |
+
<a href="/contact" className="btn btn-primary mt-4">Contact Us</a>
|
| 51 |
+
</aside>
|
| 52 |
+
</div>
|
| 53 |
+
</article>
|
| 54 |
+
);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
src/styles/global.css
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
/* Global resets and utilities */
|
| 6 |
+
:root {
|
| 7 |
+
--ring: 0 0% 0%;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
body.dot-cursor-enabled {
|
| 11 |
+
cursor: none;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body.dot-cursor-enabled a,
|
| 15 |
+
body.dot-cursor-enabled button,
|
| 16 |
+
body.dot-cursor-enabled [role="button"],
|
| 17 |
+
body.dot-cursor-enabled input,
|
| 18 |
+
body.dot-cursor-enabled textarea,
|
| 19 |
+
body.dot-cursor-enabled select {
|
| 20 |
+
cursor: none;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.dot-cursor[data-state='active'] {
|
| 24 |
+
box-shadow: 0 0 25px -2px rgba(211, 155, 35, 0.55);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Skip link for accessibility */
|
| 28 |
+
.skip-link {
|
| 29 |
+
@apply sr-only focus:not-sr-only fixed left-4 top-4 z-[9999] rounded bg-brand-600 px-4 py-2 text-white;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* Buttons */
|
| 33 |
+
.btn {
|
| 34 |
+
@apply inline-flex items-center justify-center rounded-md px-5 py-3 text-sm font-semibold transition
|
| 35 |
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
|
| 36 |
+
}
|
| 37 |
+
.btn-primary {
|
| 38 |
+
@apply bg-brand-600 text-white hover:bg-brand-700 focus-visible:ring-brand-600;
|
| 39 |
+
}
|
| 40 |
+
.btn-ghost {
|
| 41 |
+
@apply text-white/90 hover:text-white;
|
| 42 |
+
}
|
| 43 |
+
.btn-outline {
|
| 44 |
+
@apply border border-brand-600 text-brand-700 hover:bg-brand-50 focus-visible:ring-brand-600;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Cards */
|
| 48 |
+
.card {
|
| 49 |
+
@apply rounded-xl bg-white shadow-card transition hover:-translate-y-0.5 hover:shadow-lg;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* Shine effect for primary CTAs */
|
| 53 |
+
.shine {
|
| 54 |
+
position: relative;
|
| 55 |
+
overflow: hidden;
|
| 56 |
+
}
|
| 57 |
+
.shine::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: absolute;
|
| 60 |
+
inset: 0;
|
| 61 |
+
transform: translateX(-120%);
|
| 62 |
+
background: linear-gradient(120deg, transparent 0%, rgba(255,255,255,0.35) 40%, transparent 80%);
|
| 63 |
+
transition: transform 700ms ease;
|
| 64 |
+
}
|
| 65 |
+
.shine:hover::after {
|
| 66 |
+
transform: translateX(120%);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* Section spacing */
|
| 70 |
+
.section {
|
| 71 |
+
@apply py-16 md:py-24 relative;
|
| 72 |
+
z-index: 10;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* Headings */
|
| 76 |
+
.h1 {
|
| 77 |
+
@apply text-4xl font-extrabold tracking-tight md:text-5xl;
|
| 78 |
+
}
|
| 79 |
+
.h2 {
|
| 80 |
+
@apply text-3xl font-bold tracking-tight md:text-4xl;
|
| 81 |
+
}
|
| 82 |
+
.h3 {
|
| 83 |
+
@apply text-2xl font-semibold tracking-tight;
|
| 84 |
+
}
|
| 85 |
+
.lead {
|
| 86 |
+
@apply text-base text-slate-600 md:text-lg;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* Header backdrop transition */
|
| 90 |
+
.header-transparent {
|
| 91 |
+
@apply bg-transparent;
|
| 92 |
+
}
|
| 93 |
+
.header-solid {
|
| 94 |
+
@apply bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/80 shadow;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|