Add owner portal, i18n, contact API, and region/footer assets

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Goutam0612
2026-06-20 01:59:41 +05:30
parent 5ff87c6d00
commit 783c7da7a1
146 changed files with 16300 additions and 1611 deletions
+31
View File
@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import "@/components/portal/portal.css";
import { LanguageProvider } from "@/components/i18n";
export const metadata: Metadata = {
title: "YEIDA Portal — Yamuna Expressway Property Management",
description:
"Allottee portal for the Yamuna Expressway Industrial Development Authority — manage your plot, payments, documents, construction and more.",
};
export default function PortalRootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
{/* Same fonts as the main website. Loaded via <link> (not @import) so
they survive Next's CSS processing. */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Heebo:wght@300;400;500;600;700&family=Parkinsans:wght@300..800&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Philosopher:wght@400;700&family=Gloock&display=swap"
rel="stylesheet"
/>
</head>
<body className="anim-on"><LanguageProvider>{children}</LanguageProvider></body>
</html>
);
}
@@ -0,0 +1,8 @@
'use client';
import { Assistant } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function AssistantPage() {
const { go, toast } = usePortal();
return <Assistant go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Construction } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function ConstructionPage() {
const { go, toast } = usePortal();
return <Construction go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Dashboard } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function DashboardPage() {
const { go, toast } = usePortal();
return <Dashboard go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Documents } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function DocumentsPage() {
const { go, toast } = usePortal();
return <Documents go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Journey } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function JourneyPage() {
const { go, toast } = usePortal();
return <Journey go={go} toast={toast} />;
}
+26
View File
@@ -0,0 +1,26 @@
'use client';
import { usePathname } from 'next/navigation';
import { usePortalNav, routeKeyFromPath } from '@/components/portal/nav';
import { useToast } from '@/components/portal/ui';
import { Shell } from '@/components/portal/pages';
import { PortalProvider } from '@/components/portal/portal-context';
export default function PortalAppLayout({
children,
}: {
children: React.ReactNode;
}) {
const go = usePortalNav();
const pathname = usePathname();
const route = routeKeyFromPath(pathname);
const [toastNode, toast] = useToast();
return (
<PortalProvider go={go} toast={toast}>
<Shell route={route} go={go}>
{children}
</Shell>
{toastNode}
</PortalProvider>
);
}
@@ -0,0 +1,8 @@
'use client';
import { MyJourney } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function MyJourneyPage() {
const { go, toast } = usePortal();
return <MyJourney go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Notices } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function NoticesPage() {
const { go, toast } = usePortal();
return <Notices go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Payments } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function PaymentsPage() {
const { go, toast } = usePortal();
return <Payments go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { MyPlot } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function PlotPage() {
const { go, toast } = usePortal();
return <MyPlot go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Profile } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function ProfilePage() {
const { go, toast } = usePortal();
return <Profile go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { SamplePlans } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function SamplePlansPage() {
const { go, toast } = usePortal();
return <SamplePlans go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { SectorMaps } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function SectorMapsPage() {
const { go, toast } = usePortal();
return <SectorMaps go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Support } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function SupportPage() {
const { go, toast } = usePortal();
return <Support go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { Transfer } from '@/components/portal/pages';
import { usePortal } from '@/components/portal/portal-context';
export default function TransferPage() {
const { go, toast } = usePortal();
return <Transfer go={go} toast={toast} />;
}
@@ -0,0 +1,8 @@
'use client';
import { AllotmentLookup } from '@/components/portal/onboarding';
import { usePortalNav } from '@/components/portal/nav';
export default function AllotmentLookupPage() {
const go = usePortalNav();
return <AllotmentLookup go={go} />;
}
+15
View File
@@ -0,0 +1,15 @@
'use client';
import '@/components/portal/site-header.css';
import Header from '@/components/Header';
import { LoginSelect } from '@/components/portal/auth';
import { usePortalNav } from '@/components/portal/nav';
export default function LoginPage() {
const go = usePortalNav();
return (
<>
<Header />
<LoginSelect go={go} />
</>
);
}
+46
View File
@@ -0,0 +1,46 @@
'use client';
import { Icon, Logo, PageHead, SectorMaps } from '@/components/portal/pages';
import { usePortalNav } from '@/components/portal/nav';
export default function PublicMapsPage() {
const go = usePortalNav();
return (
<div style={{ minHeight: '100vh', background: 'var(--bg)' }}>
<header
style={{
height: 64,
background: '#fff',
borderBottom: '1px solid var(--line)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 32px',
position: 'sticky',
top: 0,
zIndex: 50,
}}
>
<button onClick={() => go('welcome')} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
<Logo size={36} />
</button>
<div className="row gap-3">
<button className="btn btn-ghost" onClick={() => go('welcome')}>
<Icon name="chevL" size={16} />
Home
</button>
<button className="btn btn-primary" onClick={() => go('login')}>
Login to Portal
<Icon name="arrowR" size={17} />
</button>
</div>
</header>
<div style={{ maxWidth: 1600, margin: '0 auto', padding: '28px 36px' }}>
<PageHead
title="Explore Sector Maps"
subtitle="Browse plotted layouts and master plans across YEIDA sectors — no login required"
/>
<SectorMaps go={go} />
</div>
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
import { redirect } from 'next/navigation';
// The portal has no public landing page — entering /portal sends users
// straight to the login screen.
export default function PortalIndex() {
redirect('/portal/login');
}
@@ -0,0 +1,8 @@
'use client';
import { Register } from '@/components/portal/onboarding';
import { usePortalNav } from '@/components/portal/nav';
export default function RegisterPage() {
const go = usePortalNav();
return <Register go={go} />;
}
+8
View File
@@ -0,0 +1,8 @@
'use client';
import { Verify } from '@/components/portal/auth';
import { usePortalNav } from '@/components/portal/nav';
export default function VerifyPage() {
const go = usePortalNav();
return <Verify go={go} />;
}
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { Fragment } from "react";
export const metadata: Metadata = { title: "About Us — Yamuna Expressway Authority" };
@@ -16,23 +17,23 @@ const LEADERS = [
];
const TEAM = [
{ name: "Pankaj Mishra", role: "Project Manager", email: "pankaj@yamunaexpressway.com", img: "0b7c87261b26ce5b05ffa1512e77607ea822a40f" },
{ name: "Priya Sharma", role: "Operations Head", email: "pankaj@yamunaexpressway.com", img: "6ec9dc1d3bec2b6daa67f499e88e658014d4391d" },
{ name: "Pankaj Mishra", role: "Project Manager", email: "pankaj@yamunaexpressway.com", img: "a19d44ed523529a5b722a9d59a23264b7664bf27" },
{ name: "Priya Sharma", role: "Operations Head", email: "pankaj@yamunaexpressway.com", img: "4f31d0bb3ef9597350034b4f2e52797a2b73fece" },
{ name: "Amit Patel", role: "Chief Engineer", email: "pankaj@yamunaexpressway.com", img: "a19d44ed523529a5b722a9d59a23264b7664bf27" },
{ name: "Sneha Reddy", role: "Finance Director", email: "pankaj@yamunaexpressway.com", img: "4f31d0bb3ef9597350034b4f2e52797a2b73fece" },
];
const EXPERTS = [
{ n: "25+", t: "Architectural Team", d: "Licensed architects with international awards and 15+ years of luxury villa design experience" },
{ n: "150+", t: "Construction Experts", d: "Skilled engineers, supervisors, and craftsmen ensuring premium quality at every stage" },
{ n: "40+", t: "Project Managers", d: "Dedicated professionals managing timelines, budgets, and client communication with precision" },
{ n: "25+", t: "Architectural Team", d: "Licensed architects with international awards and 15+ years of luxury villa design experience", icon: "25.svg" },
{ n: "150+", t: "Construction Experts", d: "Skilled engineers, supervisors, and craftsmen ensuring premium quality at every stage", icon: "150.svg" },
{ n: "40+", t: "Project Managers", d: "Dedicated professionals managing timelines, budgets, and client communication with precision", icon: "40.svg" },
];
const BADGES = [
{ n: "9001:2015", t: "ISO Certified" },
{ n: "Verified", t: "RERA Registered" },
{ n: "12+", t: "Industry Awards" },
{ n: "IGBC Rated", t: "Green Building" },
{ n: "9001:2015", t: "ISO Certified", icon: "isocertified.svg" },
{ n: "Verified", t: "RERA Registered", icon: "verified.svg" },
{ n: "12+", t: "Industry Awards", icon: "12.svg" },
{ n: "IGBC Rated", t: "Green Building", icon: "igbc.svg" },
];
const STEPS = [
@@ -113,22 +114,22 @@ export default function AboutPage() {
</div>
<div className="ab-leaders">
<article className="ab-leader">
<img src={`${IMG}${LEADERS[0].img}.png`} alt={LEADERS[0].name} />
<div className="teamwrap"> <img src={`${IMG}${LEADERS[0].img}.png`} alt={LEADERS[0].name} /></div>
<div className="ab-leader-info">
<h3>{LEADERS[0].name}</h3>
<span>{LEADERS[0].role}</span>
<div className="leadcont"> <h3>{LEADERS[0].name}</h3>
<span>{LEADERS[0].role}</span></div>
<a href={`mailto:${LEADERS[0].email}`}>{LEADERS[0].email}</a>
</div>
</article>
<div className="ab-leader-emblem">
<img src={`${IMG}30626e48a07e8dee69dff426122797a6f4b26a5b.png`} alt="" />
<span>Registered Contractor</span>
<span>Registered <br/> Contractor</span>
</div>
<article className="ab-leader right">
<img src={`${IMG}${LEADERS[1].img}.png`} alt={LEADERS[1].name} />
<div className="teamwrap"> <img src={`${IMG}${LEADERS[1].img}.png`} alt={LEADERS[1].name} /></div>
<div className="ab-leader-info">
<h3>{LEADERS[1].name}</h3>
<span>{LEADERS[1].role}</span>
<div className="leadcont"> <h3>{LEADERS[1].name}</h3>
<span>{LEADERS[1].role}</span> </div>
<a href={`mailto:${LEADERS[1].email}`}>{LEADERS[1].email}</a>
</div>
</article>
@@ -145,9 +146,12 @@ export default function AboutPage() {
{TEAM.map((m) => (
<article key={m.name} className="ab-member">
<div className="ab-member-photo"><img src={`${IMG}${m.img}.png`} alt={m.name} /></div>
<h4>{m.name}</h4>
<span>{m.role}</span>
<div className="leadcontwrap">
<div className="leadcont">
<h4>{m.name}</h4>
<span>{m.role}</span> </div>
<a href={`mailto:${m.email}`}>{m.email}</a>
</div>
</article>
))}
</div>
@@ -157,7 +161,7 @@ export default function AboutPage() {
{/* Experts */}
<section className="section ab-experts">
<div className="container">
<div className="why-head" style={{ marginInline: "auto", textAlign: "center" }}>
<div className="why-head" >
<p className="eyebrow">Expert Team</p>
<h2 className="section-title">Meet Our Experts</h2>
<p className="lead">Award-winning architects, experienced engineers, and dedicated construction professionals committed to excellence</p>
@@ -170,15 +174,18 @@ export default function AboutPage() {
<h4>{e.t}</h4>
<p>{e.d}</p>
</div>
<span className="ab-expert-icon"></span>
<img className="ab-expert-icon" src={`${IMG}${e.icon}`} alt="" />
</div>
))}
</div>
<div className="ab-badge-grid">
{BADGES.map((b) => (
<div key={b.t} className="ab-cert">
<strong>{b.n}</strong>
<span>{b.t}</span>
<div>
<strong>{b.n}</strong>
<span>{b.t}</span>
</div>
<img className="ab-cert-icon" src={`${IMG}${b.icon}`} alt="" />
</div>
))}
</div>
@@ -195,12 +202,19 @@ export default function AboutPage() {
</div>
<div className="ab-steps">
{STEPS.map((s, i) => (
<div key={s.n} className="ab-step">
{i === 3 && <img className="ab-step-img" src={`${IMG}c376c66c882591b36ac351bdd4592497b8acd713.png`} alt="" />}
<strong>{s.n}</strong>
<h4>{s.t}</h4>
<p>{s.d}</p>
</div>
<Fragment key={s.n}>
{i === 5 && (
<div className="ab-step ab-step-card">
<img className="ab-step-cardimg" src={`${IMG}c376c66c882591b36ac351bdd4592497b8acd713.png`} alt="" />
</div>
)}
<div className="ab-step">
{i === 3 && <img className="ab-step-img" src={`${IMG}c376c66c882591b36ac351bdd4592497b8acd713.png`} alt="" />}
<strong>{s.n}</strong>
<h4>{s.t}</h4>
<p>{s.d}</p>
</div>
</Fragment>
))}
</div>
</div>
+188
View File
@@ -0,0 +1,188 @@
"use client";
import { useState } from "react";
const EMPTY = {
firstName: "",
lastName: "",
email: "",
mobile: "",
subject: "",
message: "",
};
export default function ContactForm() {
const [form, setForm] = useState(EMPTY);
const [status, setStatus] = useState<"idle" | "sending" | "error">("idle");
const [error, setError] = useState("");
const [done, setDone] = useState(false);
const update =
(key: keyof typeof EMPTY) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
setForm((f) => ({ ...f, [key]: e.target.value }));
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus("sending");
setError("");
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || "Something went wrong. Please try again.");
}
setForm(EMPTY);
setStatus("idle");
setDone(true);
} catch (err) {
setStatus("error");
setError(err instanceof Error ? err.message : "Something went wrong.");
}
}
return (
<>
<form className="ct-form" onSubmit={onSubmit}>
<h2 className="section-title" style={{ fontSize: 40 }}>
Let&rsquo;s connect!
</h2>
<p className="ct-form-sub">
Have questions or need assistance? <br /> We&rsquo;re here to help!
</p>
<div className="ct-row">
<div className="ct-field">
<span className="ct-icon">
<img src="/img/firstname.svg" alt="" />
</span>
<input
type="text"
placeholder="First Name"
value={form.firstName}
onChange={update("firstName")}
required
/>
</div>
<div className="ct-field">
<span className="ct-icon">
<img src="/img/firstname.svg" alt="" />
</span>
<input
type="text"
placeholder="Last Name"
value={form.lastName}
onChange={update("lastName")}
/>
</div>
</div>
<div className="ct-row">
<div className="ct-field">
<span className="ct-icon">
<img src="/img/emailform.svg" alt="" />
</span>
<input
type="email"
placeholder="Email Address"
value={form.email}
onChange={update("email")}
required
/>
</div>
<div className="ct-field">
<span className="ct-icon">
<img src="/img/mobilenumber.svg" alt="" />
</span>
<input
type="tel"
placeholder="Mobile Number"
value={form.mobile}
onChange={update("mobile")}
/>
</div>
</div>
<div className="ct-field ct-field-full">
<span className="ct-icon">
<img src="/img/subject.svg" alt="" />
</span>
<input
type="text"
placeholder="Subjects"
value={form.subject}
onChange={update("subject")}
/>
</div>
<div className="ct-field ct-field-full ct-textarea-wrap">
<span className="ct-icon" style={{ alignSelf: "flex-start", marginTop: "2px" }}>
<img src="/img/message.svg" alt="" />
</span>
<textarea
rows={5}
placeholder="Write your message here..."
value={form.message}
onChange={update("message")}
required
/>
</div>
{status === "error" && <p className="ct-error">{error}</p>}
<div className="ct-submit-row">
<button
type="submit"
className="btn btn-dark ct-submit"
disabled={status === "sending"}
>
{status === "sending" ? "Sending…" : "Submit"}{" "}
<span className="arrow"></span>
</button>
</div>
</form>
{done && (
<div
className="ct-thanks-overlay"
role="dialog"
aria-modal="true"
onClick={() => setDone(false)}
>
<div className="ct-thanks-card" onClick={(e) => e.stopPropagation()}>
<button
className="ct-thanks-close"
aria-label="Close"
onClick={() => setDone(false)}
>
×
</button>
<div className="ct-thanks-check">
<svg viewBox="0 0 24 24" width="34" height="34" aria-hidden="true">
<path
d="M20 6 9 17l-5-5"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<h3>Thank you!</h3>
<p>
Your message has been received. Our team will get back to you
shortly.
</p>
<button className="btn btn-dark" onClick={() => setDone(false)}>
Done
</button>
</div>
</div>
)}
</>
);
}
@@ -3,6 +3,7 @@ import "./style.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import ScrollFX from "@/components/ScrollFX";
import { LanguageProvider } from "@/components/i18n";
export const metadata: Metadata = {
title: "Yamuna Expressway Authority — Build Your Dream Home",
@@ -18,10 +19,12 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<Header />
<main>{children}</main>
<Footer />
<ScrollFX />
<LanguageProvider>
<Header />
<main>{children}</main>
<Footer />
<ScrollFX />
</LanguageProvider>
</body>
</html>
);
+135 -54
View File
@@ -77,12 +77,12 @@ const COMPANIES = [
];
const WHY = [
{ t: "Transparent Pricing", d: "No hidden costs. Detailed BOQ with itemized pricing for every material and service." },
{ t: "Contract-Based Construction", d: "Legal agreements protecting your investment with clear deliverables and timelines." },
{ t: "Milestone Payments", d: "Pay only based on verified completion stages. Your money stays safe with escrow options." },
{ t: "Dedicated Project Manager", d: "Single point of contact managing your entire project from conception to completion." },
{ t: "Authority Support", d: "We handle all approvals, NOCs, and legal documentation with government authorities." },
{ t: "Live Project Tracking", d: "Real-time updates via app with photos, videos, and milestone completion reports." },
{ t: "Transparent Pricing", d: "No hidden costs. Detailed BOQ with itemized pricing for every material and service.", img: "631d471a32ee1dcc3f6e79f1d5eba77011cf4a50" },
{ t: "Contract-Based Construction", d: "Legal agreements protecting your investment with clear deliverables and timelines.", img: "6529219216839b0700cdffee82ac9490aba78014" },
{ t: "Milestone Payments", d: "Pay only based on verified completion stages. Your money stays safe with escrow options.", img: "b531d04468f7eb1eee6b5e3f79b73ed1b8d100ed" },
{ t: "Dedicated Project Manager", d: "Single point of contact managing your entire project from conception to completion.", img: "5cbc2b713ab3173098792ba280d0f70b6b91b370" },
{ t: "Authority Support", d: "We handle all approvals, NOCs, and legal documentation with government authorities.", img: "ca01253b7173e27cd1fc5ac3280f55a300f96bcc" },
{ t: "Live Project Tracking", d: "Real-time updates via app with photos, videos, and milestone completion reports.", img: "888fe17730be5dc5dfc0f500a63c3ff6092ada9a" },
];
const TESTIMONIALS = [
@@ -101,6 +101,30 @@ function Stars({ n }: { n: number }) {
);
}
type Testimonial = (typeof TESTIMONIALS)[number];
function TestiQuote({ t, size }: { t: Testimonial; size: "large" | "medium" }) {
return (
<article className={`testi-card ${size}`}>
<span className="quote-mark"><img src="img/testquote.svg" alt="" /></span>
<Stars n={t.stars} />
<p className="testi-quote">{t.quote}</p>
</article>
);
}
function TestiAuthor({ t, top }: { t: Testimonial; top?: boolean }) {
return (
<div className={`testi-author-card ${top ? "top-author" : ""}`}>
<img src={`${IMG}${t.avatar}.png`} alt={t.name} />
<div>
<strong>{t.name}</strong>
<span>{t.role}</span>
</div>
</div>
);
}
export default function Home() {
const [walk, setWalk] = useState(0);
const [projTab, setProjTab] = useState<"current" | "upcoming">("current");
@@ -110,6 +134,9 @@ export default function Home() {
const [pinH, setPinH] = useState(0);
const journeyRef = useRef<HTMLDivElement | null>(null);
const [why, setWhy] = useState(0);
const [whyPinned, setWhyPinned] = useState(false);
const [whyPinH, setWhyPinH] = useState(0);
const whyRef = useRef<HTMLDivElement | null>(null);
// Each "stop" = one scroll step. Walk every card, revealing its items one by one.
const journeyStops = useMemo(() => {
@@ -161,6 +188,47 @@ export default function Home() {
};
}, [journeyStops]);
// Pin the Why section; scroll advances the open row (+ its image) on the right side.
useEffect(() => {
const STEP_VH = 0.7; // viewport-heights of scroll per row
const small = window.matchMedia("(max-width: 768px)");
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)");
const layout = () => {
const on = !small.matches && !reduce.matches;
setWhyPinned(on);
setWhyPinH(on ? Math.round(WHY.length * STEP_VH * window.innerHeight + window.innerHeight) : 0);
};
let raf = 0;
const onScroll = () => {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
const el = whyRef.current;
if (!el || small.matches || reduce.matches) return;
const total = el.offsetHeight - window.innerHeight;
const scrolled = Math.min(Math.max(-el.getBoundingClientRect().top, 0), total);
const p = total > 0 ? scrolled / total : 0;
setWhy(Math.min(WHY.length - 1, Math.max(0, Math.floor(p * WHY.length))));
});
};
layout();
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", layout);
small.addEventListener("change", layout);
reduce.addEventListener("change", layout);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", layout);
small.removeEventListener("change", layout);
reduce.removeEventListener("change", layout);
if (raf) cancelAnimationFrame(raf);
};
}, []);
return (
<>
{/* ============================ HERO ============================ */}
@@ -187,7 +255,7 @@ export default function Home() {
<br />a single accountable team for your YEIDA villa.
</p>
<Link href="/contact" className="btn btn-light hero-cta">
Free Consultation <span className="arrow"></span>
Free Consultation <span className="arrow"><img src="/img/arrowblack.svg" alt="" /></span>
</Link>
</div>
<span className="hero-note">Yamuna Expressway<br />is the new NCR.</span>
@@ -244,7 +312,7 @@ export default function Home() {
<div className="container plots-pager">
<span className="pager-count">01 / 09</span>
<div className="pager-line"><span /></div>
<div className="pager-btns"><button></button><button></button></div>
<div className="pager-btns"><button className="pager-prev"><img src="/img/arrowchoosecanvas.svg" alt="Previous" /></button><button><img src="/img/arrowchoosecanvas.svg" alt="Next" /></button></div>
</div>
</section>
@@ -416,7 +484,7 @@ export default function Home() {
</div>
{/* Bottom Right Small Image */}
<div className="journey-thumb"></div>
<div className="journey-thumb"><img src={`${IMG}${j.img}.png`} alt={j.title} /></div>
</div>
);
})}
@@ -435,7 +503,7 @@ export default function Home() {
</div>
<div className="virtual-stage">
<img src={`${IMG}f470f21a29fb7fe287ce1d5ffb94d0b4845b5367.png`} alt="500 sqm Contemporary Villa" />
<div className="virtual-overlay" />
<button className="play-btn" aria-label="Play"><span></span></button>
<div className="virtual-caption">
<h3>500 sqm Contemporary Villa</h3>
@@ -443,7 +511,7 @@ export default function Home() {
</div>
<div className="virtual-features">
{WALK_FEATURES.map((f) => (
<span key={f}> {f}</span>
<span key={f}><img src="img/tick.svg" alt="" /> {f}</span>
))}
</div>
</div>
@@ -480,27 +548,39 @@ export default function Home() {
{/* ======================= WHY CHOOSE US ====================== */}
<section className="section why">
<div className="container">
<div className="why-head">
<p className="eyebrow">Excellence Guaranteed</p>
<h2 className="section-title">Why Choose Us</h2>
<p className="lead">Industry-leading standards combined with personalized service for an unmatched construction experience</p>
</div>
<div className="why-grid">
<div className="why-visual">
<img className="why-tall" src={`${IMG}1cf7daea2239eeca5e11497628834804a0b5ec46.png`} alt="" />
</div>
<div className="why-list">
<img className="why-wide" src={`${IMG}888fe17730be5dc5dfc0f500a63c3ff6092ada9a.png`} alt="" />
{WHY.map((w, i) => (
<div key={w.t} className={`why-row ${i === why ? "open" : ""}`} onClick={() => setWhy(i)}>
<span className="why-no">{String(i + 1).padStart(2, "0")}</span>
<div className="why-text">
<h4>{w.t}</h4>
{i === why && <p>{w.d}</p>}
</div>
<div
ref={whyRef}
className={`why-pin ${whyPinned ? "is-pinned" : ""}`}
style={whyPinned && whyPinH ? { height: whyPinH } : undefined}
>
<div className="why-sticky">
<div className="container">
<div className="why-head">
<p className="eyebrow">Excellence Guaranteed</p>
<h2 className="section-title">Why Choose Us</h2>
<p className="lead">Industry-leading standards combined with personalized service for an unmatched construction experience</p>
</div>
<div className="why-grid">
<div className="why-visual">
<img className="why-tall" src={`${IMG}1cf7daea2239eeca5e11497628834804a0b5ec46.png`} alt="" />
</div>
))}
<div className="why-list">
{WHY.map((w, i) => (
<div key={w.t} className={`why-row ${i === why ? "open" : ""}`} onClick={() => setWhy(i)}>
<div className="why-media">
<img className="why-wide" src={`${IMG}${w.img}.png`} alt="" />
</div>
<div className="why-row-head">
<span className="why-no">{String(i + 1).padStart(2, "0")}</span>
<div className="why-text">
<h4>{w.t}</h4>
<p className="why-desc">{w.d}</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
@@ -512,15 +592,15 @@ export default function Home() {
<div className="showcase-overlay" />
<span className="showcase-brand">HOUSECOIN</span>
<div className="showcase-card">
<img src={`${IMG}dac66fb1980459d72c43173854d68cab518070cd.png`} alt="The PIQUE of Home Design" />
<div className="showcase-bar">
<span className="sc-menu"> MENU</span>
<span className="sc-logo">PIQUE</span>
<span className="sc-contact"> CONTACT</span>
</div>
<div className="showcase-caption">
<h3>The PIQUE of<br />Home Design</h3>
</div>
<video
autoPlay
muted
loop
playsInline
poster={`${IMG}dac66fb1980459d72c43173854d68cab518070cd.png`}
>
<source src={`${IMG}PIQUE.mp4`} type="video/mp4" />
</video>
</div>
</section>
@@ -535,20 +615,21 @@ export default function Home() {
<p className="lead">Every owner here is reachable for a reference call. Every plot is registered to verifiable YEIDA records.</p>
</div>
<div className="testi-grid">
{TESTIMONIALS.map((t) => (
<article key={t.name} className="testi-card">
<span className="quote-mark">&ldquo;</span>
<Stars n={t.stars} />
<p className="testi-quote">{t.quote}</p>
<div className="testi-author">
<img src={`${IMG}${t.avatar}.png`} alt={t.name} />
<div>
<strong>{t.name}</strong>
<span>{t.role}</span>
</div>
</div>
</article>
))}
{/* LEFT */}
<div className="testi-col">
<TestiQuote t={TESTIMONIALS[0]} size="large" />
<TestiAuthor t={TESTIMONIALS[0]} />
</div>
{/* CENTER */}
<div className="testi-col">
<TestiAuthor t={TESTIMONIALS[1]} top />
<TestiQuote t={TESTIMONIALS[1]} size="medium" />
</div>
{/* RIGHT */}
<div className="testi-col">
<TestiQuote t={TESTIMONIALS[2]} size="large" />
<TestiAuthor t={TESTIMONIALS[2]} />
</div>
</div>
<div className="testi-more"><Link href="/blog">View all Stories</Link></div>
</div>
@@ -26,10 +26,10 @@ const VILLAS = [
];
const ACROSS = [
{ n: "12", t: "Uttar Pradesh", icon: "🏛" },
{ n: "5", t: "Uttarakhand", icon: "⛰" },
{ n: "8", t: "Delhi NCR", icon: "🏙" },
{ n: "6", t: "Haryana", icon: "🌾" },
{ n: "12", t: "Uttar Pradesh" },
{ n: "5", t: "Uttarakhand" },
{ n: "8", t: "Delhi NCR" },
{ n: "6", t: "Haryana" },
];
const COMPANIES = [
@@ -43,10 +43,10 @@ export default function ProjectsPage() {
return (
<>
<section className="pr-hero">
<section className="faq-hero">
<img src={`${IMG}34f8cb9b04c0ce5ee0362f88dd763769431d82d8.png`} alt="" className="pr-hero-bg" />
<div className="pr-hero-overlay" />
<div className="container pr-hero-inner">
<div className="faq-hero-overlay" />
<div className="container faq-hero-inner">
<h1>Our Projects</h1>
<p>Showcasing the landmarks, infrastructure, and developments we have successfully delivered with quality, precision, and excellence.</p>
</div>
@@ -129,12 +129,12 @@ export default function ProjectsPage() {
<article key={v.name} className="villa-card">
<div className="villa-media">
<img src={`${IMG}${v.img}.png`} alt={v.name} />
<span className="villa-tags"><span>{v.bhk}</span><span>{v.sqm}</span></span>
<span className="villa-floor">{v.floor}</span>
<span className="villa-tags"><span>{v.bhk}</span> <span className="villa-floor">{v.floor}</span><span>{v.sqm}</span></span>
</div>
<div className="villa-body">
<h3>{v.name}</h3>
<p className="villa-desc">{v.desc}</p>
<span> <h3>{v.name}</h3>
<p className="villa-desc">{v.desc}</p></span>
<div className="villa-foot">
<div className="villa-price"><small>FROM</small>{v.price}</div>
<span className="villa-months">{v.months}</span>
@@ -166,7 +166,9 @@ export default function ProjectsPage() {
<span>Projects</span>
<h4>{a.t}</h4>
</div>
<span className="pr-across-icon">{a.icon}</span>
<span className="pr-across-icon">
<img src={`${IMG}${encodeURIComponent(a.t)}.png`} alt={a.t} />
</span>
</div>
))}
</div>
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
import { NextResponse } from "next/server";
type ContactBody = {
firstName?: string;
lastName?: string;
email?: string;
mobile?: string;
subject?: string;
message?: string;
};
export async function POST(request: Request) {
const token = process.env.TEABLE_API_TOKEN;
const tableId = process.env.TEABLE_TABLE_ID;
if (!token || !tableId) {
return NextResponse.json(
{ error: "Server is not configured for submissions." },
{ status: 500 }
);
}
let body: ContactBody;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid request body." }, { status: 400 });
}
const firstName = (body.firstName ?? "").trim();
const lastName = (body.lastName ?? "").trim();
const email = (body.email ?? "").trim();
const mobile = (body.mobile ?? "").trim();
const subject = (body.subject ?? "").trim();
const message = (body.message ?? "").trim();
const name = `${firstName} ${lastName}`.trim();
if (!name || !email || !message) {
return NextResponse.json(
{ error: "Name, email and message are required." },
{ status: 400 }
);
}
// The Teable table is job-application shaped; map contact details onto the
// free-text fields that fit. Subject + mobile are folded into why_role so no
// information is lost.
const why_role = [
subject && `Subject: ${subject}`,
mobile && `Mobile: ${mobile}`,
message,
]
.filter(Boolean)
.join("\n");
const res = await fetch(
`https://app.teable.ai/api/table/${tableId}/record`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
fieldKeyType: "name",
records: [{ fields: { name, email, why_role } }],
}),
}
);
if (!res.ok) {
const detail = await res.text();
console.error("Teable error:", res.status, detail);
return NextResponse.json(
{ error: "Could not save your message. Please try again." },
{ status: 502 }
);
}
return NextResponse.json({ ok: true });
}
-57
View File
@@ -1,57 +0,0 @@
"use client";
export default function ContactForm() {
return (
<form className="ct-form" onSubmit={(e) => e.preventDefault()}>
<h2 className="section-title" style={{ fontSize: 40 }}>Let&rsquo;s connect!</h2>
<p className="ct-form-sub">Have questions or need assistance? <br /> We&rsquo;re here to help!</p>
<div className="ct-row">
<div className="ct-field">
<span className="ct-icon">
<img src="/img/firstname.svg" alt="" />
</span>
<input type="text" placeholder="First Name" />
</div>
<div className="ct-field">
<span className="ct-icon">
<img src="/img/firstname.svg" alt="" />
</span>
<input type="text" placeholder="Last Name" />
</div>
</div>
<div className="ct-row">
<div className="ct-field">
<span className="ct-icon">
<img src="/img/emailform.svg" alt="" />
</span>
<input type="email" placeholder="Email Address" />
</div>
<div className="ct-field">
<span className="ct-icon">
<img src="/img/mobilenumber.svg" alt="" />
</span>
<input type="tel" placeholder="Mobile Number" />
</div>
</div>
<div className="ct-field ct-field-full">
<span className="ct-icon">
<img src="/img/subject.svg" alt="" />
</span>
<input type="text" placeholder="Subjects" />
</div>
<div className="ct-field ct-field-full ct-textarea-wrap">
<span className="ct-icon" style={{ alignSelf: "flex-start", marginTop: "2px" }}>
<img src="/img/message.svg" alt="" />
</span>
<textarea rows={5} placeholder="Write your message here..." />
</div>
<div className="ct-submit-row">
<button type="submit" className="btn btn-dark ct-submit">Submit <span className="arrow"></span></button>
</div>
</form>
);
}
-1424
View File
File diff suppressed because it is too large Load Diff