rushiljain commited on
Commit
691cdd0
·
verified ·
1 Parent(s): e317efb

Upload 29 files

Browse files
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
+