diff --git a/public/img/12.svg b/public/img/12.svg new file mode 100644 index 0000000..28d4278 --- /dev/null +++ b/public/img/12.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/150.svg b/public/img/150.svg new file mode 100644 index 0000000..815fa4d --- /dev/null +++ b/public/img/150.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/25.svg b/public/img/25.svg new file mode 100644 index 0000000..a35cae7 --- /dev/null +++ b/public/img/25.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/img/30626e48a07e8dee69dff426122797a6f4b26a5b.png b/public/img/30626e48a07e8dee69dff426122797a6f4b26a5b.png index 4ef0b25..4a791b5 100644 Binary files a/public/img/30626e48a07e8dee69dff426122797a6f4b26a5b.png and b/public/img/30626e48a07e8dee69dff426122797a6f4b26a5b.png differ diff --git a/public/img/40.svg b/public/img/40.svg new file mode 100644 index 0000000..b71a42d --- /dev/null +++ b/public/img/40.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/Delhi NCR.png b/public/img/Delhi NCR.png new file mode 100644 index 0000000..3f27049 Binary files /dev/null and b/public/img/Delhi NCR.png differ diff --git a/public/img/Delhi NCR.svg b/public/img/Delhi NCR.svg new file mode 100644 index 0000000..c605829 --- /dev/null +++ b/public/img/Delhi NCR.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Haryana.png b/public/img/Haryana.png new file mode 100644 index 0000000..857ff50 Binary files /dev/null and b/public/img/Haryana.png differ diff --git a/public/img/Haryana.svg b/public/img/Haryana.svg new file mode 100644 index 0000000..c605829 --- /dev/null +++ b/public/img/Haryana.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/PIQUE.mp4 b/public/img/PIQUE.mp4 new file mode 100644 index 0000000..9a9eaa1 Binary files /dev/null and b/public/img/PIQUE.mp4 differ diff --git a/public/img/Uttar Pradesh.png b/public/img/Uttar Pradesh.png new file mode 100644 index 0000000..05a463e Binary files /dev/null and b/public/img/Uttar Pradesh.png differ diff --git a/public/img/Uttar Pradesh.svg b/public/img/Uttar Pradesh.svg new file mode 100644 index 0000000..5861ca9 --- /dev/null +++ b/public/img/Uttar Pradesh.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Uttarakhand.png b/public/img/Uttarakhand.png new file mode 100644 index 0000000..28ba964 Binary files /dev/null and b/public/img/Uttarakhand.png differ diff --git a/public/img/Uttarakhand.svg b/public/img/Uttarakhand.svg new file mode 100644 index 0000000..c605829 --- /dev/null +++ b/public/img/Uttarakhand.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/arrowblack.svg b/public/img/arrowblack.svg new file mode 100644 index 0000000..b78523a --- /dev/null +++ b/public/img/arrowblack.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/arrowchoosecanvas.svg b/public/img/arrowchoosecanvas.svg new file mode 100644 index 0000000..28e408d --- /dev/null +++ b/public/img/arrowchoosecanvas.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/footer.png b/public/img/footer.png new file mode 100644 index 0000000..68be208 Binary files /dev/null and b/public/img/footer.png differ diff --git a/public/img/footerfac.svg b/public/img/footerfac.svg new file mode 100644 index 0000000..3208d75 --- /dev/null +++ b/public/img/footerfac.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/footerinsta.svg b/public/img/footerinsta.svg new file mode 100644 index 0000000..59166e7 --- /dev/null +++ b/public/img/footerinsta.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/img/footerlink.svg b/public/img/footerlink.svg new file mode 100644 index 0000000..ac5c702 --- /dev/null +++ b/public/img/footerlink.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/footertwitter.svg b/public/img/footertwitter.svg new file mode 100644 index 0000000..45c5571 --- /dev/null +++ b/public/img/footertwitter.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/footeryou.svg b/public/img/footeryou.svg new file mode 100644 index 0000000..88dc30e --- /dev/null +++ b/public/img/footeryou.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/igbc.svg b/public/img/igbc.svg new file mode 100644 index 0000000..0cedb14 --- /dev/null +++ b/public/img/igbc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/isocertified.svg b/public/img/isocertified.svg new file mode 100644 index 0000000..8a4cda0 --- /dev/null +++ b/public/img/isocertified.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/testquote.svg b/public/img/testquote.svg new file mode 100644 index 0000000..e95b369 --- /dev/null +++ b/public/img/testquote.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/tick.svg b/public/img/tick.svg new file mode 100644 index 0000000..3bad84a --- /dev/null +++ b/public/img/tick.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/verified.svg b/public/img/verified.svg new file mode 100644 index 0000000..638d6c5 --- /dev/null +++ b/public/img/verified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/yeida-logo.png b/public/yeida-logo.png new file mode 100644 index 0000000..a6b65fb Binary files /dev/null and b/public/yeida-logo.png differ diff --git a/src/Yammuna Express/.thumbnail b/src/Yammuna Express/.thumbnail new file mode 100644 index 0000000..2671128 Binary files /dev/null and b/src/Yammuna Express/.thumbnail differ diff --git a/src/Yammuna Express/Aurum Estates Dashboard.html b/src/Yammuna Express/Aurum Estates Dashboard.html new file mode 100644 index 0000000..0f45cbd --- /dev/null +++ b/src/Yammuna Express/Aurum Estates Dashboard.html @@ -0,0 +1,34 @@ + + + + + + Aurum Estates — Luxury Real Estate Dashboard + + + + + + +
+ + + + + + + + + + + + + diff --git a/src/Yammuna Express/YEIDA Portal.html b/src/Yammuna Express/YEIDA Portal.html new file mode 100644 index 0000000..1399ba6 --- /dev/null +++ b/src/Yammuna Express/YEIDA Portal.html @@ -0,0 +1,44 @@ + + + + + + YEIDA Portal — Yamuna Expressway Property Management + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Yammuna Express/app.jsx b/src/Yammuna Express/app.jsx new file mode 100644 index 0000000..86c171c --- /dev/null +++ b/src/Yammuna Express/app.jsx @@ -0,0 +1,56 @@ +/* Main app router */ +(function () { + const { useState, useEffect } = React; + const Icon = window.Icon, Logo = window.Logo, Shell = window.Shell, useToast = window.useToast; + + const APP_ROUTES = ['dashboard', 'plot', 'construction', 'payments', 'documents', 'notices', 'sectormaps', 'transfer', 'assistant', 'support', 'profile', 'sampleplans', 'journey', 'myjourney']; + + function App() { + const [route, setRoute] = useState(() => localStorage.getItem('yeida_route') || 'welcome'); + const [toastNode, toast] = useToast(); + const go = (r) => { setRoute(r); localStorage.setItem('yeida_route', r); window.scrollTo(0, 0); }; + useEffect(() => { document.body.style.background = APP_ROUTES.includes(route) ? 'var(--bg)' : '#fff'; }, [route]); + + // Auth + public + if (route === 'welcome') return React.createElement(window.Welcome, { go }); + if (route === 'login') return React.createElement(window.LoginSelect, { go }); + if (route === 'login-allotment') return React.createElement(window.AllotmentLookup, { go }); + if (route === 'verify' || route === 'otp') return React.createElement(window.Verify, { go }); + if (route === 'register') return React.createElement(window.Register, { go }); + if (route === 'app') { go('dashboard'); return null; } + if (route === 'sectormaps-public') return React.createElement(PublicMaps, { go }); + + // Authenticated app + const content = { + dashboard: window.Dashboard, plot: window.MyPlot, construction: window.Construction, + payments: window.Payments, documents: window.Documents, notices: window.Notices, + sectormaps: window.SectorMaps, transfer: window.Transfer, assistant: window.Assistant, + support: window.Support, profile: window.Profile, + sampleplans: window.SamplePlans, journey: window.Journey, myjourney: window.MyJourney, + }[route] || window.Dashboard; + + return React.createElement(React.Fragment, null, + React.createElement(Shell, { route, go }, + React.createElement(content, { go, toast })), + toastNode + ); + } + + // Public sector maps (pre-login) + function PublicMaps({ go }) { + return React.createElement('div', { style: { minHeight: '100vh', background: 'var(--bg)' } }, + React.createElement('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 } }, + React.createElement('button', { onClick: () => go('welcome'), style: { background: 'none', border: 'none', cursor: 'pointer' } }, React.createElement(Logo, { size: 36 })), + React.createElement('div', { className: 'row gap-3' }, + React.createElement('button', { className: 'btn btn-ghost', onClick: () => go('welcome') }, React.createElement(Icon, { name: 'chevL', size: 16 }), 'Home'), + React.createElement('button', { className: 'btn btn-primary', onClick: () => go('login') }, 'Login to Portal', React.createElement(Icon, { name: 'arrowR', size: 17 }))) + ), + React.createElement('div', { style: { maxWidth: 1600, margin: '0 auto', padding: '28px 36px' } }, + React.createElement(window.PageHead, { title: 'Explore Sector Maps', subtitle: 'Browse plotted layouts and master plans across YEIDA sectors — no login required' }), + React.createElement(window.SectorMaps, { go }) + ) + ); + } + + ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App)); +})(); diff --git a/src/Yammuna Express/assets/yeida-logo.png b/src/Yammuna Express/assets/yeida-logo.png new file mode 100644 index 0000000..a6b65fb Binary files /dev/null and b/src/Yammuna Express/assets/yeida-logo.png differ diff --git a/src/Yammuna Express/assistant.jsx b/src/Yammuna Express/assistant.jsx new file mode 100644 index 0000000..65aed07 --- /dev/null +++ b/src/Yammuna Express/assistant.jsx @@ -0,0 +1,133 @@ +/* AI Assistant */ +(function () { + const { useState, useRef, useEffect } = React; + const Icon = window.Icon; + const D = window.YEIDA; + + const ANSWERS = [ + { match: ['construction', 'start', 'build'], reply: "To start construction on Plot B-47:\n\n1. Ensure possession is granted (✓ done for your plot)\n2. Prepare your architect-stamped building plan\n3. Upload required documents in the Construction module\n4. Submit for YEIDA scrutiny\n\nYour application is currently at Step 4 — Authority Review. Estimated decision: 12 Jun 2026.", action: ['construction', 'Open Construction'] }, + { match: ['document', 'required', 'papers'], reply: "For building plan approval you'll need:\n\n• Approved Building Plan (architect-stamped)\n• Structural Stability Certificate\n• Architect Registration (COA)\n• Soil Test Report\n• Indemnity Bond\n• Possession Certificate copy\n\n5 of 6 are already uploaded. The Soil Test Report is still pending.", action: ['documents', 'View Documents'] }, + { match: ['notice', 'latest', 'update'], reply: "Here are the latest notices:\n\n📌 Revised Annual Lease Rent rates FY 2026-27 (01 Jun)\n📌 Construction guidelines — FAR & setback (24 May)\n• Water supply augmentation, Sector 18 & 20 (18 May)\n\nThe lease rent revision affects your next due on 30 Jun.", action: ['notices', 'Open Notices'] }, + { match: ['transfer', 'sell', 'ownership'], reply: "To transfer Plot B-47:\n\n1. Pass the eligibility check (5-year lock-in — ✓ you're eligible)\n2. Pay transfer charges (~₹2.02 L incl. fees & GST)\n3. Submit the transfer application with transferee KYC\n4. Authority review & approval\n\nWould you like me to open the Transfer module?", action: ['transfer', 'Start Transfer'] }, + { match: ['payment', 'pending', 'due', 'pay'], reply: "You have 3 outstanding dues totalling ₹1,47,585:\n\n• Water Connection Charges — ₹12,400 (Overdue)\n• Annual Lease Rent 2026-27 — ₹38,625 (due 30 Jun)\n• Location Premium 14/16 — ₹96,560 (due 15 Jul)\n\nClear the overdue water charge first to avoid interest.", action: ['payments', 'Pay Dues'] }, + { match: ['map', 'sector', '18', 'plot location'], reply: "Plot B-47 is in Sector 18, a developed residential sector along the Yamuna Expressway. It's an east-facing 300 sq.m plot on a 12 m road in Block B.\n\nI can open the interactive Sector 18 map so you can locate it.", action: ['sectormaps', 'Open Map'] }, + ]; + const fallback = "I can help with construction approvals, payments, documents, transfers, notices and sector maps for your plot. Try one of the suggested questions, or ask me anything about Plot B-47."; + + function reply(text) { + const t = text.toLowerCase(); + const hit = ANSWERS.find(a => a.match.some(m => t.includes(m))); + return hit || { reply: fallback }; + } + + function Assistant({ go }) { + const [msgs, setMsgs] = useState([{ role: 'bot', text: "Hello Rajeev 👋 I'm your YEIDA assistant. Ask me anything about Plot B-47 — construction, payments, documents, transfers or sector maps." }]); + const [input, setInput] = useState(''); + const [typing, setTyping] = useState(false); + const scroller = useRef(null); + useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [msgs, typing]); + + const send = (text) => { + if (!text.trim()) return; + setMsgs(m => [...m, { role: 'user', text }]); + setInput(''); setTyping(true); + setTimeout(() => { + const r = reply(text); + setTyping(false); + setMsgs(m => [...m, { role: 'bot', text: r.reply, action: r.action }]); + }, 900); + }; + + return ( +
+ {/* chat */} +
+
+
+ +
YEIDA AI Assistant
Online · Knowledge base connected
+
+ +
+ +
+ {msgs.length === 1 && ( +
+
+

Ask anything about your plot

+

Choose a question below or type your own.

+
+ {D.assistantSuggestions.map(s => ( + + ))} +
+
+ )} + {msgs.map((m, i) => )} + {typing && ( +
+ +
+ {[0, 1, 2].map(i => )} +
+
+ )} +
+ +
+
+ setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send(input)} placeholder="Ask about construction, payments, documents…" style={{ flex: 1, border: 'none', background: 'none', outline: 'none', fontSize: 14.5, color: 'var(--ink)' }} /> + +
+
AI responses are informational. Verify critical actions with the Authority.
+
+
+ + {/* side panel */} +
+
+

Smart Suggestions

+
+ {['What payments are pending?', 'Show Sector 18 map', 'How can I transfer my plot?'].map(s => ( + + ))} +
+
+
+

Knowledge Sources

+
+ {[['Notices & Circulars', 'notices'], ['Document Center', 'documents'], ['Building Byelaws', 'file'], ['Sector Master Plans', 'map']].map(([l, ic]) => ( +
{l}
+ ))} +
+
+
+ +
Need a human?
+

Escalate to a YEIDA support officer for complex queries.

+ +
+
+
+ ); + } + + function Bubble({ m, go }) { + const isUser = m.role === 'user'; + return ( +
+ + + +
+
{m.text}
+ {m.action && } +
+
+ ); + } + + window.Assistant = Assistant; +})(); diff --git a/src/Yammuna Express/auth.jsx b/src/Yammuna Express/auth.jsx new file mode 100644 index 0000000..2c65904 --- /dev/null +++ b/src/Yammuna Express/auth.jsx @@ -0,0 +1,317 @@ +/* Authentication: Welcome, Login select, shared Verification (email+phone OTP) */ +(function () { + const { useState, useEffect, useRef } = React; + const Icon = window.Icon, Logo = window.Logo; + const D = window.YEIDA; + + /* ---- Brand panel (navy decorative side) ---- */ + function BrandPanel({ compact }) { + return ( +
+
+
+
+
+
+ Official Allottee Portal +
+

Your complete Yamuna Expressway property portal.

+

Verify your allotment, view official authority records, track construction and manage documents — securely.

+
+
+ {[['7+', 'Active Sectors'], ['62,000+', 'Allottees'], ['100%', 'Digital']].map((x, i) => ( +
+
{x[0]}
+
{x[1]}
+
+ ))} +
+
+ ); + } + + function AuthShell({ wide, children }) { + return ( +
+
+ +
{children}
+
+
+ ); + } + + /* ---------------- WELCOME ---------------- */ + function Welcome({ go }) { + return ( +
+
+
+
+
+ +
+ + +
+
+
+
+
+ Government of Uttar Pradesh · Official Portal +
+

Your Complete Yamuna Expressway Property Management Portal

+

Verify your allotment, view official authority payment records, track construction approvals and manage documents — all in one secure place.

+
+ + +
+
+ SSL Secured + OTP Verified Access + DigiLocker Ready +
+
+ +
+
+ ); + } + + function HeroCard() { + const plots = [[20, 18], [64, 18], [108, 18], [186, 18], [230, 18], [274, 18], [20, 116], [64, 116], [186, 116], [230, 116], [274, 116]]; + return ( +
+
+
+
+
+
+
+
Plot B-47, Sector 18
+
Residential · 300 sq.m
+
+
+ +
+
+ + + + + + + {plots.map((p, i) => )} + + + B-47 + +
+
+
+ ); + } + + /* ---------------- LOGIN SELECTION ---------------- */ + function LoginSelect({ go }) { + const [mode, setMode] = useState('allotment'); + const opts = [ + { k: 'allotment', icon: 'building', title: 'Allotment Number', desc: 'Login with your YEIDA allotment number' }, + { k: 'invite', icon: 'key', title: 'Invitation Code', desc: 'Access via authority-issued invite code' }, + ]; + return ( + +
+
Welcome back
+

Sign in to your portal

+

Verify yourself using your allotment number or an invitation code.

+
+ {opts.map(o => ( + + ))} +
+
+ +
+ + +
+ {mode === 'allotment' && } +
+ +
+ New allottee or representative?{' '} + +
+
+
+ ); + } + + /* ---------------- SHARED VERIFICATION (email + phone, same screen) ---------------- */ + // VerifyPanel renders both channels and reports completion via onChange(bothVerified) + function VerifyPanel({ email, phone, onChange }) { + const [cc, setCc] = useState(D.countryCodes[0]); + const [state, setState] = useState({ email: false, phone: false }); + return ( +
+ setState(s => { const n = { ...s, email: v }; onChange && onChange(n.email && n.phone); return n; })} /> + setState(s => { const n = { ...s, phone: v }; onChange && onChange(n.email && n.phone); return n; })} /> +
+ ); + } + + // channel that lifts verified state up to the panel + function VerifyChannelLifted({ kind, value, cc, setCc, onVerified }) { + const [sent, setSent] = useState(false); + const [otp, setOtp] = useState(['', '', '', '', '', '']); + const [verified, setVerified] = useState(false); + const [cooldown, setCooldown] = useState(0); + const [checking, setChecking] = useState(false); + const [api, setApi] = useState('idle'); // idle|checking|ok|invalid_email|mailbox_full|bounce_risk|invalid_mobile + const [otpErr, setOtpErr] = useState(false); + const [field, setField] = useState(value || ''); + const refs = useRef([]); + useEffect(() => { if (cooldown <= 0) return; const t = setInterval(() => setCooldown(s => s - 1), 1000); return () => clearInterval(t); }, [cooldown]); + useEffect(() => { onVerified && onVerified(verified); }, [verified]); + + const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field || ''); + const digits = (field || '').replace(/\D/g, '').length; + const phoneOk = digits >= (cc ? cc.digits : 10); + const valid = kind === 'email' ? emailOk : phoneOk; + + // simulate an email/phone validation API — keywords in the value trigger states (demo) + const runApi = () => { + setApi('checking'); setChecking(true); + setTimeout(() => { + setChecking(false); + const v = (field || '').toLowerCase(); + if (kind === 'email') { + if (!emailOk) return setApi('invalid_email'); + if (v.includes('full')) return setApi('mailbox_full'); + if (v.includes('bounce') || v.includes('temp')) return setApi('bounce_risk'); + setApi('ok'); setSent(true); setCooldown(60); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50); + } else { + if (!phoneOk) return setApi('invalid_mobile'); + setApi('ok'); setSent(true); setCooldown(60); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50); + } + }, 950); + }; + + const API_MSG = { + checking: { tone: 'blue', icon: 'clock', text: kind === 'email' ? 'Validating email via API…' : 'Validating mobile via API…' }, + ok: { tone: 'green', icon: 'checkCircle', text: kind === 'email' ? 'Email deliverable · OTP sent' : 'Mobile valid · OTP sent' }, + invalid_email: { tone: 'red', icon: 'alert', text: 'Invalid email address' }, + mailbox_full: { tone: 'amber', icon: 'alert', text: 'Mailbox full — try another email' }, + bounce_risk: { tone: 'amber', icon: 'alert', text: 'High bounce risk — verify spelling' }, + invalid_mobile: { tone: 'red', icon: 'alert', text: `Invalid mobile number for ${cc ? cc.label : 'IN'}` }, + }; + + const set = (i, v) => { + if (!/^\d?$/.test(v)) return; + const d = otp.slice(); d[i] = v; setOtp(d); setOtpErr(false); + if (v && i < 5) refs.current[i + 1] && refs.current[i + 1].focus(); + if (d.every(x => x !== '')) { + // demo: "000000" simulates an incorrect OTP + if (d.join('') === '000000') { setOtpErr(true); setOtp(['', '', '', '', '', '']); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50); } + else setVerified(true); + } + }; + const onKey = (i, e) => { if (e.key === 'Backspace' && !otp[i] && i > 0) refs.current[i - 1] && refs.current[i - 1].focus(); }; + + const apiMsg = API_MSG[api]; + const toneColor = { red: 'var(--red-600)', amber: 'var(--amber-600)', blue: 'var(--blue-600)', green: 'var(--emerald-700)' }; + const toneBg = { red: 'var(--red-50)', amber: 'var(--amber-50)', blue: 'var(--blue-50)', green: 'var(--emerald-50)' }; + + return ( +
+
+
+ +
+
{kind === 'email' ? 'Verify Email' : 'Verify Phone Number'}
+
{verified ? 'Verified successfully' : sent ? 'Enter the 6-digit OTP' : 'OTP verification required'}
+
+
+ {verified && Verified} +
+ {kind === 'email' ? ( +
{ setField(e.target.value); setApi('idle'); }} disabled={sent} />
+ ) : ( +
+ +
{ setField(e.target.value); setApi('idle'); }} disabled={sent} />
+
+ )} + + {/* API validation status banner */} + {apiMsg && ( +
+ {api === 'checking' ? : } + {apiMsg.text} +
+ )} + + {!sent ? ( + + ) : ( +
+
+ {otp.map((d, i) => ( + refs.current[i] = el} value={d} inputMode="numeric" maxLength={1} disabled={verified} + onChange={e => set(i, e.target.value)} onKeyDown={e => onKey(i, e)} + style={{ width: 44, height: 50, textAlign: 'center', fontSize: 20, fontWeight: 700, fontFamily: 'var(--font-display)', border: '1.5px solid ' + (otpErr ? 'var(--red-500)' : d ? (verified ? '#7fd3ad' : 'var(--blue-400)') : 'var(--line)'), borderRadius: 11, color: 'var(--ink)', outline: 'none', background: otpErr ? 'var(--red-50)' : d ? (verified ? '#fff' : 'var(--blue-50)') : '#fff' }} /> + ))} +
+ {otpErr &&
OTP incorrect — please try again
} + {!verified && ( +
+ Didn't get the code? +
+ {kind === 'email' && cooldown <= 0 && } + {cooldown > 0 + ? Resend in 0:{String(cooldown).padStart(2, '0')} + : } +
+
+ )} +
+ )} +
+ ); + } + + /* standalone Verify screen (login path) */ + function Verify({ go }) { + const [both, setBoth] = useState(false); + return ( + +
+ +
+

Verify it's you

+

Confirm both your email and mobile to securely access your records.

+ + +

Demo: enter any 6 digits to verify · type "000000" for an incorrect-OTP state · use an email with "full"/"bounce" to see API states

+
+
+ ); + } + + Object.assign(window, { Welcome, LoginSelect, Verify, AuthShell, VerifyPanel, BrandPanel }); + const s = document.createElement('style'); + s.textContent = '@keyframes ping{0%{transform:scale(.8);opacity:.6}100%{transform:scale(1.8);opacity:0}}'; + document.head.appendChild(s); +})(); diff --git a/src/Yammuna Express/check.png b/src/Yammuna Express/check.png new file mode 100644 index 0000000..a20690c Binary files /dev/null and b/src/Yammuna Express/check.png differ diff --git a/src/Yammuna Express/construction.jsx b/src/Yammuna Express/construction.jsx new file mode 100644 index 0000000..dcc316d --- /dev/null +++ b/src/Yammuna Express/construction.jsx @@ -0,0 +1,218 @@ +/* Construction module — 6-step stepper */ +(function () { + const { useState } = React; + const Icon = window.Icon, PageHead = window.PageHead; + const D = window.YEIDA; + + function Construction({ go, toast }) { + const steps = D.constructionSteps; + const [active, setActive] = useState(3); // Authority Review + const [docs, setDocs] = useState(D.requiredDocs); + + const upload = (i) => { setDocs(d => d.map((x, j) => j === i ? { ...x, status: 'uploaded' } : x)); toast && toast('Document uploaded for scrutiny'); }; + + return ( +
+ + + + + {/* Stepper rail */} +
+
+ {steps.map((s, i) => { + const state = s.done ? 'done' : i === active ? 'active' : 'todo'; + const cc = state === 'done' ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : 'var(--line)'; + return ( +
+ {i < steps.length - 1 &&
} + +
+ ); + })} +
+
+ + {/* Step content */} +
+ {active === 0 && } + {active === 1 && } + {active === 2 && } + {active === 3 && } + {active === 4 && } + {active === 5 && } +
+ + {/* nav */} +
+ + +
+
+ ); + } + + function Card({ children, ...p }) { return
{children}
; } + + function Overview({ go }) { + return ( +
+ + Step 1 · Overview +

Start construction on Plot B-47

+

Before beginning construction, your building plan must be sanctioned by YEIDA in accordance with the prevailing building byelaws, FAR and setback norms for {D.user.sector}.

+
+ {[['Max Ground Coverage', '50%'], ['Permissible FAR', '1.5'], ['Max Height', '15 m'], ['Front Setback', '3 m']].map(([l, v]) => ( +
+
{l}
+
{v}
+
+ ))} +
+
+ +

Eligibility

+ {[['Possession granted', true], ['100% premium not mandatory', true], ['No pending dues for plan', false], ['Architect empanelled', true]].map(([l, ok], i) => ( +
+ + {l} +
+ ))} +
+ + Clear the Water Connection due of ₹12,400 before plan submission. +
+
+
+ ); + } + + function Required({ docs }) { + return ( + + Step 2 · Required Documents +

Document checklist

+

Prepare the following before submitting your building plan for scrutiny.

+
+ {docs.map((d, i) => ( +
+ +
+
{d.name}
+
{d.note}
+
+ {d.status === 'uploaded' ? 'Ready' : 'Pending'} +
+ ))} +
+
+ ); + } + + function Upload({ docs, upload }) { + return ( +
+ + Step 3 · Upload +

Upload documents

+
+ +
Drag & drop files here
+
PDF, DWG, JPG · up to 25 MB each
+ +
+
+ +

Submission status

+
+ {docs.map((d, i) => ( +
+
+ +
{d.name}
{d.note}
+
+ {d.status === 'uploaded' + ? Uploaded + : } +
+ ))} +
+
+
+ ); + } + + function Review({ setActive }) { + return ( + +
+ + +
+ Step 4 · Authority Review +

Your application is under scrutiny

+

YEIDA's Building Plan section is reviewing your submission against byelaws. You'll be notified once the sanction letter is issued.

+
+ {[['Submitted', '16 Feb 2026'], ['Application No.', 'BP-2026-18-0472'], ['Est. Decision', '12 Jun 2026'], ['Reviewing Officer', 'AE (Planning)']].map(([l, v]) => ( +
{l}
{v}
+ ))} +
+
+
Document CheckTechnical ScrutinySanction
+
+
+
+ ); + } + + function Approval() { + return ( + +
+ Step 5 · Approval +

Sanction letter ready

+

Once approved, your building plan sanction letter will be available to download here, valid for 3 years from the date of issue.

+
+ + +
+
+ ); + } + + function Progress() { + return ( + +
+
Step 6 · Progress Tracker

On-site construction progress

+
41%
overall complete
+
+
+ {D.constructionMilestones.map((m, i) => ( +
+
+
+ 0 ? 'var(--blue-50)' : 'var(--bg-2)', color: m.pct === 100 ? 'var(--emerald-600)' : m.pct > 0 ? 'var(--blue-600)' : 'var(--faint)' }}> +
{m.label}
{m.date}
+
+
+ 0 ? 'blue' : 'gray')} style={{ fontSize: 11 }}>{m.status} + {m.pct}% +
+
+
+
+ ))} +
+
+ ); + } + + window.Construction = Construction; +})(); diff --git a/src/Yammuna Express/dashboard.jsx b/src/Yammuna Express/dashboard.jsx new file mode 100644 index 0000000..1c65e81 --- /dev/null +++ b/src/Yammuna Express/dashboard.jsx @@ -0,0 +1,404 @@ +/* Dashboard — split into A) Authority Status & Records and B) Construction Progress */ +(function () { + const Icon = window.Icon, Donut = window.Donut, StatusBadge = window.StatusBadge; + const D = window.YEIDA, fmt = D.fmtINR; + + function SectionLabel({ n, title, sub, children }) { + return ( +
+
+
Module {n}
+

{title}

+ {sub &&
{sub}
} +
+ {children} +
+ ); + } + + function StatusCard({ icon, title, code, date, color }) { + const s = D.STATUS_CODES[code] || {}; + const tone = s.tone || 'gray'; + const tc = { green: ['var(--emerald-50)', 'var(--emerald-700)', 'var(--emerald-500)'], amber: ['var(--amber-50)', 'var(--amber-600)', 'var(--amber-500)'], blue: ['var(--blue-50)', 'var(--blue-600)', 'var(--blue-500)'], gray: ['var(--bg-2)', 'var(--muted)', '#c2cedd'] }[tone]; + return ( +
+
+ + {tone === 'green' ? : tone === 'amber' ? : } +
+
+
{title}
+
{s.label}
+
{date}
+
+
+ ); + } + + function Dashboard({ go }) { + const [view, setView] = React.useState(() => localStorage.getItem('yeida_dash') || 'plot'); + const A = D.authorityStatus; + const switchTo = (v) => { setView(v); localStorage.setItem('yeida_dash', v); }; + + return ( +
+ {/* Welcome banner */} +
+
+ {/* decorative concentric rings */} + +
+
+
Official Allottee Portal
+

Welcome back, {D.user.firstName}

+
+ Plot {D.user.plot} · {D.user.sector} + {D.user.relationship} · {D.user.isAllottee ? 'Allottee' : 'Representative'} + {A.leaseDeed.label} + {A.atl.label} +
+
+
+ + +
+
+
+ + {/* ===== Module switcher: Plot Dashboard (primary) · Construction Project Dashboard (secondary) ===== */} +
+ {[ + { k: 'plot', icon: 'building', title: 'Yamuna Expressway · Plot Dashboard', sub: 'Authority status, records & payments', tag: 'Primary' }, + { k: 'construction', icon: 'construction', title: 'Construction Project Dashboard', sub: 'Build progress, milestones & contractor', tag: D.journey.constructionStarted ? 'Active' : 'Not started' }, + ].map(m => { + const on = view === m.k; + return ( + + ); + })} +
+ + {view === 'plot' ? : } +
+ ); + } + + /* ============================ MODULE 1 — PLOT DASHBOARD (PRIMARY) ============================ */ + function PlotDashboard({ go }) { + const totalDue = D.dues.reduce((s, d) => s + d.amount, 0); + const paidPct = Math.round(D.property.paid / D.property.totalValue * 100); + const A = D.authorityStatus; + return ( +
+ {/* Pending actions strip */} +
+ +
+
3 pending actions need your attention
+
ATL pending · 1 overdue payment · Soil Test Report awaited
+
+
+ + +
+
+ + + + + +
+ + + + +
+ + {/* Authority Status — key metrics */} + {(() => { + const last = D.authorityRecords[D.authorityRecords.length - 1] || D.authorityRecords[0]; + const nextDue = [...D.dues].sort((a, b) => a.due.localeCompare(b.due))[0] || D.dues[0]; + const metrics = [ + { label: 'Total Paid', value: fmt(D.property.paid), sub: D.authorityRecords.length + ' receipts', icon: 'checkCircle', color: 'emerald' }, + { label: 'Outstanding Dues', value: fmt(totalDue), sub: D.dues.length + ' pending', icon: 'rupee', color: 'amber' }, + { label: 'Last Payment', value: fmt(last.amount), sub: last.date, icon: 'history', color: 'blue' }, + { label: 'Next Due', value: nextDue.due, sub: fmt(nextDue.amount) + ' · ' + nextDue.type, icon: 'calendar', color: 'navy' }, + ]; + const C = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }; + return ( +
+ {metrics.map(m => { + const cc = C[m.color]; + return ( +
+ +
+
{m.label}
+
{m.value}
+
{m.sub}
+
+
+ ); + })} +
+ ); + })()} + +
+ {/* Authority payments summary */} +
+
+
Authority Payments

Premium & lease account

+ +
+
+
Total Premium
{fmt(D.property.totalValue)}
+
Deposited
{fmt(D.property.paid)}
+
Balance
{fmt(D.property.totalValue - D.property.paid)}
+
+
+
{paidPct}% of premium deposited{D.authorityRecords.length} official receipts
+
+ + {/* Pending dues + due dates */} +
+
+

Pending Dues & Due Dates

+ {D.dues.length} pending +
+
+ {D.dues.map(d => ( +
+
+
{d.label}
+
+ {d.status === 'overdue' ? 'Overdue' : d.status === 'due' ? 'Due soon' : 'Upcoming'} + {d.due} +
+
+
{fmt(d.amount)}
+
+ ))} +
+ +
+
+ + {/* Official records table */} +
+
+

Official Authority Records

Verified deposits & challans on record with YEIDA

+ +
+ + + + {D.authorityRecords.map(r => ( + + + + + + + + + ))} + +
Head of AccountChallan No.DateModeStatusAmount
{r.head}{r.challan}{r.date}{r.mode}{r.status}{fmt(r.amount)}
+
+ + {/* Latest Notices · AI Assistant · Recent Documents */} +
+
+
+

Latest Notices

+ +
+
+ {D.notices.slice(0, 3).map(n => ( + + ))} +
+
+ +
+
+ +

AI Assistant

Online · ask anything
+
+
+ {['What payments are pending?', 'What is my ATL status?'].map(q => ( + + ))} +
+ +
+ +
+
+

Recent Documents

+ +
+
+ {D.documents.slice(0, 4).map((d, i) => ( + + ))} +
+
+
+
+ ); + } + + /* ============================ MODULE 2 — CONSTRUCTION PROJECT DASHBOARD (SECONDARY) ============================ */ + function ConstructionDashboard({ go }) { + const J = D.journey; + return ( +
+ + + + + {/* construction financial / site widgets */} +
+
+
+ + {Math.round(D.construction.spent / D.construction.estimatedCost * 100)}% spent +
+
Estimated Build Cost
+
{fmt(D.construction.estimatedCost)}
+
+
{fmt(D.construction.spent)} spent to date
+
+
+
+ + {D.construction.contractor.status} +
+
Contractor Status
+
{D.construction.contractor.name}
+
+ {D.construction.contractor.rating} + Engaged since {D.construction.contractor.since} +
+
+
+
+ + {D.construction.onSchedule ? 'On schedule' : 'Delayed'} +
+
Completion Forecast
+
{D.construction.completionForecast}
+
Current: {D.construction.currentMilestone}
+
+
+ +
+ {/* status + progress */} +
+
+ {J.constructionStarted ? 'Construction Started' : 'Not Started'} +
+
+
+
Current Stage
+
{J.stage}
+
+
+
Next Required Action
+
+ {J.nextAction} + +
+
+
+ + {/* milestones */} +
+

Milestones

+
+ {D.constructionMilestones.map((m, i) => ( +
+
+ {m.label} + 0 ? 'blue' : 'gray')} style={{ fontSize: 10.5 }}>{m.status} +
+
+
{m.date}
+
+ ))} +
+
+ + {/* timeline */} +
+

Activity Timeline

+
+ {D.activity.slice(0, 4).map((a, i, arr) => { + const cc = { emerald: 'var(--emerald-500)', blue: 'var(--blue-500)', amber: 'var(--amber-500)', navy: 'var(--navy-700)' }[a.color]; + return ( +
+ {i !== arr.length - 1 && } + +
+
{a.title}
+
{a.desc}
+
{a.time}
+
+
+ ); + })} +
+
+
+ + {/* construction payments shortcut + sample plans CTA */} +
+
+
+ +
Construction Payments
Milestone invoices & receipts
+
+ +
+
+
+ +
Sample Plans & Tours
Designs matched to your plot
+
+ +
+
+
+ ); + } + + window.Dashboard = Dashboard; +})(); diff --git a/src/Yammuna Express/data.js b/src/Yammuna Express/data.js new file mode 100644 index 0000000..ab2e4d7 --- /dev/null +++ b/src/Yammuna Express/data.js @@ -0,0 +1,304 @@ +/* Mock data for the YEIDA portal — realistic Yamuna Expressway style. */ +window.YEIDA = (function () { + const fmtINR = (n) => '₹' + n.toLocaleString('en-IN'); + const user = { + name: 'Rajeev Malhotra', + firstName: 'Rajeev', + lastName: 'Malhotra', + initials: 'RM', + allotment: 'YEA-654321', + sector: 'Sector 18', + pocket: 'Pocket B', + plot: 'B-47', + category: 'Residential', + size: '300 sq.m', + mobile: '+91 98xxxxxx12', + email: 'r.malhotra@email.com', + memberSince: 'Mar 2019', + relationship: 'Self', // Self | Son | Daughter | Spouse | Relative | Authorized Representative + isAllottee: true, // true = self registration (Allottee = User) + allotteeFirstName: 'Rajeev', // original allottee (may differ from registrant) + allotteeLastName: 'Malhotra', + registeredAddress: { + line1: 'Plot B-47, Pocket B, Sector 18', + line2: 'Yamuna Expressway, Gautam Buddh Nagar', + city: 'Greater Noida', state: 'Uttar Pradesh', pin: '203201', + }, + mailingAddress: { + line1: 'C-204, Orchid Residency, Sector 50', + line2: 'Near City Centre', + city: 'Noida', state: 'Uttar Pradesh', pin: '201301', + }, + }; + + // Real authority statuses (NOT a generic "Verified") — driven by backend status codes + const authorityStatus = { + leaseDeed: { code: 'LD_COMPLETED', label: 'Lease Deed Completed', state: 'done', date: '05 Sep 2019' }, + atl: { code: 'ATL_PENDING', label: 'ATL Pending', state: 'pending', date: 'Action required' }, + verification: { code: 'VERIFIED_AUTH', label: 'Authority Verified', state: 'done', date: '12 Mar 2019' }, + possession: { code: 'POSS_GRANTED', label: 'Possession Granted', state: 'done', date: '22 Aug 2019' }, + }; + + // status-code → human label + colour (single source of truth for badges) + const STATUS_CODES = { + LD_COMPLETED: { label: 'Lease Deed Completed', tone: 'green', icon: 'file' }, + LD_PENDING: { label: 'Lease Deed Pending', tone: 'amber', icon: 'file' }, + ATL_COMPLETED: { label: 'ATL Completed', tone: 'green', icon: 'checkCircle' }, + ATL_PENDING: { label: 'ATL Pending', tone: 'amber', icon: 'clock' }, + VERIFICATION_REVIEW: { label: 'Verification Under Review', tone: 'blue', icon: 'clock' }, + VERIFIED_AUTH: { label: 'Authority Verified', tone: 'green', icon: 'shield' }, + POSS_GRANTED: { label: 'Possession Granted', tone: 'green', icon: 'home' }, + }; + + const property = { + allotmentNo: 'YEA-654321', + sector: 'Sector 18', + plot: 'B-47', + block: 'Block B', + size: '300 sq.m', + category: 'Residential', + facing: 'East', + road: '12 m road', + allottedOn: '14 Mar 2019', + possession: '22 Aug 2019', + leasePeriod: '90 years', + rate: '₹25,750 / sq.m', + totalValue: 7725000, + paid: 6952500, + coords: { col: 3, row: 2 }, + }; + + const dues = [ + { id: 'D-2026-Q1', label: 'Annual Lease Rent 2026-27', due: '30 Jun 2026', amount: 38625, status: 'due', type: 'Lease Rent' }, + { id: 'D-2026-LP', label: 'Location Premium Instalment 14/16', due: '15 Jul 2026', amount: 96560, status: 'upcoming', type: 'Premium' }, + { id: 'D-2026-WC', label: 'Water Connection Charges', due: '10 Jun 2026', amount: 12400, status: 'overdue', type: 'Utility' }, + ]; + + const payments = [ + { id: 'TXN-849213', date: '12 Mar 2026', label: 'Location Premium Instalment 13/16', mode: 'Net Banking', amount: 96560, status: 'Success', challan: 'CH-2026-0312-18' }, + { id: 'TXN-831077', date: '28 Jan 2026', label: 'Annual Lease Rent 2025-26', mode: 'UPI', amount: 36750, status: 'Success', challan: 'CH-2026-0128-04' }, + { id: 'TXN-802461', date: '15 Nov 2025', label: 'Building Plan Scrutiny Fee', mode: 'Debit Card', amount: 8500, status: 'Success', challan: 'CH-2025-1115-77' }, + { id: 'TXN-778190', date: '12 Sep 2025', label: 'Location Premium Instalment 12/16', mode: 'Net Banking', amount: 96560, status: 'Success', challan: 'CH-2025-0912-21' }, + { id: 'TXN-751002', date: '30 Jun 2025', label: 'Annual Lease Rent 2024-25', mode: 'UPI', amount: 35000, status: 'Success', challan: 'CH-2025-0630-09' }, + ]; + + const constructionSteps = [ + { key: 'overview', title: 'Overview', desc: 'Project & eligibility', done: true }, + { key: 'required', title: 'Required Documents', desc: 'Checklist', done: true }, + { key: 'upload', title: 'Upload Documents', desc: 'Building plans & approvals', done: true }, + { key: 'review', title: 'Authority Review', desc: 'YEIDA scrutiny', done: false, active: true }, + { key: 'approval', title: 'Approval', desc: 'Sanction letter', done: false }, + { key: 'progress', title: 'Progress Tracker', desc: 'On-site milestones', done: false }, + ]; + + const requiredDocs = [ + { name: 'Approved Building Plan', note: 'Architect-stamped, 4 sets', status: 'uploaded' }, + { name: 'Structural Stability Certificate', note: 'By registered structural engineer', status: 'uploaded' }, + { name: 'Architect Registration (COA)', note: 'Valid council certificate', status: 'uploaded' }, + { name: 'Soil Test Report', note: 'Geotechnical investigation', status: 'pending' }, + { name: 'Indemnity Bond', note: '₹100 stamp paper, notarised', status: 'uploaded' }, + { name: 'Possession Certificate Copy', note: 'Self-attested', status: 'uploaded' }, + ]; + + const constructionMilestones = [ + { label: 'Foundation & Plinth', pct: 100, status: 'Completed', date: 'Done · Apr 2026' }, + { label: 'Ground Floor Structure', pct: 65, status: 'In Progress', date: 'Est. Aug 2026' }, + { label: 'First Floor Structure', pct: 0, status: 'Not Started', date: 'Est. Nov 2026' }, + { label: 'Finishing & Handover', pct: 0, status: 'Not Started', date: 'Est. Mar 2027' }, + ]; + + const documents = [ + { name: 'Allotment Letter', type: 'Allotment', date: '14 Mar 2019', size: '1.2 MB', tag: 'blue' }, + { name: 'Possession Letter', type: 'Possession', date: '22 Aug 2019', size: '0.9 MB', tag: 'green' }, + { name: 'Lease Deed (Registered)', type: 'Lease Deed', date: '05 Sep 2019', size: '3.4 MB', tag: 'navy' }, + { name: 'Building Plan Sanction', type: 'Building Approvals', date: '18 Feb 2026', size: '5.1 MB', tag: 'amber' }, + { name: 'Fire NOC', type: 'NOCs', date: '02 Mar 2026', size: '0.6 MB', tag: 'green' }, + { name: 'Sector 18 Master Plan', type: 'Master Plans', date: '11 Jan 2024', size: '8.7 MB', tag: 'navy' }, + { name: 'Circular: Transfer Policy 2026', type: 'Circulars', date: '20 Apr 2026', size: '0.4 MB', tag: 'blue' }, + { name: 'Govt. Order — Lease Rent Revision', type: 'Government Orders', date: '01 Apr 2026', size: '0.3 MB', tag: 'amber' }, + ]; + + const notices = [ + { id: 1, title: 'Revised Annual Lease Rent rates effective FY 2026-27', cat: 'Policy Update', date: '01 Jun 2026', pinned: true, body: 'The Authority has revised annual lease rent rates for all residential and institutional allottees with effect from 1 April 2026. Allottees are advised to clear outstanding dues at the revised rate before 30 June 2026 to avoid interest.', attachments: ['GO-LeaseRent-2026.pdf'] }, + { id: 2, title: 'Construction guidelines update — setback & FAR norms', cat: 'Construction Guidelines', date: '24 May 2026', pinned: true, body: 'Updated Floor Area Ratio (FAR) and setback norms apply to building plans submitted after 1 May 2026. Refer to the attached circular for sector-wise ground coverage limits.', attachments: ['FAR-Norms-2026.pdf', 'Setback-Chart.pdf'] }, + { id: 3, title: 'Water supply augmentation work in Sector 18 & 20', cat: 'Sector Update', date: '18 May 2026', pinned: false, body: 'Scheduled maintenance and pipeline augmentation will be carried out from 25–28 May. Intermittent supply disruption is expected.', attachments: [] }, + { id: 4, title: 'Online possession application window now open', cat: 'Announcement', date: '10 May 2026', pinned: false, body: 'Allottees who have completed payment of 100% premium can now apply for possession online through the portal.', attachments: ['Possession-SOP.pdf'] }, + { id: 5, title: 'Authority Circular: e-Challan mandatory for all dues', cat: 'Authority Circular', date: '02 May 2026', pinned: false, body: 'With effect from 1 June 2026, all payments must be made via e-Challan generated through the portal. Offline challans will be discontinued.', attachments: [] }, + ]; + + const activity = [ + { icon: 'payments', color: 'emerald', title: 'Payment received', desc: 'Location Premium Instalment 13/16 — ₹96,560', time: '12 Mar 2026' }, + { icon: 'upload', color: 'blue', title: 'Documents uploaded', desc: 'Building plan & structural certificate', time: '15 Feb 2026' }, + { icon: 'construction', color: 'amber', title: 'Construction application submitted', desc: 'Under authority review', time: '16 Feb 2026' }, + { icon: 'check', color: 'emerald', title: 'Fire NOC approved', desc: 'Valid till Mar 2031', time: '02 Mar 2026' }, + { icon: 'notices', color: 'navy', title: 'New notice published', desc: 'Revised lease rent rates FY 2026-27', time: '01 Jun 2026' }, + ]; + + const sectors = [ + { id: '18', name: 'Sector 18', type: 'Residential', plots: 1240, status: 'Developed', mine: true }, + { id: '20', name: 'Sector 20', type: 'Residential', plots: 1810, status: 'Developed', mine: false }, + { id: '22D', name: 'Sector 22D', type: 'Institutional', plots: 420, status: 'Developing', mine: false }, + { id: '24A', name: 'Sector 24A', type: 'Industrial', plots: 690, status: 'Developing', mine: false }, + { id: '25', name: 'Sector 25', type: 'Mixed Use', plots: 980, status: 'Developed', mine: false }, + { id: '28', name: 'Sector 28', type: 'Residential', plots: 1530, status: 'Planned', mine: false }, + { id: '29', name: 'Sector 29', type: 'Commercial', plots: 350, status: 'Planned', mine: false }, + ]; + + const transferFees = [ + { label: 'Transfer Charges (2.5% of value)', amount: 193125 }, + { label: 'Processing Fee', amount: 5000 }, + { label: 'Documentation Charges', amount: 2500 }, + { label: 'GST (18% on fees)', amount: 1350 }, + ]; + + const assistantSuggestions = [ + 'How do I start construction?', + 'What documents are required?', + 'Show latest notices', + 'How can I transfer my plot?', + 'What payments are pending?', + 'Show Sector 18 map', + ]; + + const tickets = [ + { id: 'TKT-4821', subject: 'Lease deed correction request', status: 'Open', priority: 'High', updated: '2h ago' }, + { id: 'TKT-4790', subject: 'Challan not reflecting after UPI payment', status: 'Resolved', priority: 'Medium', updated: '3d ago' }, + { id: 'TKT-4755', subject: 'Building plan re-submission query', status: 'In Progress', priority: 'Medium', updated: '5d ago' }, + ]; + + // Official authority deposit/challan records (read-only, from authority backend) — + // shown immediately after login to build trust + const authorityRecords = [ + { id: 'AR-2019-001', date: '14 Mar 2019', head: 'Registration / Earnest Money', challan: 'CH-2019-0314-01', amount: 772500, mode: 'Demand Draft', status: 'Deposited' }, + { id: 'AR-2019-014', date: '20 Aug 2019', head: 'Allotment Premium (Lump-sum 25%)', challan: 'CH-2019-0820-14', amount: 1158750, mode: 'RTGS', status: 'Deposited' }, + { id: 'AR-2021-077', date: '12 Jul 2021', head: 'Location Premium Instalment 6/16', challan: 'CH-2021-0712-77', amount: 96560, mode: 'Net Banking', status: 'Deposited' }, + { id: 'AR-2024-052', date: '30 Jun 2024', head: 'Annual Lease Rent 2023-24', challan: 'CH-2024-0630-52', amount: 33500, mode: 'UPI', status: 'Deposited' }, + { id: 'AR-2025-091', date: '12 Sep 2025', head: 'Location Premium Instalment 12/16', challan: 'CH-2025-0912-21', amount: 96560, mode: 'Net Banking', status: 'Deposited' }, + ]; + + // Construction journey state — drives the first-time "what next?" experience + const journey = { + constructionStarted: true, // toggleable in the demo via the journey screen + stage: 'Authority Review', + nextAction: 'Upload pending Soil Test Report', + progressPct: 41, + }; + + // Architectural sample plans (pre-construction guidance) + const samplePlans = [ + { id: 'SP-300-E', name: 'Aangan — Courtyard Villa', size: '300 sq.m', facing: 'East', floors: 'G+1', beds: 4, style: 'Contemporary', tour: true, accent: '#1d5fbf' }, + { id: 'SP-300-N', name: 'Surya — Sunlit Modern', size: '300 sq.m', facing: 'North', floors: 'G+2', beds: 5, style: 'Modern', tour: true, accent: '#0f9d6b' }, + { id: 'SP-200-E', name: 'Nivaas — Compact Smart', size: '200 sq.m', facing: 'East', floors: 'G+1', beds: 3, style: 'Minimal', tour: false, accent: '#c77a08' }, + { id: 'SP-450-S', name: 'Vista — Luxury Duplex', size: '450 sq.m', facing: 'South', floors: 'G+2', beds: 6, style: 'Luxury', tour: true, accent: '#103263' }, + { id: 'SP-200-W', name: 'Saral — Efficient Living', size: '200 sq.m', facing: 'West', floors: 'G+1', beds: 3, style: 'Contemporary', tour: false, accent: '#7a3fbf' }, + { id: 'SP-300-S', name: 'Prakriti — Green Home', size: '300 sq.m', facing: 'South', floors: 'G+1', beds: 4, style: 'Eco / Sustainable', tour: true, accent: '#0a7d56' }, + ]; + + // Construction financials & site (Section B dashboard widgets) + const construction = { + estimatedCost: 9800000, + spent: 4018000, + contractor: { name: 'Shreeji Constructions Pvt. Ltd.', status: 'On Site', rating: 4.6, since: 'Feb 2026' }, + completionForecast: 'Mar 2027', + onSchedule: true, + currentMilestone: 'Ground Floor Structure', + siteUpdates: [ + { title: 'RCC slab casting completed', date: '04 Jun 2026', icon: 'check', color: 'emerald' }, + { title: 'Steel reinforcement inspection passed', date: '28 May 2026', icon: 'shield', color: 'blue' }, + { title: 'Brickwork — ground floor started', date: '20 May 2026', icon: 'construction', color: 'amber' }, + ], + }; + + // Construction-side payments (milestones / invoices / receipts) + const constructionPayments = [ + { id: 'CINV-014', milestone: 'Ground Floor Structure (40%)', vendor: 'Shreeji Constructions', date: '02 Jun 2026', amount: 1568000, status: 'Paid' }, + { id: 'CINV-009', milestone: 'Foundation & Plinth (25%)', vendor: 'Shreeji Constructions', date: '14 Apr 2026', amount: 2450000, status: 'Paid' }, + { id: 'CINV-018', milestone: 'First Floor Structure (20%)', vendor: 'Shreeji Constructions', date: 'Due 15 Aug 2026', amount: 1960000, status: 'Upcoming' }, + { id: 'CINV-006', milestone: 'Architect & Design Fees', vendor: 'Studio Aangan', date: '10 Feb 2026', amount: 320000, status: 'Paid' }, + ]; + + // Onboarding decision — 7 action cards + const onboardingActions = [ + { id: 'plot', icon: 'plot', title: 'View My Plot Status', desc: 'Authority records, lease deed & status', route: 'plot', color: 'blue' }, + { id: 'paperwork', icon: 'documents', title: 'Complete Authority Paperwork', desc: 'ATL, lease deed & pending formalities', route: 'documents', color: 'navy' }, + { id: 'construction', icon: 'construction', title: 'Start Construction', desc: 'Plans, approvals & build tracking', route: 'construction', color: 'amber' }, + { id: 'plans', icon: 'grid', title: 'View Sample Plans', desc: 'Architect designs for your plot', route: 'sampleplans', color: 'emerald' }, + { id: 'tours', icon: 'eye', title: 'Explore Virtual Tours', desc: '3D walkthroughs of ready designs', route: 'sampleplans', color: 'blue' }, + { id: 'sell', icon: 'transfer', title: 'Sell My Plot', desc: 'Eligibility, fees & transfer application', route: 'transfer', color: 'navy' }, + { id: 'expert', icon: 'support', title: 'Contact Expert', desc: 'Talk to a YEIDA support officer', route: 'support', color: 'emerald' }, + ]; + + const relationshipOptions = ['Self', 'Son', 'Daughter', 'Spouse', 'Relative', 'Authorized Representative']; + + // ---- Construction Journey (8-step survey workflow) ---- + const journeyGoals = [ + { id: 'start', icon: 'construction', title: 'Start construction soon', desc: 'Begin your build journey with plans & approvals' }, + { id: 'completion', icon: 'checkCircle', title: 'Obtain a completion certificate', desc: 'Finalise an existing or finished build' }, + { id: 'full', icon: 'building', title: 'Complete full construction', desc: 'End-to-end managed construction' }, + { id: 'consult', icon: 'support', title: 'Need construction consultation', desc: 'Talk to an expert before deciding' }, + { id: 'explore', icon: 'grid', title: 'Explore design options', desc: 'Browse plans & 3D walkthroughs first' }, + ]; + + const journeySteps = [ + { key: 'goal', title: 'Goal Selection', desc: 'What you want to achieve', icon: 'flag', + summary: 'Start construction soon', done: true }, + { key: 'plot', title: 'Plot Verification', desc: 'Confirm your allotment & plot', icon: 'building', + summary: 'YEA-654321 · Plot B-47, Sector 18', done: true }, + { key: 'design', title: 'Design Selection', desc: 'Choose a house design', icon: 'grid', + summary: 'Aangan — Courtyard Villa (G+1)', done: true }, + { key: 'planning', title: 'Construction Planning', desc: 'Scope, timeline & permissions', icon: 'calendar', + summary: 'G+1 · est. 11 months · plan sanctioned', done: true }, + { key: 'budget', title: 'Budget & Payment Setup', desc: 'Cost plan & milestones', icon: 'rupee', + summary: '₹98,00,000 budget · 5 milestones', done: false, active: true }, + { key: 'contractor', title: 'Contractor Assignment', desc: 'Assign your project team', icon: 'users', + summary: 'Pending assignment', done: false }, + { key: 'progress', title: 'Construction Progress', desc: 'Track on-site milestones', icon: 'trend', + summary: 'Not started', done: false }, + { key: 'certificate', title: 'Completion Certificate', desc: 'Final inspection & handover', icon: 'checkCircle', + summary: 'Not started', done: false }, + ]; + + const projectTeam = [ + { role: 'Project Manager', name: 'Anil Verma', org: 'YEIDA PMC', phone: '+91 98xxxxxx01', avatar: 'AV', online: true }, + { role: 'Site Engineer', name: 'Priya Nair', org: 'Shreeji Constructions', phone: '+91 98xxxxxx02', avatar: 'PN', online: true }, + { role: 'Architect', name: 'Studio Aangan', org: 'Lead: R. Mehta', phone: '+91 98xxxxxx03', avatar: 'SA', online: false }, + { role: 'Construction Team', name: '12-member crew', org: 'Shreeji Constructions', phone: '+91 98xxxxxx04', avatar: 'CT', online: true }, + ]; + + const teamUpdates = [ + { who: 'Priya Nair', text: 'RCC slab casting for ground floor completed and cured.', time: '2h ago', avatar: 'PN' }, + { who: 'Anil Verma', text: 'First-floor material procurement scheduled for next week.', time: 'Yesterday', avatar: 'AV' }, + { who: 'Studio Aangan', text: 'Revised electrical layout shared for approval.', time: '3 days ago', avatar: 'SA' }, + ]; + + // Project Progress Center — visual construction stages + const progressStages = [ + { name: 'Site Cleaning', pct: 100, date: '02 Mar 2026', note: 'Site cleared & leveled', photos: 4 }, + { name: 'Excavation', pct: 100, date: '14 Mar 2026', note: 'Foundation trench dug', photos: 6 }, + { name: 'Foundation', pct: 100, date: '04 Apr 2026', note: 'PCC & footing laid', photos: 8 }, + { name: 'Boundary Wall', pct: 100, date: '20 Apr 2026', note: 'Perimeter wall complete', photos: 3 }, + { name: 'Columns', pct: 100, date: '06 May 2026', note: 'GF columns cast', photos: 5 }, + { name: 'Slab Casting', pct: 65, date: 'In progress', note: 'GF roof slab — curing', photos: 7 }, + { name: 'Brick Work', pct: 20, date: 'Started', note: 'GF walls begun', photos: 2 }, + { name: 'Electrical', pct: 0, date: 'Upcoming', note: '', photos: 0 }, + { name: 'Plumbing', pct: 0, date: 'Upcoming', note: '', photos: 0 }, + { name: 'Finishing', pct: 0, date: 'Upcoming', note: '', photos: 0 }, + { name: 'Completion', pct: 0, date: 'Est. Mar 2027', note: '', photos: 0 }, + ]; + const countryCodes = [ + { code: '+91', label: 'IN', digits: 10 }, + { code: '+971', label: 'AE', digits: 9 }, + { code: '+1', label: 'US', digits: 10 }, + { code: '+44', label: 'UK', digits: 10 }, + { code: '+65', label: 'SG', digits: 8 }, + ]; + + return { + fmtINR, user, authorityStatus, STATUS_CODES, authorityRecords, journey, samplePlans, + relationshipOptions, countryCodes, construction, constructionPayments, onboardingActions, + journeyGoals, journeySteps, projectTeam, teamUpdates, progressStages, + property, dues, payments, constructionSteps, requiredDocs, + constructionMilestones, documents, notices, activity, sectors, transferFees, + assistantSuggestions, tickets, + }; +})(); diff --git a/src/Yammuna Express/documents.jsx b/src/Yammuna Express/documents.jsx new file mode 100644 index 0000000..280c1b2 --- /dev/null +++ b/src/Yammuna Express/documents.jsx @@ -0,0 +1,116 @@ +/* Document Center */ +(function () { + const { useState } = React; + const Icon = window.Icon, PageHead = window.PageHead; + const D = window.YEIDA; + + const CATS = ['All', 'Allotment', 'Possession', 'Lease Deed', 'Building Approvals', 'NOCs', 'Circulars', 'Master Plans', 'Government Orders']; + + function Documents({ toast }) { + const [cat, setCat] = useState('All'); + const [q, setQ] = useState(''); + const [view, setView] = useState('grid'); + const [bookmarks, setBookmarks] = useState({}); + const [preview, setPreview] = useState(null); + const tagColor = { blue: ['var(--blue-50)', 'var(--blue-600)'], green: ['var(--emerald-50)', 'var(--emerald-700)'], navy: ['#e9eefb', 'var(--navy-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'] }; + + const list = D.documents.filter(d => (cat === 'All' || d.type === cat) && d.name.toLowerCase().includes(q.toLowerCase())); + + return ( +
+ +
+ {[['grid', 'grid'], ['list', 'menu']].map(([k, ic]) => ( + + ))} +
+
+ + {/* search + filters */} +
+
+
+ + setQ(e.target.value)} /> +
+ +
+
+ {CATS.map(c => )} +
+
+ + {view === 'grid' ? ( +
+ {list.map((d, i) => { + const tc = tagColor[d.tag]; + return ( +
+ + +
{d.name}
+
{d.type}
+
{d.date}{d.size}
+
+ + +
+
+ ); + })} +
+ ) : ( +
+ + + + {list.map((d, i) => { + const tc = tagColor[d.tag]; + return ( + + + + + + ); + })} + +
DocumentCategoryDateSize
{d.name}
{d.type}{d.date}{d.size}
+
+ )} + + {preview && setPreview(null)} toast={toast} />} +
+ ); + } + + function PreviewModal({ doc, onClose, toast }) { + return ( +
+
e.stopPropagation()} className="anim-pop card" style={{ width: 720, maxWidth: '100%', maxHeight: '88vh', padding: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}> +
+
{doc.name}
{doc.type} · {doc.size}
+
+
+
+ {/* faux document page */} +
+
+
Yamuna Expressway Industrial Development Authority
+
+
{doc.name}
+ {[...Array(8)].map((_, i) =>
)} +
+ {[...Array(4)].map((_, i) =>
)} +
Authorised Signatory
OFFICIAL SEAL
+
+
+
+
+ ); + } + + window.Documents = Documents; +})(); diff --git a/src/Yammuna Express/extras.jsx b/src/Yammuna Express/extras.jsx new file mode 100644 index 0000000..c0292cc --- /dev/null +++ b/src/Yammuna Express/extras.jsx @@ -0,0 +1,277 @@ +/* Notices, Sector Maps, Support, Profile */ +(function () { + const { useState } = React; + const Icon = window.Icon, PageHead = window.PageHead, SectorMap = window.SectorMap, Avatar = window.Avatar; + const D = window.YEIDA; + + /* ---------------- NOTICES ---------------- */ + const NCATS = ['All', 'Policy Update', 'Construction Guidelines', 'Sector Update', 'Authority Circular', 'Announcement']; + function Notices() { + const [cat, setCat] = useState('All'); + const [sel, setSel] = useState(D.notices[0]); + const list = D.notices.filter(n => cat === 'All' || n.cat === cat); + return ( +
+ +
+ {NCATS.map(c => )} +
+
+
+ {list.map(n => ( + + ))} +
+
+
+ {sel.cat} +
+
+

{sel.title}

+
{sel.date}YEIDA Authority
+
+

{sel.body}

+ {sel.attachments.length > 0 && ( +
+
Attachments
+
+ {sel.attachments.map(a => ( +
+ {a} + +
+ ))} +
+
+ )} +
+
+
+ ); + } + + /* ---------------- SECTOR MAPS ---------------- */ + function SectorMaps({ go }) { + const [active, setActive] = useState(D.sectors[0]); + return ( +
+ +
+ {D.sectors.map(s => ( + + ))} +
+
+
+
+
+

{active.name}

{active.mine && Your sector}
+
+ {[['Type', active.type], ['Total Plots', active.plots.toLocaleString('en-IN')], ['Status', active.status], ['Location', 'Yamuna Expressway']].map(([l, v]) => ( +
{l}{v}
+ ))} +
+
+
+

Map Layers

+
+ {[['Road Network', 'map'], ['Infrastructure', 'building'], ['Master Plan Overlay', 'grid'], ['Satellite View', 'layers']].map(([l, ic]) => ( +
{l}
+ ))} +
+
+
+ +
Master Plan PDF
+

Download the official {active.name} master plan.

+ +
+
+
+
+ ); + } + + /* ---------------- SUPPORT ---------------- */ + function Support() { + return ( +
+ + + +
+ {[['Live Chat', 'Avg. response 2 min', 'chat', 'emerald'], ['Call Request', 'Request a callback', 'phone', 'blue'], ['Email Support', 'support@yeida.gov.in', 'mail', 'navy']].map(([t, s, ic, c]) => { + const cc = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c]; + return ( +
+ +
{t}
+
{s}
+
+ ); + })} +
+
+
+

My Tickets

+ + + + {D.tickets.map(t => ( + + + + + + + + ))} + +
TicketSubjectPriorityStatusUpdated
{t.id}{t.subject}{t.priority}{t.status}{t.updated}
+
+
+

Frequently Asked

+
+ {['How do I generate an e-Challan?', 'What is the construction approval timeline?', 'How is transfer fee calculated?', 'How do I update my KYC?', 'Where can I find my lease deed?'].map(q => ( + + ))} +
+
+
+
+ ); + } + + /* ---------------- PROFILE ---------------- */ + function Profile({ go }) { + const [tab, setTab] = useState('personal'); + const tabs = [['personal', 'Personal Info', 'user'], ['kyc', 'KYC Verification', 'shield'], ['security', 'Security', 'lock'], ['notifications', 'Notifications', 'bell'], ['devices', 'Devices', 'device']]; + return ( +
+ +
+
+
+
+
+ +

{D.user.name}

Allottee · {D.user.allotment}
+
+
+ KYC Verified + Member since {D.user.memberSince} +
+
+
+
+
+
+ {tabs.map(([k, l, ic]) => ( + + ))} +
+
+ {tab === 'personal' && } + {tab === 'kyc' && } + {tab === 'security' && } + {tab === 'notifications' && } + {tab === 'devices' && } +
+
+
+ ); + } + + function PersonalTab() { + const f = [['Full Name', D.user.name, 'user'], ['Mobile Number', D.user.mobile, 'phone'], ['Email Address', D.user.email, 'mail'], ['Allotment Number', D.user.allotment, 'building'], ['Sector', D.user.sector, 'map'], ['Plot Number', D.user.plot, 'plot']]; + return ( +
+

Personal Information

+
+ {f.map(([l, v, ic]) => ( +
+ ))} +
+
+ ); + } + function KycTab() { + return ( +
+

KYC Verification

+
+ +
Your KYC is fully verified
Verified on 12 Mar 2019 · Aadhaar & PAN linked
+
+ {[['Aadhaar', 'xxxx xxxx 4821', true], ['PAN', 'ABCDE****F', true], ['Address Proof', 'Verified', true]].map(([l, v, ok]) => ( +
+
{l}
{v}
+ Verified +
+ ))} +
+ ); + } + function SecurityTab() { + return ( +
+

Security Settings

+ {[['Password', 'Last changed 3 months ago', 'Change'], ['Two-Factor Authentication', 'OTP via mobile enabled', 'Manage'], ['Login Alerts', 'Email on new device login', 'Configure']].map(([l, s, b]) => ( +
+
{l}
{s}
+ +
+ ))} +
+ ); + } + function NotifTab() { + const items = [['Payment reminders', true], ['Construction approval updates', true], ['New notices & circulars', true], ['Transfer status', false], ['Promotional updates', false]]; + return ( +
+

Notification Preferences

+ {items.map(([l, on], i) => )} +
+ ); + } + function Toggle({ label, on }) { + const [v, setV] = useState(on); + return ( +
+ {label} + +
+ ); + } + function DevicesTab() { + const dev = [['MacBook Pro · Chrome', 'Greater Noida · Active now', true], ['iPhone 14 · YEIDA App', 'Greater Noida · 2h ago', false], ['Windows PC · Edge', 'Delhi · 5 days ago', false]]; + return ( +
+

Connected Devices

+ {dev.map(([l, s, cur], i) => ( +
+
{l}
{s}
+ {cur ? This device : } +
+ ))} +
+ ); + } + + Object.assign(window, { Notices, SectorMaps, Support, Profile }); +})(); diff --git a/src/Yammuna Express/floating-assistant.jsx b/src/Yammuna Express/floating-assistant.jsx new file mode 100644 index 0000000..371f077 --- /dev/null +++ b/src/Yammuna Express/floating-assistant.jsx @@ -0,0 +1,90 @@ +/* Floating AI Assistant — global launcher + compact chat panel, on every app page */ +(function () { + const { useState, useRef, useEffect } = React; + const Icon = window.Icon; + const D = window.YEIDA; + + // lightweight reply engine (shares the spirit of the full-page assistant) + function reply(text) { + const t = text.toLowerCase(); + const A = [ + { m: ['construction', 'build', 'start'], r: "Your construction is at Step 4 — Authority Review. Next action: upload the pending Soil Test Report. Want me to open the Construction module?", a: ['construction', 'Open Construction'] }, + { m: ['payment', 'due', 'pay', 'challan'], r: "You have ₹1,47,585 across 3 dues. Authority records and challans are on your Payments page.", a: ['payments', 'Open Payments'] }, + { m: ['document', 'paper', 'lease', 'atl'], r: "Lease Deed: Completed. ATL: Pending — that's your next paperwork item. All documents live in the Document Center.", a: ['documents', 'Document Center'] }, + { m: ['sell', 'transfer'], r: "To sell Plot B-47 you're eligible (7 years held). Estimated transfer cost ≈ ₹2.02 L. Shall I open the Transfer module?", a: ['transfer', 'Start Transfer'] }, + { m: ['plan', 'design', 'tour', 'sample'], r: "I've matched 3 plans to your 300 sq.m east-facing plot, with 3D walkthroughs. Open Sample Plans?", a: ['sampleplans', 'View Plans'] }, + { m: ['map', 'sector', 'location'], r: "Plot B-47 is in Sector 18, Block B — an east-facing 300 sq.m plot. I can open the interactive map.", a: ['sectormaps', 'Open Map'] }, + ]; + const hit = A.find(x => x.m.some(k => t.includes(k))); + return hit ? { reply: hit.r, action: hit.a } : { reply: "I can help with your plot status, authority paperwork, payments, construction, sample plans and transfers. What would you like to know?" }; + } + + function FloatingAssistant({ go }) { + const [open, setOpen] = useState(false); + const [msgs, setMsgs] = useState([{ role: 'bot', text: "Hi Rajeev 👋 Quick help on your plot — payments, paperwork, construction or plans. Ask away, or open the full assistant." }]); + const [input, setInput] = useState(''); + const [typing, setTyping] = useState(false); + const scroller = useRef(null); + useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [msgs, typing, open]); + + const send = (text) => { + if (!text.trim()) return; + setMsgs(m => [...m, { role: 'user', text }]); setInput(''); setTyping(true); + setTimeout(() => { const r = reply(text); setTyping(false); setMsgs(m => [...m, { role: 'bot', text: r.reply, action: r.action }]); }, 800); + }; + + const quick = ['What payments are pending?', 'What is my ATL status?', 'Show sample plans']; + + return ( +
+ {open && ( +
+ {/* header */} +
+
+ +
YEIDA Assistant
Online
+
+
+ + +
+
+ {/* messages */} +
+ {msgs.map((m, i) => ( +
+ {m.role === 'bot' && } +
+
{m.text}
+ {m.action && } +
+
+ ))} + {typing &&
{[0, 1, 2].map(i => )}
} + {msgs.length === 1 && ( +
+ {quick.map(q => )} +
+ )} +
+ {/* input */} +
+
+ setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send(input)} placeholder="Ask anything…" style={{ flex: 1, border: 'none', background: 'none', outline: 'none', fontSize: 13.5 }} /> + +
+
+
+ )} + {/* launcher */} + +
+ ); + } + + window.FloatingAssistant = FloatingAssistant; +})(); diff --git a/src/Yammuna Express/icons.jsx b/src/Yammuna Express/icons.jsx new file mode 100644 index 0000000..9ae8fc7 --- /dev/null +++ b/src/Yammuna Express/icons.jsx @@ -0,0 +1,101 @@ +/* Icon set — lightweight stroke icons as React components. + Usage: ; all share window.Icon */ +(function () { + const P = { + dashboard: 'M3 13h8V3H3v10Zm0 8h8v-6H3v6Zm10 0h8V11h-8v10Zm0-18v6h8V3h-8Z', + plot: 'M4 4h16v16H4z M4 9h16 M4 14h16 M9 4v16 M14 4v16', + construction: 'M3 21h18 M5 21V8l7-4 7 4v13 M9 21v-5h6v5 M9.5 11h.01 M14.5 11h.01', + payments: 'M3 7h18v10H3z M3 11h18 M7 15h3', + documents: 'M14 3v5h5 M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-5Z M9 13h6 M9 17h6', + notices: 'M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0', + map: 'M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2Z M9 4v14 M15 6v14', + transfer: 'M7 8h14 M7 8l4-4 M7 8l4 4 M17 16H3 M17 16l-4-4 M17 16l-4 4', + assistant: 'M12 3a9 9 0 0 0-9 9v5a3 3 0 0 0 3 3h2v-7H5v-1a7 7 0 0 1 14 0v1h-3v7h2a3 3 0 0 0 3-3v-5a9 9 0 0 0-9-9Z', + support: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3 M12 17h.01', + profile: 'M20 21a8 8 0 1 0-16 0 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z', + logout: 'M16 17l5-5-5-5 M21 12H9 M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4', + search: 'M21 21l-4.3-4.3 M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z', + bell: 'M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0', + chevR: 'M9 6l6 6-6 6', + chevL: 'M15 6l-6 6 6 6', + chevD: 'M6 9l6 6 6-6', + chevU: 'M6 15l6-6 6 6', + arrowR: 'M5 12h14 M13 6l6 6-6 6', + check: 'M20 6 9 17l-5-5', + checkCircle: 'M22 11.1V12a10 10 0 1 1-5.9-9.1 M22 4 12 14.1l-3-3', + x: 'M18 6 6 18 M6 6l12 12', + plus: 'M12 5v14 M5 12h14', + download: 'M12 3v12 M7 10l5 5 5-5 M5 21h14', + upload: 'M12 21V9 M7 14l5-5 5 5 M5 3h14', + eye: 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', + bookmark: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z', + filter: 'M3 5h18 M6 12h12 M10 19h4', + clock: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M12 7v5l3 2', + calendar: 'M8 3v4 M16 3v4 M3 9h18 M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z', + mapPin: 'M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', + phone: 'M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2 4.2 2 2 0 0 1 4 2h3a2 2 0 0 1 2 1.7c.1.9.4 1.8.7 2.7a2 2 0 0 1-.5 2.1L8 9.6a16 16 0 0 0 6 6l1.1-1.1a2 2 0 0 1 2.1-.5c.9.3 1.8.6 2.7.7A2 2 0 0 1 22 16.9Z', + mail: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z M22 7l-10 6L2 7', + lock: 'M5 11h14a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1Z M8 11V7a4 4 0 1 1 8 0v4', + shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z M9 12l2 2 4-4', + rupee: 'M6 4h12 M6 8h12 M9 4c4 0 6 2 6 5s-2 4-5 4H8l6 7 M6 13h2', + building: 'M3 21h18 M5 21V4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v17 M15 8h3a1 1 0 0 1 1 1v12 M8 7h3 M8 11h3 M8 15h3', + trend: 'M3 17l6-6 4 4 8-8 M21 7v6h-6', + alert: 'M12 9v4 M12 17h.01 M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z', + info: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M12 16v-4 M12 8h.01', + send: 'M22 2 11 13 M22 2l-7 20-4-9-9-4 20-7Z', + sparkles: 'M12 3l1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3Z M19 14l.8 2L22 16.8 20 17.6 19 20l-.8-2.4L16 16.8l2.2-.8L19 14Z', + menu: 'M3 6h18 M3 12h18 M3 18h18', + moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z', + globe: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M2 12h20 M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20Z', + grid: 'M3 3h7v7H3z M14 3h7v7h-7z M14 14h7v7h-7z M3 14h7v7H3z', + layers: 'M12 2 2 7l10 5 10-5-10-5Z M2 17l10 5 10-5 M2 12l10 5 10-5', + zoomIn: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z M21 21l-4.3-4.3 M11 8v6 M8 11h6', + zoomOut: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z M21 21l-4.3-4.3 M8 11h6', + file: 'M14 3v5h5 M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-5Z', + receipt: 'M5 3h14v18l-3-2-2 2-2-2-2 2-2-2-3 2V3Z M9 8h6 M9 12h6', + edit: 'M12 20h9 M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z', + star: 'M12 3l2.9 6 6.5.9-4.7 4.6 1.1 6.5L12 18l-5.8 3 1.1-6.5L2.6 9.9 9 9l3-6Z', + user: 'M20 21a8 8 0 1 0-16 0 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z', + settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1A1.6 1.6 0 0 0 7 19.4a1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H1a2 2 0 1 1 0-4h.1A1.6 1.6 0 0 0 2.6 7a1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1A1.6 1.6 0 0 0 7 2.6 1.6 1.6 0 0 0 8 1.1V1a2 2 0 1 1 4 0v.1A1.6 1.6 0 0 0 14.9 2.6a1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0 1.1 2.7H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z', + history: 'M3 3v6h6 M3.5 9a9 9 0 1 0 2-5L3 9 M12 7v5l3 2', + home: 'M3 11l9-8 9 8 M5 9v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9', + key: 'M15 7a4 4 0 1 1-5.7 3.6L3 17v3h3l1-1h2v-2h2v-2l1.3-1.3A4 4 0 0 1 15 7Z M16.5 7.5h.01', + device: 'M4 4h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M8 20h8 M12 16v4', + paperclip: 'M21 11.5 12 20a5 5 0 0 1-7-7l9-8.5a3.3 3.3 0 0 1 4.7 4.7L9.6 17.7a1.7 1.7 0 0 1-2.3-2.3l8-7.6', + ticket: 'M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2 2 2 0 0 0 0 6 2 2 0 0 1-2 2H5a2 2 0 0 1-2-2 2 2 0 0 0 0-6Z M13 7v10', + chat: 'M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 21l1.2-4.8A8.4 8.4 0 0 1 12 3a8.4 8.4 0 0 1 9 8.5Z', + flag: 'M4 22V4 M4 4h13l-2 4 2 4H4', + refresh: 'M21 12a9 9 0 1 1-2.6-6.4 M21 3v5h-5', + bed: 'M3 7v11 M3 12h18v6 M21 18v-5a3 3 0 0 0-3-3h-7v5 M7 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', + bath: 'M4 12V6a2 2 0 0 1 2-2 2 2 0 0 1 2 2 M3 12h18v3a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4v-3Z M6 19l-1 2 M18 19l1 2 M8 6h.01', + ruler: 'M3 9l6-6 12 12-6 6L3 9Z M9 5l1.5 1.5 M12 8l1.5 1.5 M15 11l1.5 1.5 M6 12l1.5 1.5', + briefcase: 'M3 8h18v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8Z M8 8V6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2 M3 13h18', + users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z M22 21v-2a4 4 0 0 0-3-3.9 M16 3.1a4 4 0 0 1 0 7.8', + tag: 'M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8Z M7.5 7.5h.01', + image: 'M4 4h16a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z M21 16l-5-5L5 21', + dots: 'M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z M12 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z M12 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z', + megaphone: 'M3 11v2a1 1 0 0 0 1 1h2l5 4V6L6 10H4a1 1 0 0 0-1 1Z M16 8a4 4 0 0 1 0 8 M19 5a8 8 0 0 1 0 14', + arrowUpRight: 'M7 17 17 7 M8 7h9v9', + arrowDownRight: 'M7 7l10 10 M17 8v9H8', + heart: 'M20.8 5.6a5 5 0 0 0-7.1 0L12 7.3l-1.7-1.7a5 5 0 1 0-7.1 7.1L12 21.5l8.8-8.8a5 5 0 0 0 0-7.1Z', + messages: 'M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 21l1.2-4.8A8.4 8.4 0 0 1 12 3a8.4 8.4 0 0 1 9 8.5Z', + sun: 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10Z M12 1v2 M12 21v2 M4.2 4.2l1.4 1.4 M18.4 18.4l1.4 1.4 M1 12h2 M21 12h2 M4.2 19.8l1.4-1.4 M18.4 5.6l1.4-1.4', + chartBar: 'M3 3v18h18 M8 17v-5 M13 17V8 M18 17v-9', + funnel: 'M3 5h18 M6 10h12 M10 15h4 M11 20h2', + bookmark2: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z', + pin: 'M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', + expand: 'M15 3h6v6 M9 21H3v-6 M21 3l-7 7 M3 21l7-7', + panelLeft: 'M3 4h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M9 4v16', + }; + function Icon({ name, size, style, className, strokeWidth }) { + const d = P[name]; + if (!d) return null; + const segs = d.split(' M').map((s, i) => (i === 0 ? s : 'M' + s)); + return React.createElement('svg', { + width: size || 20, height: size || 20, viewBox: '0 0 24 24', fill: 'none', + stroke: 'currentColor', strokeWidth: strokeWidth || 1.8, strokeLinecap: 'round', + strokeLinejoin: 'round', style, className, 'aria-hidden': true, + }, segs.map((seg, i) => React.createElement('path', { key: i, d: seg }))); + } + window.Icon = Icon; +})(); diff --git a/src/Yammuna Express/image-slot.js b/src/Yammuna Express/image-slot.js new file mode 100644 index 0000000..f90728a --- /dev/null +++ b/src/Yammuna Express/image-slot.js @@ -0,0 +1,643 @@ +// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design) +/* BEGIN USAGE */ +/** + * — user-fillable image placeholder. + * + * Drop this into a deck, mockup, or page wherever you want the user to + * supply an image. You control the slot's shape and size; the user fills it + * by dragging an image file onto it (or clicking to browse). The dropped + * image persists across reloads via a .image-slots.state.json sidecar — + * same read-via-fetch / write-via-window.omelette pattern as + * design_canvas.jsx, so the filled slot shows on share links, downloaded + * zips, and PPTX export. Outside the omelette runtime the slot is read-only. + * + * The host bridge only allows sidecar writes at the project root, so the + * HTML that uses this component is assumed to live at the project root too + * (same constraint as design_canvas.jsx). + * + * Attributes: + * id Persistence key. REQUIRED for the drop to survive reload — + * every slot on the page needs a distinct id. + * shape 'rect' | 'rounded' | 'circle' | 'pill' (default 'rounded') + * 'circle' applies 50% border-radius; on a non-square slot + * that's an ellipse — set equal width and height for a true + * circle. + * radius Corner radius in px for 'rounded'. (default 12) + * mask Any CSS clip-path value. Overrides `shape` — use this for + * hexagons, blobs, arbitrary polygons. + * fit object-fit: cover | contain | fill. (default 'cover') + * With cover (the default) double-clicking the filled slot + * enters a reframe mode: the whole image spills past the mask + * (translucent outside, opaque inside), drag to reposition, + * corner-drag to scale. The crop persists alongside the image + * in the sidecar. contain/fill stay static. + * position object-position for fit=contain|fill. (default '50% 50%') + * placeholder Empty-state caption. (default 'Drop an image') + * src Optional initial/fallback image URL. A user drop overrides + * it; clearing the drop reveals src again. + * + * Size and layout come from ordinary CSS on the element — width/height + * inline or from a parent grid — so it composes with any layout. + * + * Usage: + * + * + * + */ +/* END USAGE */ + +(() => { + const STATE_FILE = '.image-slots.state.json'; + // 2× a ~600px slot in a 1920-wide deck — retina-sharp without making the + // sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB. + const MAX_DIM = 1200; + // Raster formats only. SVG is excluded (can carry script; createImageBitmap + // on SVG blobs is inconsistent). GIF is excluded because the canvas + // re-encode keeps only the first frame, so an animated GIF would silently + // go still — better to reject than surprise. + const ACCEPT = ['image/png', 'image/jpeg', 'image/webp', 'image/avif']; + + // ── Shared sidecar store ──────────────────────────────────────────────── + // One fetch + immediate write-on-change for every on the + // page. Reads via fetch() so viewing works anywhere the HTML and sidecar + // are served together; writes go through window.omelette.writeFile, which + // the host allowlists to *.state.json basenames only. + const subs = new Set(); + let slots = {}; + // ids explicitly cleared before the sidecar fetch resolved — otherwise + // the merge below can't tell "never set" from "just deleted" and would + // resurrect the sidecar's stale value. + const tombstones = new Set(); + let loaded = false; + let loadP = null; + + function load() { + if (loadP) return loadP; + loadP = fetch(STATE_FILE) + .then((r) => (r.ok ? r.json() : null)) + .then((j) => { + // Merge: sidecar loses to any in-memory change that raced ahead of + // the fetch (drop or clear) so neither is clobbered by hydration. + if (j && typeof j === 'object') { + const merged = Object.assign({}, j, slots); + // A framing-only write that raced ahead of hydration must not + // drop a user image that's only on disk — inherit u from the + // sidecar for any in-memory entry that lacks one. + for (const k in slots) { + if (merged[k] && !merged[k].u && j[k]) { + merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u; + } + } + for (const id of tombstones) delete merged[id]; + slots = merged; + } + tombstones.clear(); + }) + .catch(() => {}) + .then(() => { loaded = true; subs.forEach((fn) => fn()); }); + return loadP; + } + + // Serialize writes so two near-simultaneous drops on different slots + // can't reorder at the backend and leave the sidecar with only the + // first. A save requested mid-flight just marks dirty and re-fires on + // completion with the then-current slots. + let saving = false; + let saveDirty = false; + function save() { + if (saving) { saveDirty = true; return; } + const w = window.omelette && window.omelette.writeFile; + if (!w) return; + saving = true; + Promise.resolve(w(STATE_FILE, JSON.stringify(slots))) + .catch(() => {}) + .then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } }); + } + + const S_MAX = 5; + const clampS = (s) => Math.max(1, Math.min(S_MAX, s)); + + // Normalize a stored slot value. Pre-reframe sidecars stored a bare + // data-URL string; newer ones store {u, s, x, y}. Either shape is valid. + function getSlot(id) { + const v = slots[id]; + if (!v) return null; + return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v; + } + + function setSlot(id, val) { + if (!id) return; + if (val) { slots[id] = val; tombstones.delete(id); } + else { delete slots[id]; if (!loaded) tombstones.add(id); } + subs.forEach((fn) => fn()); + // A drop is rare + high-value — write immediately so nav-away can't lose + // it. Gate on the initial read so we don't overwrite a sidecar we haven't + // merged yet; the merge in load() keeps this change once the read lands. + if (loaded) save(); else load().then(save); + } + + // ── Image downscale ───────────────────────────────────────────────────── + // Encode through a canvas so the sidecar carries resized bytes, not the + // raw upload. Longest side is capped at 2× the slot's rendered width + // (retina) and at MAX_DIM. WebP keeps alpha and is ~10× smaller than PNG + // for photos, so there's no need for per-image format picking. + async function toDataUrl(file, targetW) { + const bitmap = await createImageBitmap(file); + try { + const cap = Math.min(MAX_DIM, Math.max(1, Math.round(targetW * 2)) || MAX_DIM); + const scale = Math.min(1, cap / Math.max(bitmap.width, bitmap.height)); + const w = Math.max(1, Math.round(bitmap.width * scale)); + const h = Math.max(1, Math.round(bitmap.height * scale)); + const canvas = document.createElement('canvas'); + canvas.width = w; canvas.height = h; + canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h); + return canvas.toDataURL('image/webp', 0.85); + } finally { + bitmap.close && bitmap.close(); + } + } + + // ── Custom element ────────────────────────────────────────────────────── + const stylesheet = + ':host{display:inline-block;position:relative;vertical-align:top;' + + ' font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);width:240px;height:160px}' + + '.frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}' + + // .frame img (clipped) and .spill (unclipped ghost + handles) share the + // same left/top/width/height in frame-%, computed by _applyView(), so the + // inside-mask crop and the outside-mask spill stay pixel-aligned. + '.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' + + ' -webkit-user-drag:none;user-select:none;touch-action:none}' + + // Reframe mode (double-click): the full image spills past the mask. The + // spill layer is sized to the IMAGE bounds so its corners are where the + // resize handles belong. The ghost inside is translucent; the real + // clipped underneath shows the opaque in-mask crop. + '.spill{position:absolute;transform:translate(-50%,-50%);display:none;z-index:1;' + + ' cursor:grab;touch-action:none}' + + ':host([data-panning]) .spill{cursor:grabbing}' + + '.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' + + ' pointer-events:none;-webkit-user-drag:none;user-select:none;' + + ' box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' + + '.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' + + ' background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' + + ' transform:translate(-50%,-50%)}' + + '.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' + + '.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' + + '.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' + + '.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' + + ':host([data-reframe]){z-index:10}' + + ':host([data-reframe]) .spill{display:block}' + + ':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' + + '.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' + + ' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' + + ' cursor:pointer;user-select:none}' + + '.empty svg{opacity:.45}' + + '.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' + + '.empty .sub{font-size:11px}' + + '.empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}' + + '.empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}' + + ':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' + + ' background:rgba(201,100,66,.10)}' + + '.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);' + + ' transition:border-color .12s}' + + ':host([data-over]) .ring{border-color:#c96442}' + + ':host([data-filled]) .ring{display:none}' + + // Controls sit BELOW the mask (top:100%), absolutely positioned so the + // author-declared slot height is unaffected. The gap is padding, not a + // top offset, so the hover target stays contiguous with the frame. + '.ctl{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding-top:8px;' + + ' display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' + + ' white-space:nowrap}' + + ':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' + + ' {opacity:1;pointer-events:auto}' + + '.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' + + ' background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;' + + ' backdrop-filter:blur(6px)}' + + '.ctl button:hover{background:rgba(0,0,0,.8)}' + + '.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' + + ' background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}'; + + const icon = + '' + + '' + + ''; + + class ImageSlot extends HTMLElement { + static get observedAttributes() { + return ['shape', 'radius', 'mask', 'fit', 'position', 'placeholder', 'src', 'id']; + } + + constructor() { + super(); + const root = this.attachShadow({ mode: 'open' }); + // .spill and .ctl sit OUTSIDE .frame so overflow:hidden + border-radius + // on the frame (circle, pill, rounded) can't clip them. + root.innerHTML = + '' + + '
' + + ' ' + + '
' + icon + + '
' + + '
or browse files
' + + '
' + + '
' + + '
' + + ' ' + + '
' + + '
' + + '
' + + '
' + + '
' + + ''; + this._frame = root.querySelector('.frame'); + this._ring = root.querySelector('.ring'); + this._img = root.querySelector('.frame img'); + this._empty = root.querySelector('.empty'); + this._cap = root.querySelector('.cap'); + this._sub = root.querySelector('.sub'); + this._spill = root.querySelector('.spill'); + this._ghost = root.querySelector('.ghost'); + this._err = null; + this._input = root.querySelector('input'); + this._depth = 0; + this._gen = 0; + this._view = { s: 1, x: 0, y: 0 }; + this._subFn = () => this._render(); + // Shadow-DOM listeners live with the shadow DOM — bound once here so + // disconnect/reconnect (e.g. React remount) doesn't stack handlers. + this._empty.addEventListener('click', () => this._input.click()); + root.addEventListener('click', (e) => { + const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act'); + if (act === 'replace') { this._exitReframe(true); this._input.click(); } + if (act === 'clear') { + this._exitReframe(false); + this._gen++; + this._local = null; + if (this.id) setSlot(this.id, null); else this._render(); + } + }); + this._input.addEventListener('change', () => { + const f = this._input.files && this._input.files[0]; + if (f) this._ingest(f); + this._input.value = ''; + }); + // naturalWidth/Height aren't known until load — re-apply so the cover + // baseline is computed from real dimensions, not the 100%×100% fallback. + this._img.addEventListener('load', () => this._applyView()); + // Gated on editable + fit=cover so share links and contain/fill slots + // stay static. + this.addEventListener('dblclick', (e) => { + if (!this.hasAttribute('data-editable') || !this._reframes()) return; + e.preventDefault(); + if (this.hasAttribute('data-reframe')) this._exitReframe(true); + else this._enterReframe(); + }); + // Pan + resize both originate on the spill layer. A handle pointerdown + // drives an aspect-locked resize anchored at the opposite corner; any + // other pointerdown on the spill pans. Offsets are frame-% so a + // reframed slot survives responsive resize / PPTX export. + this._spill.addEventListener('pointerdown', (e) => { + if (e.button !== 0 || !this.hasAttribute('data-reframe')) return; + e.preventDefault(); + e.stopPropagation(); + this._spill.setPointerCapture(e.pointerId); + const rect = this.getBoundingClientRect(); + const fw = rect.width || 1, fh = rect.height || 1; + const corner = e.target.getAttribute && e.target.getAttribute('data-c'); + let move; + if (corner) { + // Resize about the OPPOSITE corner. Viewport-px throughout (rect + // fw/fh, not clientWidth) so the math survives a transform:scale() + // ancestor — deck_stage renders slides scaled-to-fit. + const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1; + const base = Math.max(fw / iw, fh / ih); + const sx = corner.includes('e') ? 1 : -1; + const sy = corner.includes('s') ? 1 : -1; + const s0 = this._view.s; + const w0 = iw * base * s0, h0 = ih * base * s0; + const cx0 = (50 + this._view.x) / 100 * fw; + const cy0 = (50 + this._view.y) / 100 * fh; + const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2; + const diag0 = Math.hypot(w0, h0); + const ux = sx * w0 / diag0, uy = sy * h0 / diag0; + move = (ev) => { + const proj = (ev.clientX - rect.left - ox) * ux + + (ev.clientY - rect.top - oy) * uy; + const s = clampS(s0 * proj / diag0); + const d = diag0 * s / s0; + this._view.s = s; + this._view.x = (ox + ux * d / 2) / fw * 100 - 50; + this._view.y = (oy + uy * d / 2) / fh * 100 - 50; + this._clampView(); + this._applyView(); + }; + } else { + this.setAttribute('data-panning', ''); + const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y }; + move = (ev) => { + this._view.x = start.x + (ev.clientX - start.px) / fw * 100; + this._view.y = start.y + (ev.clientY - start.py) / fh * 100; + this._clampView(); + this._applyView(); + }; + } + const up = () => { + try { this._spill.releasePointerCapture(e.pointerId); } catch {} + this._spill.removeEventListener('pointermove', move); + this._spill.removeEventListener('pointerup', up); + this._spill.removeEventListener('pointercancel', up); + this.removeAttribute('data-panning'); + this._dragUp = null; + }; + // Stashed so _exitReframe (Escape / outside-click mid-drag) can + // tear the capture + listeners down synchronously. + this._dragUp = up; + this._spill.addEventListener('pointermove', move); + this._spill.addEventListener('pointerup', up); + this._spill.addEventListener('pointercancel', up); + }); + // Wheel zoom stays available inside reframe mode as a trackpad nicety — + // zooms toward the cursor (offset' = cursor·(1-k) + offset·k). + this.addEventListener('wheel', (e) => { + if (!this.hasAttribute('data-reframe')) return; + e.preventDefault(); + const r = this.getBoundingClientRect(); + const cx = (e.clientX - r.left) / r.width * 100 - 50; + const cy = (e.clientY - r.top) / r.height * 100 - 50; + const prev = this._view.s; + const next = clampS(prev * Math.pow(1.0015, -e.deltaY)); + if (next === prev) return; + const k = next / prev; + this._view.s = next; + this._view.x = cx * (1 - k) + this._view.x * k; + this._view.y = cy * (1 - k) + this._view.y * k; + this._clampView(); + this._applyView(); + }, { passive: false }); + } + + connectedCallback() { + // Warn once per page — an id-less slot works for the session but + // cannot persist, and two id-less slots would share nothing. + if (!this.id && !ImageSlot._warned) { + ImageSlot._warned = true; + console.warn(' without an id will not persist its dropped image.'); + } + this.addEventListener('dragenter', this); + this.addEventListener('dragover', this); + this.addEventListener('dragleave', this); + this.addEventListener('drop', this); + subs.add(this._subFn); + // width%/height% in _applyView encode the frame aspect at call time — + // a host resize (responsive grid, pane divider) would stretch the + // image until the next _render. Re-render on size change: _render() + // re-seeds _view from stored before clamp/apply, so a shrink→grow + // cycle round-trips instead of ratcheting x/y toward the narrower + // frame's clamp range. + this._ro = new ResizeObserver(() => this._render()); + this._ro.observe(this); + load(); + this._render(); + } + + disconnectedCallback() { + subs.delete(this._subFn); + this.removeEventListener('dragenter', this); + this.removeEventListener('dragover', this); + this.removeEventListener('dragleave', this); + this.removeEventListener('drop', this); + if (this._ro) { this._ro.disconnect(); this._ro = null; } + this._exitReframe(false); + } + + _enterReframe() { + if (this.hasAttribute('data-reframe')) return; + this.setAttribute('data-reframe', ''); + this._applyView(); + // Close on click outside (the spill handler stopPropagation()s so + // in-image drags don't reach this) and on Escape. Listeners are held + // on the instance so _exitReframe / disconnectedCallback can detach + // exactly what was attached. + this._outside = (e) => { + if (e.composedPath && e.composedPath().includes(this)) return; + this._exitReframe(true); + }; + this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); }; + document.addEventListener('pointerdown', this._outside, true); + document.addEventListener('keydown', this._esc, true); + } + + _exitReframe(commit) { + if (!this.hasAttribute('data-reframe')) return; + if (this._dragUp) this._dragUp(); + this.removeAttribute('data-reframe'); + this.removeAttribute('data-panning'); + if (this._outside) document.removeEventListener('pointerdown', this._outside, true); + if (this._esc) document.removeEventListener('keydown', this._esc, true); + this._outside = this._esc = null; + if (commit) this._commitView(); + } + + attributeChangedCallback() { if (this.shadowRoot) this._render(); } + + // handleEvent — one listener object for all four drag events keeps the + // add/remove symmetric and the depth counter correct. + handleEvent(e) { + if (e.type === 'dragenter' || e.type === 'dragover') { + // Without preventDefault the browser never fires 'drop'. + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + if (e.type === 'dragenter') this._depth++; + this.setAttribute('data-over', ''); + } else if (e.type === 'dragleave') { + // dragenter/leave fire for every descendant crossing — count depth + // so hovering the icon inside the empty state doesn't flicker. + if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); } + } else if (e.type === 'drop') { + e.preventDefault(); + e.stopPropagation(); + this._depth = 0; + this.removeAttribute('data-over'); + const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; + if (f) this._ingest(f); + } + } + + async _ingest(file) { + this._setError(null); + if (!file || ACCEPT.indexOf(file.type) < 0) { + this._setError('Drop a PNG, JPEG, WebP, or AVIF image.'); + return; + } + // toDataUrl can take hundreds of ms on a large photo. A Clear or a + // newer drop during that window would be clobbered when this await + // resumes — bump + capture a generation so stale encodes bail. + const gen = ++this._gen; + try { + const w = this.clientWidth || this.offsetWidth || MAX_DIM; + const url = await toDataUrl(file, w); + if (gen !== this._gen) return; + // Only exit reframe once the new image is in hand — a rejected type + // or decode failure leaves the in-progress crop untouched. + this._exitReframe(false); + const val = { u: url, s: 1, x: 0, y: 0 }; + setSlot(this.id || '', val); + // Keep a session-local copy for id-less slots so the drop still + // shows, even though it cannot persist. + if (!this.id) { this._local = val; this._render(); } + } catch (err) { + if (gen !== this._gen) return; + this._setError('Could not read that image.'); + console.warn(' ingest failed:', err); + } + } + + _setError(msg) { + if (this._err) { this._err.remove(); this._err = null; } + if (!msg) return; + const d = document.createElement('div'); + d.className = 'err'; d.textContent = msg; + this.shadowRoot.appendChild(d); + this._err = d; + setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000); + } + + // Reframing (pan/resize) is only meaningful for fit=cover — contain/fill + // keep the old object-fit path and double-click is a no-op. + _reframes() { + return this.hasAttribute('data-filled') && + (this.getAttribute('fit') || 'cover') === 'cover'; + } + + // Cover-baseline geometry, shared by clamp/apply/resize. Null until the + // img has loaded (naturalWidth is 0 before that) or when the slot has no + // layout box — ResizeObserver fires with a 0×0 rect under display:none, + // and clamping against a degenerate 1×1 frame would silently pull the + // stored pan toward zero. + _geom() { + const iw = this._img.naturalWidth, ih = this._img.naturalHeight; + const fw = this.clientWidth, fh = this.clientHeight; + if (!iw || !ih || !fw || !fh) return null; + return { iw, ih, fw, fh, base: Math.max(fw / iw, fh / ih) }; + } + + _clampView() { + // Pan range on each axis is half the overflow past the frame edge. + const g = this._geom(); + if (!g) return; + const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50); + const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50); + this._view.x = Math.max(-mx, Math.min(mx, this._view.x)); + this._view.y = Math.max(-my, Math.min(my, this._view.y)); + } + + _applyView() { + const g = this._geom(); + const fit = this.getAttribute('fit') || 'cover'; + if (fit !== 'cover' || !g) { + // Non-cover, or dimensions not known yet (before img load). + this._img.style.width = '100%'; + this._img.style.height = '100%'; + this._img.style.left = '50%'; + this._img.style.top = '50%'; + this._img.style.objectFit = fit; + this._img.style.objectPosition = this.getAttribute('position') || '50% 50%'; + return; + } + // Cover baseline: img fills the frame on its tighter axis at s=1, so + // pan works immediately on the overflowing axis without zooming first. + // Width/height and left/top are all frame-% — depends only on the + // frame aspect ratio, so a responsive resize keeps the same crop. The + // spill layer mirrors the same box so its corners = image corners. + const k = g.base * this._view.s; + const w = (g.iw * k / g.fw * 100) + '%'; + const h = (g.ih * k / g.fh * 100) + '%'; + const l = (50 + this._view.x) + '%'; + const t = (50 + this._view.y) + '%'; + this._img.style.width = w; this._img.style.height = h; + this._img.style.left = l; this._img.style.top = t; + this._img.style.objectFit = ''; + this._spill.style.width = w; this._spill.style.height = h; + this._spill.style.left = l; this._spill.style.top = t; + } + + _commitView() { + const v = { s: this._view.s, x: this._view.x, y: this._view.y }; + if (this._userUrl) v.u = this._userUrl; + // Framing-only (no u) persists too so an author-src slot remembers its + // crop; clearing the sidecar still falls through to src=. + if (this.id) setSlot(this.id, v); + else { this._local = v; } + } + + _render() { + // Shape / mask. Presets use border-radius so the dashed ring can + // follow the rounded outline; clip-path is only applied for an + // explicit `mask` (the ring is hidden there since a rectangle + // dashed border chopped by an arbitrary polygon looks broken). + const mask = this.getAttribute('mask'); + const shape = (this.getAttribute('shape') || 'rounded').toLowerCase(); + let radius = ''; + if (shape === 'circle') radius = '50%'; + else if (shape === 'pill') radius = '9999px'; + else if (shape === 'rounded') { + const n = parseFloat(this.getAttribute('radius')); + radius = (Number.isFinite(n) ? n : 12) + 'px'; + } + this._frame.style.borderRadius = mask ? '' : radius; + this._frame.style.clipPath = mask || ''; + this._ring.style.borderRadius = mask ? '' : radius; + this._ring.style.display = mask ? 'none' : ''; + + // Controls and reframe entry gate on this so share links stay read-only. + const editable = !!(window.omelette && window.omelette.writeFile); + this.toggleAttribute('data-editable', editable); + this._sub.style.display = editable ? '' : 'none'; + + // Content. The sidecar is also writable by the agent's write_file + // tool, so its value isn't guaranteed canvas-originated — only accept + // data:image/ URLs from it. The `src` attribute is author-controlled + // (Claude wrote it into the HTML) so it passes through unchanged. + let stored = this.id ? getSlot(this.id) : this._local; + if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null; + const srcAttr = this.getAttribute('src') || ''; + this._userUrl = (stored && stored.u) || null; + const url = this._userUrl || srcAttr; + // Don't clobber an in-flight reframe with a store-triggered re-render. + if (!this.hasAttribute('data-reframe')) { + this._view = { + s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1, + x: stored && Number.isFinite(stored.x) ? stored.x : 0, + y: stored && Number.isFinite(stored.y) ? stored.y : 0, + }; + } + this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image'; + // Toggle via style.display — the [hidden] attribute alone loses to + // the display:flex / display:block rules in the stylesheet above. + if (url) { + if (this._img.getAttribute('src') !== url) { + this._img.src = url; + this._ghost.src = url; + } + this._img.style.display = 'block'; + this._empty.style.display = 'none'; + this.setAttribute('data-filled', ''); + this._clampView(); + this._applyView(); + } else { + this._img.style.display = 'none'; + this._img.removeAttribute('src'); + this._ghost.removeAttribute('src'); + this._empty.style.display = 'flex'; + this.removeAttribute('data-filled'); + } + } + } + + if (!customElements.get('image-slot')) { + customElements.define('image-slot', ImageSlot); + } +})(); diff --git a/src/Yammuna Express/lux-app.jsx b/src/Yammuna Express/lux-app.jsx new file mode 100644 index 0000000..30067fa --- /dev/null +++ b/src/Yammuna Express/lux-app.jsx @@ -0,0 +1,24 @@ +/* Aurum Estates — router */ +(function () { + const { useState, useEffect } = React; + const Shell = window.LuxShell, useToast = window.useLuxToast; + + function App() { + const [route, setRoute] = useState(() => localStorage.getItem('aurum_route') || 'dashboard'); + const [toastNode, toast] = useToast(); + const go = (r) => { setRoute(r); localStorage.setItem('aurum_route', r); const m = document.querySelector('main'); if (m) m.scrollTop = 0; }; + + const PAGES = { + dashboard: window.LuxDashboard, properties: window.LuxProperties, projects: window.LuxProjects, + clients: window.LuxClients, leads: window.LuxLeads, bookings: window.LuxBookings, + payments: window.LuxPayments, blogs: window.LuxBlogs, media: window.LuxMedia, + reviews: window.LuxReviews, analytics: window.LuxAnalytics, reports: window.LuxReports, + settings: window.LuxSettings, + }; + const Page = PAGES[route] || window.LuxDashboard; + return React.createElement(Shell, { route, go, toastNode }, + React.createElement(Page, { go, toast })); + } + + ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App)); +})(); diff --git a/src/Yammuna Express/lux-dashboard.jsx b/src/Yammuna Express/lux-dashboard.jsx new file mode 100644 index 0000000..14b6443 --- /dev/null +++ b/src/Yammuna Express/lux-dashboard.jsx @@ -0,0 +1,165 @@ +/* Aurum Estates — Dashboard */ +(function () { + const Icon = window.Icon, A = window.AURUM; + const { Sparkline, AreaChart, Bars, LuxDonut, PropImage, LuxAvatar, Stars } = window; + + const COL = { + blue: ['var(--blue-soft)', 'var(--blue)'], navy: ['#e6edf4', 'var(--navy)'], + gold: ['var(--gold-soft)', '#9c7b3a'], green: ['var(--green-soft)', 'var(--green-600)'], + }; + + function Dashboard({ go }) { + return ( +
+ + + + + + {/* KPI row */} +
+ {A.kpis.map(k => { + const c = COL[k.color]; + return ( +
+
+ + {k.delta} +
+
{k.value}
+
+ {k.label} + +
+
+ ); + })} +
+ + {/* Revenue + occupancy */} +
+
+
+
+

Revenue Overview

+

Monthly closed revenue · 2026

+
+
+
YTD Revenue
$48.6M
+
Avg / month
$4.05M
+
+
+ +
+
+

Occupancy & Sales

+

Across managed inventory

+
+
+ {A.subKpis.slice(0, 3).map(s => { + const c = COL[s.color]; + return ( +
+ +
{s.label}
{s.sub}
+
{s.value}
+
+ ); + })} +
+
+
+ + {/* Featured properties */} +
+
+

Featured Properties

Your highest-performing luxury listings

+ +
+
+ {A.properties.slice(0, 3).map(p => )} +
+
+ + {/* Lower: leads + activity */} +
+
+
+

Recent Leads

Latest inquiries & site-visit requests

+ +
+
+ + + + {A.leads.slice(0, 4).map(l => ( + + + + + + + + ))} + +
LeadTypeInterestStageScore
{l.name}
{l.when}
{l.type}{l.interest}{l.stage}= 85 ? 'var(--green-600)' : 'var(--ink)' }}>{l.score}
+
+
+ +
+

Activity Timeline

+
+ {A.activity.map((a, i) => { + const c = COL[a.color]; + return ( +
+ {i !== A.activity.length - 1 && } + +
+
{a.title}
+
{a.desc}
+
{a.time}
+
+
+ ); + })} +
+
+
+
+ ); + } + + function PropertyCard({ p, go }) { + const statusColor = { 'For Sale': 'green', 'Under Offer': 'gold', 'Sold': 'gray' }[p.status] || 'blue'; + return ( +
go('properties')}> + + {p.tag ? {p.tag === 'Featured' ? : }{p.tag} : } + +
}> +
+ {p.status} +
+ +
+
+

{p.name}

+
+
{p.loc}
+
{A.fmt(p.price)}
+
+ {p.beds} + {p.baths} + {p.area} m² + {p.views.toLocaleString()} +
+
+
+ ); + } + + window.LuxDashboard = Dashboard; + window.LuxPropertyCard = PropertyCard; +})(); diff --git a/src/Yammuna Express/lux-data.js b/src/Yammuna Express/lux-data.js new file mode 100644 index 0000000..3ee3df1 --- /dev/null +++ b/src/Yammuna Express/lux-data.js @@ -0,0 +1,98 @@ +/* Aurum Estates — mock data */ +window.AURUM = (function () { + const fmt = (n) => '$' + n.toLocaleString('en-US'); + const fmtK = (n) => n >= 1e6 ? '$' + (n / 1e6).toFixed(2) + 'M' : n >= 1e3 ? '$' + (n / 1e3).toFixed(0) + 'K' : '$' + n; + + const kpis = [ + { label: 'Total Revenue', value: '$48.6M', delta: '+12.4%', up: true, icon: 'tag', spark: [22, 28, 26, 34, 31, 40, 38, 46, 44, 52], color: 'blue' }, + { label: 'Active Projects', value: '34', delta: '+5', up: true, icon: 'briefcase', spark: [8, 10, 9, 12, 14, 13, 16, 15, 18, 20], color: 'navy' }, + { label: 'Property Listings', value: '186', delta: '+18', up: true, icon: 'building', spark: [120, 130, 128, 140, 150, 158, 162, 170, 178, 186], color: 'gold' }, + { label: 'New Leads', value: '512', delta: '+9.1%', up: true, icon: 'users', spark: [30, 42, 38, 50, 47, 60, 58, 70, 66, 80], color: 'green' }, + ]; + + const subKpis = [ + { label: 'Client Growth', value: '+24%', sub: 'vs last quarter', icon: 'trend', color: 'green' }, + { label: 'Monthly Sales', value: '$6.2M', sub: '42 deals closed', icon: 'tag', color: 'blue' }, + { label: 'Occupancy Rate', value: '92%', sub: 'across managed units', icon: 'building', color: 'navy' }, + { label: 'Conversion Rate', value: '18.4%', sub: 'lead → booking', icon: 'funnel', color: 'gold' }, + ]; + + const properties = [ + { id: 'P-2041', name: 'Villa Serenissima', loc: 'Lake Como, Italy', price: 12400000, area: 820, beds: 6, baths: 7, status: 'For Sale', tag: 'Featured', views: 1820, slot: 'prop1', accent: '#6a8fc9' }, + { id: 'P-2038', name: 'The Glass Pavilion', loc: 'Beverly Hills, CA', price: 28900000, area: 1140, beds: 8, baths: 10, status: 'For Sale', tag: 'New', views: 3210, slot: 'prop2', accent: '#7a8b7e' }, + { id: 'P-2035', name: 'Marina Sky Penthouse', loc: 'Dubai Marina, UAE', price: 9650000, area: 540, beds: 4, baths: 5, status: 'Under Offer', tag: '', views: 2440, slot: 'prop3', accent: '#c9a86a' }, + { id: 'P-2030', name: 'Cliffside Retreat', loc: 'Amalfi Coast, Italy', price: 7300000, area: 460, beds: 5, baths: 5, status: 'For Sale', tag: '', views: 1290, slot: 'prop4', accent: '#9c8398' }, + { id: 'P-2027', name: 'Aspen Timber Lodge', loc: 'Aspen, Colorado', price: 15800000, area: 690, beds: 6, baths: 8, status: 'Sold', tag: '', views: 980, slot: 'prop5', accent: '#8a7d6b' }, + { id: 'P-2021', name: 'Azure Bay Estate', loc: 'Côte d\u2019Azur, France', price: 21200000, area: 970, beds: 7, baths: 9, status: 'For Sale', tag: 'Featured', views: 2760, slot: 'prop6', accent: '#5d8aa8' }, + ]; + + const projects = [ + { name: 'Riviera Residences', loc: 'Monaco', units: 48, sold: 31, progress: 78, status: 'Construction', value: 142000000 }, + { name: 'The Onyx Towers', loc: 'Singapore', units: 120, sold: 86, progress: 64, status: 'Construction', value: 310000000 }, + { name: 'Palm Grove Villas', loc: 'Maldives', units: 24, sold: 24, progress: 100, status: 'Completed', value: 96000000 }, + { name: 'Hillcrest Manors', loc: 'Tuscany, Italy', units: 36, sold: 12, progress: 28, status: 'Planning', value: 78000000 }, + ]; + + const leads = [ + { name: 'Eleanor Whitmore', type: 'Site Visit', interest: 'Villa Serenissima', stage: 'Qualified', score: 92, when: '10 min ago', avatar: 'EW' }, + { name: 'Rajesh Kapoor', type: 'Inquiry', interest: 'Marina Sky Penthouse', stage: 'New', score: 68, when: '1 hr ago', avatar: 'RK' }, + { name: 'Sofia Marchetti', type: 'Contact Request', interest: 'Azure Bay Estate', stage: 'Contacted', score: 80, when: '3 hr ago', avatar: 'SM' }, + { name: 'James Okafor', type: 'Site Visit', interest: 'The Glass Pavilion', stage: 'Negotiation', score: 88, when: 'Yesterday', avatar: 'JO' }, + { name: 'Mei Lin Chen', type: 'Inquiry', interest: 'Cliffside Retreat', stage: 'New', score: 55, when: 'Yesterday', avatar: 'MC' }, + ]; + + const pipeline = [ + { stage: 'New Inquiries', count: 184, color: '#0D6EFD' }, + { stage: 'Contacted', count: 126, color: '#3b86ff' }, + { stage: 'Qualified', count: 78, color: '#7AC943' }, + { stage: 'Negotiation', count: 41, color: '#c9a86a' }, + { stage: 'Closed Won', count: 27, color: '#102A43' }, + ]; + + const bookings = [ + { client: 'Eleanor Whitmore', property: 'Villa Serenissima', date: '12 Jun 2026', type: 'Site Visit', status: 'Confirmed' }, + { client: 'James Okafor', property: 'The Glass Pavilion', date: '13 Jun 2026', type: 'Closing', status: 'Confirmed' }, + { client: 'Sofia Marchetti', property: 'Azure Bay Estate', date: '14 Jun 2026', type: 'Site Visit', status: 'Pending' }, + { client: 'Rajesh Kapoor', property: 'Marina Sky Penthouse', date: '16 Jun 2026', type: 'Consultation', status: 'Confirmed' }, + ]; + + const clients = [ + { name: 'Eleanor Whitmore', tier: 'Platinum', deals: 3, value: 34000000, since: '2021', avatar: 'EW' }, + { name: 'James Okafor', tier: 'Gold', deals: 2, value: 28900000, since: '2022', avatar: 'JO' }, + { name: 'Sofia Marchetti', tier: 'Gold', deals: 1, value: 21200000, since: '2023', avatar: 'SM' }, + { name: 'Rajesh Kapoor', tier: 'Silver', deals: 1, value: 9650000, since: '2024', avatar: 'RK' }, + ]; + + const payments = [ + { id: 'INV-7841', client: 'Eleanor Whitmore', property: 'Villa Serenissima', amount: 1240000, date: '05 Jun 2026', status: 'Paid', method: 'Wire' }, + { id: 'INV-7836', client: 'James Okafor', property: 'The Glass Pavilion', amount: 2890000, date: '03 Jun 2026', status: 'Paid', method: 'Escrow' }, + { id: 'INV-7829', client: 'Sofia Marchetti', property: 'Azure Bay Estate', amount: 2120000, date: '01 Jun 2026', status: 'Pending', method: 'Wire' }, + { id: 'INV-7820', client: 'Rajesh Kapoor', property: 'Marina Sky Penthouse', amount: 965000, date: '28 May 2026', status: 'Overdue', method: 'Escrow' }, + { id: 'INV-7812', client: 'Mei Lin Chen', property: 'Cliffside Retreat', amount: 730000, date: '24 May 2026', status: 'Paid', method: 'Wire' }, + ]; + + const activity = [ + { icon: 'tag', color: 'green', title: 'Deal closed', desc: 'The Glass Pavilion — $28.9M', time: '2h ago' }, + { icon: 'users', color: 'blue', title: 'New lead assigned', desc: 'Eleanor Whitmore · Villa Serenissima', time: '4h ago' }, + { icon: 'building', color: 'gold', title: 'Listing published', desc: 'Azure Bay Estate, Côte d\u2019Azur', time: '8h ago' }, + { icon: 'pin', color: 'navy', title: 'Site visit booked', desc: 'James Okafor · tomorrow 11:00', time: 'Yesterday' }, + { icon: 'star', color: 'gold', title: 'New 5-star review', desc: '"Impeccable service" — S. Marchetti', time: '2d ago' }, + ]; + + const reviews = [ + { name: 'Sofia Marchetti', stars: 5, text: 'Impeccable from first viewing to closing. The team understood exactly what we wanted.', when: '2 days ago', avatar: 'SM' }, + { name: 'James Okafor', stars: 5, text: 'A seamless, discreet process for a complex acquisition. Highly recommended.', when: '1 week ago', avatar: 'JO' }, + { name: 'Eleanor Whitmore', stars: 4, text: 'Beautiful portfolio and attentive agents. Closing took slightly longer than expected.', when: '2 weeks ago', avatar: 'EW' }, + ]; + + const blogs = [ + { title: 'The 2026 Luxury Market Outlook', cat: 'Market Insights', date: '04 Jun 2026', status: 'Published', reads: 4820 }, + { title: 'Designing the Modern Glass Villa', cat: 'Architecture', date: '28 May 2026', status: 'Published', reads: 3140 }, + { title: 'Coastal Investment Hotspots', cat: 'Investment', date: '20 May 2026', status: 'Draft', reads: 0 }, + ]; + + const revenueSeries = [3.1, 3.6, 3.3, 4.1, 4.4, 4.0, 4.8, 5.1, 4.9, 5.6, 6.0, 6.2]; + const bookingTrend = [12, 18, 15, 22, 26, 21, 30, 34, 29, 38, 42, 46]; + + return { fmt, fmtK, kpis, subKpis, properties, projects, leads, pipeline, bookings, clients, payments, activity, reviews, blogs, revenueSeries, bookingTrend }; +})(); diff --git a/src/Yammuna Express/lux-pages.jsx b/src/Yammuna Express/lux-pages.jsx new file mode 100644 index 0000000..b84c8d9 --- /dev/null +++ b/src/Yammuna Express/lux-pages.jsx @@ -0,0 +1,207 @@ +/* Aurum Estates — Properties, Projects, Clients, Leads, Bookings */ +(function () { + const { useState } = React; + const Icon = window.Icon, A = window.AURUM; + const { LuxAvatar, LuxPageHead, LuxPropertyCard, AreaChart, Bars } = window; + + /* ---------------- PROPERTIES ---------------- */ + const PFILTERS = ['All', 'For Sale', 'Under Offer', 'Sold', 'Featured']; + function Properties({ go }) { + const [f, setF] = useState('All'); + const list = A.properties.filter(p => f === 'All' || p.status === f || (f === 'Featured' && p.tag === 'Featured')); + return ( +
+ + + + +
+
{PFILTERS.map(x => )}
+
+
+
+ {list.map(p => )} +
+
+ ); + } + + /* ---------------- PROJECTS ---------------- */ + function Projects() { + return ( +
+ + + +
+ {A.projects.map(p => { + const sc = { Construction: 'blue', Completed: 'green', Planning: 'gold' }[p.status]; + return ( +
+
+
+ +

{p.name}

{p.loc}
+
+ {p.status} +
+
+ {p.sold} of {p.units} units sold + {p.progress}% +
+
+
+
GDV
{A.fmtK(p.value)}
+ +
+
+ ); + })} +
+
+ ); + } + + /* ---------------- CLIENTS ---------------- */ + function Clients() { + const tierColor = { Platinum: 'navy', Gold: 'gold', Silver: 'gray' }; + return ( +
+ + + +
+ + + + {A.clients.map(c => ( + + + + + + + + + ))} + +
ClientTierDealsLifetime ValueClient Since
{c.name}
{c.tier === 'Platinum' && }{c.tier}{c.deals}{A.fmtK(c.value)}{c.since}
+
+
+ ); + } + + /* ---------------- LEADS ---------------- */ + function Leads() { + return ( +
+ + + + + + {/* pipeline funnel */} +
+

Lead Conversion Funnel

+
+ {A.pipeline.map((s, i) => ( +
+
+
{s.count}
+
{s.stage}
+
+ {i < A.pipeline.length - 1 &&
{Math.round(A.pipeline[i + 1].count / s.count * 100)}% →
} +
+ ))} +
+
+ + {/* leads table */} +
+
+

All Leads

+
+
+ + + + {A.leads.map(l => ( + + + + + + + + + ))} + +
LeadSourceInterestStageFollow-upScore
{l.name}
{l.when}
{l.type}{l.interest}{l.stage}{l.stage === 'New' ? 'Call today' : 'Email sent'} +
+
= 85 ? 'var(--green)' : 'var(--blue)' }} />
+ {l.score} +
+
+
+
+ ); + } + + /* ---------------- BOOKINGS ---------------- */ + function Bookings() { + return ( +
+ + + +
+
+

Upcoming

+ + + + {A.bookings.map(b => ( + + + + + + + + ))} + +
ClientPropertyTypeDateStatus
x[0]).join('')} size={34} />{b.client}
{b.property}{b.type}{b.date}{b.status}
+
+
+

June 2026

+ +
+
+
+ ); + } + + function MiniCalendar() { + const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const marked = { 12: true, 13: true, 14: true, 16: true }; + const cells = []; + for (let i = 0; i < 1; i++) cells.push(null); + for (let d = 1; d <= 30; d++) cells.push(d); + return ( +
+
+ {days.map((d, i) =>
{d}
)} +
+
+ {cells.map((d, i) => ( +
+ {d}{marked[d] && d !== 12 && } +
+ ))} +
+
+ ); + } + + Object.assign(window, { LuxProperties: Properties, LuxProjects: Projects, LuxClients: Clients, LuxLeads: Leads, LuxBookings: Bookings }); +})(); diff --git a/src/Yammuna Express/lux-pages2.jsx b/src/Yammuna Express/lux-pages2.jsx new file mode 100644 index 0000000..e7fd6c2 --- /dev/null +++ b/src/Yammuna Express/lux-pages2.jsx @@ -0,0 +1,265 @@ +/* Aurum Estates — Analytics, Payments, Blogs, Media, Reviews, Reports, Settings */ +(function () { + const { useState } = React; + const Icon = window.Icon, A = window.AURUM; + const { LuxAvatar, LuxPageHead, AreaChart, Bars, LuxDonut, Stars, PropImage } = window; + + /* ---------------- ANALYTICS ---------------- */ + function Analytics() { + return ( +
+ + + + + +
+ {A.subKpis.map(s => { + const c = { blue: ['var(--blue-soft)', 'var(--blue)'], navy: ['#e6edf4', 'var(--navy)'], gold: ['var(--gold-soft)', '#9c7b3a'], green: ['var(--green-soft)', 'var(--green-600)'] }[s.color]; + return ( +
+ +
{s.value}
+
{s.label}
+
{s.sub}
+
+ ); + })} +
+ +
+
+

Revenue Growth

+

Monthly revenue · $M

+ +
+
+

Conversion

+

Lead → booking rate

+
+
+ {[['Lead → Qualified', 42], ['Qualified → Visit', 61], ['Visit → Offer', 38]].map(([l, v]) => ( +
{l}{v}%
+ ))} +
+
+
+ +
+
+

Booking Trends

+

Monthly confirmed bookings

+ +
+
+

Property Performance

+
+ {A.properties.slice(0, 5).map(p => ( +
+
+
+
{p.name}{p.views.toLocaleString()} views
+
+
+
+ ))} +
+
+
+
+ ); + } + + /* ---------------- PAYMENTS ---------------- */ + function Payments() { + const total = A.payments.reduce((s, p) => s + p.amount, 0); + const paid = A.payments.filter(p => p.status === 'Paid').reduce((s, p) => s + p.amount, 0); + return ( +
+ + + + +
+ {[['Total Invoiced', A.fmtK(total), 'blue', 'tag'], ['Collected', A.fmtK(paid), 'green', 'check'], ['Outstanding', A.fmtK(total - paid), 'gold', 'clock']].map(([l, v, c, ic]) => { + const cc = { blue: ['var(--blue-soft)', 'var(--blue)'], green: ['var(--green-soft)', 'var(--green-600)'], gold: ['var(--gold-soft)', '#9c7b3a'] }[c]; + return ( +
+ +
{v}
+
{l}
+
+ ); + })} +
+
+

Transactions

+ + + + {A.payments.map(p => ( + + + + + + + + + + ))} + +
InvoiceClientPropertyMethodDateStatusAmount
{p.id}{p.client}{p.property}{p.method}{p.date}{p.status}{A.fmt(p.amount)}
+
+
+ ); + } + + /* ---------------- BLOGS ---------------- */ + function Blogs() { + return ( +
+ + + +
+ {A.blogs.map((b, i) => ( +
+
+ {b.cat} +
+
+ {b.status} +

{b.title}

+
{b.date}{b.reads > 0 && {b.reads.toLocaleString()} reads}
+
+
+ ))} +
+
+ ); + } + + /* ---------------- MEDIA LIBRARY ---------------- */ + function Media() { + const accents = ['#6a8fc9', '#7a8b7e', '#c9a86a', '#9c8398', '#8a7d6b', '#5d8aa8', '#7e9e8b', '#a98b6b']; + return ( +
+ + + +
+ {accents.map((a, i) => ( +
+ +
+
villa_render_{i + 1}.jpg
4.2 MB · 4K
+ +
+
+ ))} +
+
+ ); + } + + /* ---------------- REVIEWS ---------------- */ + function Reviews() { + return ( +
+ +
+
4.9
+
+
+
+ {A.reviews.map(r => ( +
+
+
{r.name}
{r.when}
+ +
+

"{r.text}"

+
+ ))} +
+
+ ); + } + + /* ---------------- REPORTS ---------------- */ + function Reports() { + const reports = [ + ['Q2 2026 Portfolio Performance', 'Financial', '12 pages'], + ['Lead Source Attribution', 'Marketing', '8 pages'], + ['Market Valuation Index — June', 'Market', '20 pages'], + ['Agent Productivity Summary', 'Operations', '6 pages'], + ]; + return ( +
+ + + +
+ + + + {reports.map(([t, c, l]) => ( + + + + + + + ))} + +
ReportCategoryLength
{t}
{c}{l}
+
+
+ ); + } + + /* ---------------- SETTINGS ---------------- */ + function Settings() { + const [tab, setTab] = useState('profile'); + const tabs = [['profile', 'Profile', 'user'], ['company', 'Company', 'building'], ['team', 'Team', 'users'], ['billing', 'Billing', 'tag'], ['notifications', 'Notifications', 'bell']]; + return ( +
+ +
+
+ {tabs.map(([k, l, ic]) => ( + + ))} +
+
+ {tab === 'profile' ? ( +
+
+ +

Alexander Vale

Managing Director · Aurum Estates
+
+
+ {[['Full Name', 'Alexander Vale', 'user'], ['Email', 'a.vale@aurumestates.com', 'mail'], ['Phone', '+1 310 555 0148', 'phone'], ['Role', 'Managing Director', 'briefcase']].map(([l, v, ic]) => ( +
+ ))} +
+
+
+ ) : ( +
+ t[0] === tab)[2]} size={27} /> +

{tabs.find(t => t[0] === tab)[1]} settings

+

This section is part of the configuration suite.

+
+ )} +
+
+
+ ); + } + + Object.assign(window, { LuxAnalytics: Analytics, LuxPayments: Payments, LuxBlogs: Blogs, LuxMedia: Media, LuxReviews: Reviews, LuxReports: Reports, LuxSettings: Settings }); +})(); diff --git a/src/Yammuna Express/lux-shell.jsx b/src/Yammuna Express/lux-shell.jsx new file mode 100644 index 0000000..127a2f8 --- /dev/null +++ b/src/Yammuna Express/lux-shell.jsx @@ -0,0 +1,88 @@ +/* Aurum Estates — app shell: collapsible sidebar + glass sticky header */ +(function () { + const { useState } = React; + const Icon = window.Icon, Logo = window.LuxLogo, Avatar = window.LuxAvatar; + + const NAV = [ + { k: 'dashboard', label: 'Dashboard', icon: 'dashboard' }, + { k: 'properties', label: 'Properties', icon: 'building' }, + { k: 'projects', label: 'Projects', icon: 'briefcase' }, + { k: 'clients', label: 'Clients', icon: 'users' }, + { k: 'leads', label: 'Leads', icon: 'funnel', badge: '12' }, + { k: 'bookings', label: 'Bookings', icon: 'calendar' }, + { k: 'payments', label: 'Payments', icon: 'tag' }, + { k: 'blogs', label: 'Blogs', icon: 'megaphone' }, + { k: 'media', label: 'Media Library', icon: 'image' }, + { k: 'reviews', label: 'Reviews', icon: 'star' }, + { k: 'analytics', label: 'Analytics', icon: 'chartBar' }, + { k: 'reports', label: 'Reports', icon: 'documents' }, + { k: 'settings', label: 'Settings', icon: 'settings' }, + ]; + + function Shell({ route, go, children, toastNode }) { + const [collapsed, setCollapsed] = useState(false); + const w = collapsed ? 'var(--sidebar-collapsed)' : 'var(--sidebar)'; + return ( +
+ {/* Sidebar */} + + + {/* Main column */} +
+ {/* glass header */} +
+ +
+ + + ⌘K +
+
+ + + + +
+ +
+
+ +
+
{children}
+
+
+ {toastNode} +
+ ); + } + + window.LuxShell = Shell; +})(); diff --git a/src/Yammuna Express/lux-ui.jsx b/src/Yammuna Express/lux-ui.jsx new file mode 100644 index 0000000..7c5175b --- /dev/null +++ b/src/Yammuna Express/lux-ui.jsx @@ -0,0 +1,150 @@ +/* Aurum Estates — shared UI: logo, charts, property image, helpers */ +(function () { + const { useState, useEffect, useRef } = React; + const Icon = window.Icon; + + function Logo({ collapsed, light }) { + const fg = light ? '#fff' : 'var(--navy)'; + return React.createElement('div', { className: 'row gap-3', style: { minWidth: 0 } }, + React.createElement('div', { style: { width: 40, height: 40, borderRadius: 12, flex: 'none', display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,#0D6EFD,#102A43)', boxShadow: '0 8px 18px -6px rgba(13,110,253,.6)' } }, + React.createElement('svg', { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none' }, + React.createElement('path', { d: 'M4 21V10l8-6 8 6v11', stroke: '#fff', strokeWidth: 1.7, strokeLinejoin: 'round', strokeLinecap: 'round' }), + React.createElement('path', { d: 'M12 4l8 6', stroke: '#fff', strokeWidth: 1.7, strokeLinecap: 'round' }), + React.createElement('rect', { x: 10, y: 13, width: 4, height: 8, fill: '#7AC943' }))), + !collapsed && React.createElement('div', { style: { lineHeight: 1.05, minWidth: 0 } }, + React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 19, color: fg, letterSpacing: '.01em' } }, 'AURUM'), + React.createElement('div', { style: { fontSize: 10.5, fontWeight: 600, letterSpacing: '.22em', color: light ? 'rgba(174,187,207,.9)' : 'var(--muted)', textTransform: 'uppercase' } }, 'Estates'))); + } + + function Avatar({ initials, size = 38, style }) { + return React.createElement('div', { className: 'avatar', style: { width: size, height: size, fontSize: size * 0.38, ...style } }, initials); + } + + function Sparkline({ data, w = 120, h = 40, color = 'var(--blue)', fill = true }) { + const max = Math.max(...data), min = Math.min(...data), range = max - min || 1; + const stepX = w / (data.length - 1); + const pts = data.map((d, i) => [i * stepX, h - 4 - (h - 8) * (d - min) / range]); + const line = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); + const area = `${line} L${w},${h} L0,${h} Z`; + const id = 'sg' + Math.random().toString(36).slice(2, 7); + return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: w, height: h, preserveAspectRatio: 'none', style: { display: 'block', overflow: 'visible' } }, + React.createElement('defs', null, React.createElement('linearGradient', { id, x1: 0, y1: 0, x2: 0, y2: 1 }, + React.createElement('stop', { offset: '0%', stopColor: color, stopOpacity: .2 }), + React.createElement('stop', { offset: '100%', stopColor: color, stopOpacity: 0 }))), + fill && React.createElement('path', { d: area, fill: `url(#${id})` }), + React.createElement('path', { d: line, fill: 'none', stroke: color, strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' })); + } + + function AreaChart({ data, w = 640, h = 220, color = 'var(--blue)', labels }) { + const max = Math.max(...data) * 1.1, min = 0, range = max - min || 1; + const padL = 4, padB = 22; + const stepX = (w - padL) / (data.length - 1); + const pts = data.map((d, i) => [padL + i * stepX, (h - padB) * (1 - (d - min) / range) + 4]); + const line = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); + const area = `${line} L${pts[pts.length - 1][0]},${h - padB} L${pts[0][0]},${h - padB} Z`; + const ref = useRef(null); + useEffect(() => { const el = ref.current; if (el) { const len = el.getTotalLength(); el.style.strokeDasharray = len; el.style.strokeDashoffset = len; el.getBoundingClientRect(); el.style.transition = 'stroke-dashoffset 1.2s cubic-bezier(.3,.7,.3,1)'; el.style.strokeDashoffset = 0; } }, []); + const id = 'ac' + Math.random().toString(36).slice(2, 7); + return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, preserveAspectRatio: 'none', style: { display: 'block', overflow: 'visible' } }, + React.createElement('defs', null, React.createElement('linearGradient', { id, x1: 0, y1: 0, x2: 0, y2: 1 }, + React.createElement('stop', { offset: '0%', stopColor: color, stopOpacity: .22 }), + React.createElement('stop', { offset: '100%', stopColor: color, stopOpacity: 0 }))), + [0.25, 0.5, 0.75, 1].map((f, i) => React.createElement('line', { key: i, x1: 0, y1: (h - padB) * f, x2: w, y2: (h - padB) * f, stroke: '#eef1f6', strokeWidth: 1 })), + React.createElement('path', { d: area, fill: `url(#${id})` }), + React.createElement('path', { ref, d: line, fill: 'none', stroke: color, strokeWidth: 2.6, strokeLinecap: 'round', strokeLinejoin: 'round' }), + pts.map((p, i) => React.createElement('circle', { key: i, cx: p[0], cy: p[1], r: i === pts.length - 1 ? 4.5 : 0, fill: '#fff', stroke: color, strokeWidth: 2.6 })), + labels && labels.map((l, i) => React.createElement('text', { key: i, x: padL + i * ((w - padL) / (labels.length - 1)), y: h - 4, fontSize: 11, fontWeight: 600, fill: 'var(--faint)', textAnchor: i === 0 ? 'start' : i === labels.length - 1 ? 'end' : 'middle' }, l))); + } + + function Bars({ data, w = 300, h = 110, color = 'var(--blue)', labels }) { + const max = Math.max(...data) || 1; + const gap = 8, bw = (w - gap * (data.length - 1)) / data.length; + return React.createElement('svg', { viewBox: `0 0 ${w} ${h + 18}`, width: '100%', height: h + 18, style: { display: 'block' } }, + data.map((d, i) => { + const bh = Math.max(4, (d / max) * h); + return React.createElement('g', { key: i }, + React.createElement('rect', { x: i * (bw + gap), y: h - bh, width: bw, height: bh, rx: 5, fill: i === data.length - 1 ? color : '#dbe7fb', style: { transformOrigin: `center ${h}px`, animation: `growBar .6s ${i * 0.04}s both` } }), + labels && React.createElement('text', { x: i * (bw + gap) + bw / 2, y: h + 14, fontSize: 10.5, fontWeight: 600, fill: 'var(--faint)', textAnchor: 'middle' }, labels[i])); + })); + } + + function Donut({ value, size = 130, stroke = 13, color = 'var(--blue)', track = '#eef1f6', label, sub }) { + const r = (size - stroke) / 2, c = 2 * Math.PI * r; + const target = c - (value / 100) * c; + // start at final value when hidden (preview/print) so the arc is always visible + const [off, setOff] = useState(typeof document !== 'undefined' && document.hidden ? target : c); + useEffect(() => { const t = setTimeout(() => setOff(target), 120); return () => clearTimeout(t); }, [target]); + return React.createElement('div', { style: { position: 'relative', width: size, height: size } }, + React.createElement('svg', { width: size, height: size, style: { transform: 'rotate(-90deg)' } }, + React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: track, strokeWidth: stroke }), + React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: color, strokeWidth: stroke, strokeLinecap: 'round', strokeDasharray: c, strokeDashoffset: off, style: { transition: 'stroke-dashoffset 1.1s cubic-bezier(.3,.7,.3,1)' } })), + React.createElement('div', { style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center' } }, + React.createElement('div', null, + React.createElement('div', { className: 'serif', style: { fontWeight: 800, fontSize: size * 0.24 } }, label != null ? label : value + '%'), + sub && React.createElement('div', { style: { fontSize: 11.5, color: 'var(--muted)', fontWeight: 600 } }, sub)))); + } + + // Property image: uses so user can drop a real photo; styled placeholder by default + function PropImage({ slot, accent, height = 200, radius = 16, placeholder = 'Drop property photo', children, badge }) { + const ref = useRef(null); + useEffect(() => { + // inject a tasteful default placeholder background into the slot's empty state + }, []); + return React.createElement('div', { style: { position: 'relative', height, borderRadius: radius, overflow: 'hidden' } }, + React.createElement('div', { style: { position: 'absolute', inset: 0, background: `linear-gradient(150deg, ${accent || '#6a8fc9'}, ${shade(accent || '#6a8fc9', -28)})` } }, + React.createElement(Architecture, { accent })), + React.createElement('image-slot', { ref, id: slot, shape: 'rect', placeholder, style: { position: 'absolute', inset: 0, width: '100%', height: '100%' } }), + badge, + children); + } + + // simple architectural silhouette as default placeholder content + function Architecture({ accent }) { + return React.createElement('svg', { viewBox: '0 0 400 220', width: '100%', height: '100%', preserveAspectRatio: 'xMidYMid slice', style: { position: 'absolute', inset: 0, opacity: .9 } }, + React.createElement('rect', { width: 400, height: 220, fill: 'none' }), + // sky glow + React.createElement('circle', { cx: 320, cy: 40, r: 60, fill: 'rgba(255,255,255,.12)' }), + // villa silhouette + React.createElement('rect', { x: 70, y: 110, width: 160, height: 90, fill: 'rgba(255,255,255,.16)' }), + React.createElement('rect', { x: 230, y: 80, width: 90, height: 120, fill: 'rgba(255,255,255,.22)' }), + React.createElement('rect', { x: 95, y: 130, width: 26, height: 26, fill: 'rgba(255,255,255,.3)' }), + React.createElement('rect', { x: 132, y: 130, width: 26, height: 26, fill: 'rgba(255,255,255,.3)' }), + React.createElement('rect', { x: 250, y: 100, width: 22, height: 30, fill: 'rgba(255,255,255,.34)' }), + React.createElement('rect', { x: 282, y: 100, width: 22, height: 30, fill: 'rgba(255,255,255,.34)' }), + // waterline + React.createElement('rect', { x: 0, y: 200, width: 400, height: 20, fill: 'rgba(255,255,255,.1)' }), + React.createElement('path', { d: 'M0 200 Q100 192 200 200 T400 200', stroke: 'rgba(255,255,255,.25)', strokeWidth: 2, fill: 'none' }), + React.createElement('foreignObject', { x: 0, y: 0, width: 400, height: 220 })); + } + + function shade(hex, amt) { + const n = parseInt(hex.slice(1), 16); + let r = (n >> 16) + amt, g = ((n >> 8) & 0xff) + amt, b = (n & 0xff) + amt; + r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b)); + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + } + + function PageHead({ title, kicker, subtitle, children }) { + return React.createElement('div', { className: 'row spread wrap', style: { gap: 16, marginBottom: 26 } }, + React.createElement('div', null, + kicker && React.createElement('div', { className: 'kicker', style: { marginBottom: 8 } }, kicker), + React.createElement('h1', { style: { fontSize: 30, fontWeight: 800 } }, title), + subtitle && React.createElement('p', { className: 'muted', style: { fontSize: 14.5, marginTop: 6 } }, subtitle)), + children && React.createElement('div', { className: 'row gap-3' }, children)); + } + + function useToast() { + const [toasts, setToasts] = useState([]); + const push = (msg, kind = 'success') => { const id = Math.random(); setToasts(t => [...t, { id, msg, kind }]); setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3000); }; + const node = React.createElement('div', { style: { position: 'fixed', bottom: 24, right: 24, display: 'flex', flexDirection: 'column', gap: 10, zIndex: 9000 } }, + toasts.map(t => React.createElement('div', { key: t.id, className: 'anim-pop', style: { display: 'flex', alignItems: 'center', gap: 11, padding: '13px 17px', borderRadius: 13, background: 'var(--navy)', color: '#fff', boxShadow: 'var(--sh-lg)', fontSize: 14, fontWeight: 500, maxWidth: 360 } }, + React.createElement('span', { style: { display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 99, background: 'var(--green)', flex: 'none', color: '#173a05' } }, React.createElement(Icon, { name: 'check', size: 14 })), t.msg))); + return [node, push]; + } + + function Stars({ n, size = 14 }) { + return React.createElement('div', { className: 'row gap-1' }, [0, 1, 2, 3, 4].map(i => React.createElement(Icon, { key: i, name: 'star', size, style: { color: i < n ? 'var(--gold)' : '#dfe4ec', fill: i < n ? 'var(--gold)' : '#dfe4ec' } }))); + } + + Object.assign(window, { LuxLogo: Logo, LuxAvatar: Avatar, Sparkline, AreaChart, Bars, LuxDonut: Donut, PropImage, LuxPageHead: PageHead, useLuxToast: useToast, Stars }); +})(); diff --git a/src/Yammuna Express/lux.css b/src/Yammuna Express/lux.css new file mode 100644 index 0000000..2255e75 --- /dev/null +++ b/src/Yammuna Express/lux.css @@ -0,0 +1,203 @@ +/* ============================================================ + Aurum Estates — Luxury Real Estate Dashboard + Playfair Display (display) + Inter (UI) · glassmorphism · blue/navy + ============================================================ */ +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,500;0,600;0,700;0,800;0,900;1,500&family=Inter:wght@400;500;600;700&display=swap'); + +:root { + --blue: #0D6EFD; + --blue-600: #0a59d4; + --blue-700: #0848a8; + --blue-soft: #e7f0ff; + --blue-50: #f2f7ff; + --navy: #102A43; + --navy-700: #1c3e5e; + --black: #111827; + --green: #7AC943; + --green-600: #62a934; + --green-soft:#eaf7df; + --gold: #c9a86a; + --gold-soft: #f6efe1; + + --bg: #F7F9FC; + --white: #FFFFFF; + --border:#E5E7EB; + --border-soft:#eef1f5; + + --ink: #111827; + --ink-2: #374151; + --muted: #6b7689; + --faint: #98a2b3; + + --font-display: 'Playfair Display', Georgia, serif; + --font-ui: 'Inter', system-ui, -apple-system, sans-serif; + + --r: 14px; + --r-lg: 20px; + --r-xl: 26px; + + --sh-xs: 0 1px 2px rgba(16,42,67,.05); + --sh-sm: 0 2px 6px rgba(16,42,67,.06), 0 1px 2px rgba(16,42,67,.04); + --sh-md: 0 12px 28px -10px rgba(16,42,67,.16), 0 4px 10px -4px rgba(16,42,67,.08); + --sh-lg: 0 30px 60px -22px rgba(16,42,67,.30), 0 10px 24px -12px rgba(16,42,67,.16); + --sh-blue: 0 14px 30px -10px rgba(13,110,253,.45); + + --sidebar: 264px; + --sidebar-collapsed: 80px; + --header: 72px; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: var(--font-ui); color: var(--ink); background: var(--bg); + -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; +} +#root { min-height: 100vh; } +h1,h2,h3,h4 { font-family: var(--font-display); margin: 0; color: var(--ink); letter-spacing: -.01em; font-weight: 700; } +p { margin: 0; } +button { font-family: inherit; cursor: pointer; } +a { color: inherit; text-decoration: none; } +input, select, textarea { font-family: inherit; } +::selection { background: var(--blue-soft); } +.num { font-variant-numeric: tabular-nums; } +.serif { font-family: var(--font-display); } +.muted { color: var(--muted); } +.faint { color: var(--faint); } + +*::-webkit-scrollbar { width: 10px; height: 10px; } +*::-webkit-scrollbar-thumb { background: #d3dae5; border-radius: 99px; border: 2px solid transparent; background-clip: padding-box; } +*::-webkit-scrollbar-thumb:hover { background: #bcc6d6; background-clip: padding-box; } +*::-webkit-scrollbar-track { background: transparent; } + +/* layout helpers */ +.row { display: flex; align-items: center; } +.col { display: flex; flex-direction: column; } +.spread { justify-content: space-between; } +.center { align-items: center; justify-content: center; } +.grow { flex: 1; } +.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.gap-5{gap:20px}.gap-6{gap:24px} +.wrap { flex-wrap: wrap; } + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 9px; + font-weight: 600; font-size: 14px; padding: 11px 18px; border-radius: 11px; + border: 1px solid transparent; background: transparent; color: var(--ink); white-space: nowrap; line-height: 1; + transition: transform .12s ease, box-shadow .18s, background .18s, border-color .18s, color .15s; +} +.btn:active { transform: translateY(1px); } +.btn svg { width: 18px; height: 18px; flex: none; } +.btn-primary { background: var(--blue); color: #fff; box-shadow: var(--sh-blue); } +.btn-primary:hover { background: var(--blue-600); box-shadow: 0 18px 36px -10px rgba(13,110,253,.55); } +.btn-navy { background: var(--navy); color: #fff; } +.btn-navy:hover { background: var(--navy-700); } +.btn-dark { background: var(--black); color: #fff; } +.btn-dark:hover { filter: brightness(1.25); } +.btn-outline { background: #fff; border-color: var(--border); color: var(--ink-2); box-shadow: var(--sh-xs); } +.btn-outline:hover { border-color: #cdd5e0; background: #fcfdff; } +.btn-ghost { color: var(--muted); } +.btn-ghost:hover { background: #f0f3f8; color: var(--ink); } +.btn-sm { padding: 8px 13px; font-size: 13px; border-radius: 9px; } +.btn-sm svg { width: 15px; height: 15px; } +.btn-lg { padding: 14px 24px; font-size: 15px; border-radius: 13px; } +.btn-block { width: 100%; } + +/* ---------- Cards ---------- */ +.card { background: #fff; border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--sh-sm); } +.card-pad { padding: 24px; } +.card-hover { transition: box-shadow .22s, transform .22s, border-color .22s; } +.card-hover:hover { box-shadow: var(--sh-md); transform: translateY(-3px); border-color: #dbe2ec; } + +/* glass */ +.glass { background: rgba(255,255,255,.72); backdrop-filter: blur(16px) saturate(1.4); -webkit-backdrop-filter: blur(16px) saturate(1.4); border: 1px solid rgba(255,255,255,.6); } + +/* ---------- Badges ---------- */ +.badge { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 99px; line-height: 1.3; white-space: nowrap; } +.badge .dot { width: 6px; height: 6px; border-radius: 99px; background: currentColor; } +.badge-blue { background: var(--blue-soft); color: var(--blue-700); } +.badge-green { background: var(--green-soft); color: var(--green-600); } +.badge-navy { background: #e6edf4; color: var(--navy); } +.badge-gold { background: var(--gold-soft); color: #9c7b3a; } +.badge-gray { background: #eef1f5; color: var(--muted); } +.badge-amber { background: #fdf2e0; color: #b9760c; } +.badge-red { background: #fdecec; color: #cf4040; } + +.chip { + display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font-size: 13px; font-weight: 500; + padding: 8px 15px; border-radius: 99px; background: #fff; border: 1px solid var(--border); color: var(--ink-2); transition: all .15s; +} +.chip:hover { border-color: #cdd5e0; } +.chip.active { background: var(--navy); color: #fff; border-color: var(--navy); } + +/* ---------- Inputs ---------- */ +.field { display: flex; flex-direction: column; gap: 7px; } +.field label { font-size: 13px; font-weight: 600; color: var(--ink-2); } +.input, .select { + width: 100%; padding: 12px 14px; font-size: 14px; color: var(--ink); background: #fff; + border: 1.5px solid var(--border); border-radius: 11px; outline: none; transition: border-color .15s, box-shadow .15s; +} +.input::placeholder { color: #9aa6b6; } +.input:focus, .select:focus { border-color: var(--blue); box-shadow: 0 0 0 4px rgba(13,110,253,.12); } +.input-icon { position: relative; } +.input-icon svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #9aa6b6; } +.input-icon .input { padding-left: 42px; } + +/* ---------- Sidebar nav ---------- */ +.nav-item { + display: flex; align-items: center; gap: 13px; width: 100%; padding: 10px 13px; border-radius: 12px; + border: none; background: transparent; color: #aebbcf; font-size: 14px; font-weight: 500; text-align: left; + transition: background .15s, color .15s; position: relative; +} +.nav-item svg { width: 20px; height: 20px; flex: none; } +.nav-item:hover { background: rgba(255,255,255,.06); color: #eaf1fb; } +.nav-item.active { background: linear-gradient(100deg, var(--blue), var(--blue-600)); color: #fff; box-shadow: var(--sh-blue); font-weight: 600; } +.nav-badge { margin-left: auto; font-size: 11px; font-weight: 700; background: var(--green); color: #173a05; padding: 1px 7px; border-radius: 99px; } + +/* ---------- Tables ---------- */ +.tbl { width: 100%; border-collapse: separate; border-spacing: 0 8px; } +.tbl th { text-align: left; font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); padding: 4px 16px; } +.tbl td { padding: 14px 16px; font-size: 14px; color: var(--ink-2); background: #fff; border-top: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); } +.tbl td:first-child { border-left: 1px solid var(--border-soft); border-radius: 12px 0 0 12px; } +.tbl td:last-child { border-right: 1px solid var(--border-soft); border-radius: 0 12px 12px 0; } +.tbl tbody tr { transition: box-shadow .15s, transform .15s; } +.tbl tbody tr:hover td { background: #fafcff; } +.tbl tbody tr:hover { box-shadow: var(--sh-sm); } + +/* progress */ +.progress { height: 8px; background: #eef1f6; border-radius: 99px; overflow: hidden; } +.progress > span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--blue), var(--blue-600)); transition: width .8s cubic-bezier(.2,.7,.3,1); } + +/* icon button */ +.icon-btn { width: 40px; height: 40px; border-radius: 12px; border: 1px solid transparent; display: inline-flex; align-items: center; justify-content: center; background: transparent; color: var(--muted); transition: background .15s, color .15s, border-color .15s; position: relative; } +.icon-btn:hover { background: #f0f3f8; color: var(--ink); } +.icon-btn svg { width: 20px; height: 20px; } +.notif-dot { position: absolute; top: 8px; right: 9px; width: 8px; height: 8px; border-radius: 99px; background: var(--green); border: 2px solid #fff; } + +.kicker { font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--blue); } +.divider { height: 1px; background: var(--border); border: none; } + +.avatar { border-radius: 99px; object-fit: cover; background: linear-gradient(135deg, var(--blue), var(--navy)); display: inline-flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; } + +/* property image slot default look */ +image-slot { display: block; } + +/* animations (opt-in via body.anim-on) */ +@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } } +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes popIn { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } } +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: .35; } } +@keyframes growBar { from { transform: scaleY(0); } to { transform: scaleY(1); } } +body.anim-on .anim-fade-up { animation: fadeUp .55s cubic-bezier(.2,.7,.3,1) both; } +body.anim-on .anim-fade-in { animation: fadeIn .4s ease both; } +body.anim-on .anim-pop { animation: popIn .35s cubic-bezier(.2,.8,.3,1) both; } +body.anim-on .stagger > * { animation: fadeUp .55s cubic-bezier(.2,.7,.3,1) both; } +body.anim-on .stagger > *:nth-child(1){animation-delay:.04s} +body.anim-on .stagger > *:nth-child(2){animation-delay:.10s} +body.anim-on .stagger > *:nth-child(3){animation-delay:.16s} +body.anim-on .stagger > *:nth-child(4){animation-delay:.22s} +body.anim-on .stagger > *:nth-child(5){animation-delay:.28s} +body.anim-on .stagger > *:nth-child(6){animation-delay:.34s} +body.anim-on .stagger > *:nth-child(7){animation-delay:.40s} +body.anim-on .stagger > *:nth-child(8){animation-delay:.46s} diff --git a/src/Yammuna Express/map.jsx b/src/Yammuna Express/map.jsx new file mode 100644 index 0000000..6a93156 --- /dev/null +++ b/src/Yammuna Express/map.jsx @@ -0,0 +1,160 @@ +/* Interactive stylized sector map — pan via plots, zoom, layers, hover, select. */ +(function () { + const { useState, useMemo } = React; + const Icon = window.Icon; + + // Generate a plausible plot layout: blocks of plots separated by roads. + function buildPlots(seed) { + const rng = (() => { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; })(); + const blocks = [ + { code: 'A', x: 40, y: 40, cols: 6, rows: 3 }, + { code: 'B', x: 40, y: 230, cols: 6, rows: 3 }, + { code: 'C', x: 470, y: 40, cols: 5, rows: 3 }, + { code: 'D', x: 470, y: 230, cols: 5, rows: 4 }, + ]; + const pw = 58, ph = 50, gap = 6; + const plots = []; + blocks.forEach(b => { + for (let r = 0; r < b.rows; r++) for (let c = 0; c < b.cols; c++) { + const n = r * b.cols + c + 1; + const types = ['res', 'res', 'res', 'comm', 'inst', 'park']; + const t = rng() < 0.08 ? 'park' : rng() < 0.12 ? 'comm' : rng() < 0.08 ? 'inst' : 'res'; + plots.push({ + id: b.code + '-' + n, block: b.code, + x: b.x + c * (pw + gap), y: b.y + r * (ph + gap), w: pw, h: ph, type: t, + }); + } + }); + return plots; + } + + const TYPE = { + res: { fill: '#ffffff', label: 'Residential', sw: '#cbd8e8' }, + comm: { fill: '#fff2dc', label: 'Commercial', sw: '#f0d49a' }, + inst: { fill: '#e7eeff', label: 'Institutional', sw: '#bcd0f5' }, + park: { fill: '#e3f6ec', label: 'Green / Park', sw: '#b6e6cd' }, + }; + + function SectorMap({ sectorName = 'Sector 18', myPlot = 'B-47', height = 460, compact }) { + const plots = useMemo(() => buildPlots(sectorName.length * 137 + 7), [sectorName]); + const [zoom, setZoom] = useState(1); + const [sel, setSel] = useState(null); + const [hover, setHover] = useState(null); + const [layers, setLayers] = useState({ satellite: false, master: false, infra: true }); + const [search, setSearch] = useState(''); + // map myPlot to an existing plot id; fall back to one in the same block, then a default + const mine = plots.find(p => p.id === myPlot) + || (myPlot && plots.find(p => p.block === myPlot.split('-')[0])) + || plots[14]; + const matched = search ? plots.find(p => p.id.toLowerCase() === search.toLowerCase()) : null; + + const W = 760, H = 480; + return ( +
+ {/* toolbar */} +
+
+ {sectorName} +
+ + { setSearch(e.target.value); const m = plots.find(p => p.id.toLowerCase() === e.target.value.toLowerCase()); if (m) setSel(m.id); }} /> +
+
+
+ {[['satellite', 'layers', 'Satellite'], ['master', 'grid', 'Master Plan'], ['infra', 'building', 'Infrastructure']].map(([k, ic, l]) => ( + + ))} +
+
+ + {/* map canvas */} +
+
+ + {/* land base */} + + {/* greens / blocks bg when satellite */} + {layers.satellite && plots.filter(p => p.type === 'park').map(p => )} + {/* roads */} + + + + + + + {/* master plan grid overlay */} + {layers.master && Array.from({ length: 20 }).map((_, i) => ( + + + + + ))} + {/* plots */} + {plots.map(p => { + const t = TYPE[p.type]; + const isMine = p.id === mine.id; + const isSel = sel === p.id; + const isHover = hover === p.id; + const isMatch = matched && matched.id === p.id; + let fill = layers.satellite ? (p.type === 'park' ? '#356b4e' : '#cdd9e6') : t.fill; + if (isMine) fill = 'var(--emerald-500)'; + if (isSel || isMatch) fill = 'var(--blue-500)'; + return ( + setHover(p.id)} onMouseLeave={() => setHover(null)} + onClick={() => setSel(p.id)}> + + {p.id} + + ); + })} + {/* my plot pulse */} + + +
+ + {/* legend */} + {!compact && ( +
+
Legend
+ {[['Your Plot', 'var(--emerald-500)'], ['Selected', 'var(--blue-500)'], ['Residential', '#fff'], ['Commercial', '#fff2dc'], ['Green / Park', '#e3f6ec']].map(([l, c]) => ( +
+ {l} +
+ ))} +
+ )} + + {/* zoom controls */} +
+ + + +
+ + {/* selected plot tooltip */} + {sel && (() => { const p = plots.find(x => x.id === sel); const isMine = p.id === mine.id; return ( +
+
Plot {p.id}
+
{TYPE[p.type].label} · Block {p.block}
+
+
+
Area{p.type === 'comm' ? '450' : '300'} sq.m
+
Status{isMine ? Yours : Allotted}
+
+
+ ); })()} +
+
+ ); + } + + window.SectorMap = SectorMap; +})(); diff --git a/src/Yammuna Express/myjourney.jsx b/src/Yammuna Express/myjourney.jsx new file mode 100644 index 0000000..ba08307 --- /dev/null +++ b/src/Yammuna Express/myjourney.jsx @@ -0,0 +1,301 @@ +/* Construction Journey — goal selection + 8-step survey + Project Team + Progress Center */ +(function () { + const { useState } = React; + const Icon = window.Icon, PageHead = window.PageHead, Donut = window.Donut, Avatar = window.Avatar; + const D = window.YEIDA, fmt = D.fmtINR; + + function MyJourney({ go, toast }) { + const [tab, setTab] = useState('journey'); + return ( +
+ + + + + + {/* sub-tabs */} +
+ {[['journey', 'Journey Steps'], ['team', 'Project Team'], ['progress', 'Progress Center']].map(([k, l]) => ( + + ))} +
+ + {tab === 'journey' && } + {tab === 'team' && } + {tab === 'progress' && } +
+ ); + } + + /* ---------------- GOAL SELECTION + 8-STEP ACCORDION SURVEY ---------------- */ + function JourneySteps({ go, toast }) { + const [goal, setGoal] = useState('start'); + const [steps, setSteps] = useState(D.journeySteps); + const [open, setOpen] = useState(() => { + const a = D.journeySteps.findIndex(s => s.active); + return a >= 0 ? a : 0; + }); + + const doneCount = steps.filter(s => s.done).length; + const save = (i) => { + setSteps(arr => arr.map((s, j) => j === i ? { ...s, done: true, active: false } : s)); + toast && toast('Information saved · ' + steps[i].title); + const next = steps.findIndex((s, j) => j > i && !s.done); + setOpen(next >= 0 ? next : -1); + }; + + return ( +
+ {/* progress header */} +
+ +
+

You're {Math.round(doneCount / steps.length * 100)}% through your journey

+

Complete each step below — saved steps turn green and stay editable. We'll guide you to the next action.

+
+ {doneCount} completed + {steps.length - doneCount} remaining +
+
+
+ + {/* goal selection */} +
+

What would you like to do with your plot?

+

Pick a goal — we'll personalise your journey around it.

+
+ {D.journeyGoals.map(gOpt => { + const on = goal === gOpt.id; + return ( + + ); + })} +
+
+ + {/* accordion survey */} +
+ {steps.map((s, i) => { + const isOpen = open === i; + const state = s.done ? 'done' : s.active || isOpen ? 'active' : 'todo'; + return ( +
+ {/* header row */} + + + {/* body */} + {isOpen && ( +
+ +
+ + {s.done && Editable anytime} +
+
+ )} +
+ ); + })} +
+
+ ); + } + + function StepBody({ step, go }) { + // tailored, lightweight content per step + switch (step.key) { + case 'goal': + return Your selected goal drives the recommended steps, designs and budget. You can change it from the selector above.; + case 'plot': + return ( +
+ {[['Allotment No.', D.user.allotment], ['Plot', D.user.plot], ['Sector', D.user.sector], ['Pocket', D.user.pocket]].map(([l, v]) => )} +
+ ); + case 'design': + return ( +
+ Selected design from the Design Explorer. Browse alternatives anytime. + +
+ ); + case 'planning': + return ( +
+ {[['Configuration', 'G+1'], ['Built-up area', '4,200 sq.ft'], ['Est. duration', '11 months'], ['Plan status', 'Sanctioned']].map(([l, v]) => )} +
+ ); + case 'budget': + return ( +
+
+ {[['Total budget', fmt(D.construction.estimatedCost)], ['Milestones', '5'], ['Funded', fmt(D.construction.spent)]].map(([l, v]) => )} +
+ +
+ ); + case 'contractor': + return ( +
+ Assign your project team — manager, site engineer, architect & crew. View the Project Team tab to manage contacts. +
+ ); + case 'progress': + return Once construction starts, track every stage with photos, notes and dates in the Progress Center.; + case 'certificate': + return After final inspection, apply for and download your Completion Certificate here.; + default: + return null; + } + } + + function Field({ label, value }) { + return ( +
+
{label}
+
{value}
+
+ ); + } + function Note({ children }) { + return

{children}

; + } + + /* ---------------- PROJECT TEAM ---------------- */ + function ProjectTeam({ toast }) { + return ( +
+
+

Your Project Team

+
+ {D.projectTeam.map(m => ( +
+
+
+ + {m.online && } +
+ {m.role} +
+
{m.name}
+
{m.org}
+
+ + +
+
+ ))} +
+
+ {/* team updates / milestone */} +
+

Team Updates

+
+ {D.teamUpdates.map((u, i, arr) => ( +
+ {i !== arr.length - 1 && } + +
+
{u.who} — {u.text}
+
{u.time}
+
+
+ ))} +
+
+
+ ); + } + + /* ---------------- PROGRESS CENTER (stages + live view) ---------------- */ + function ProgressCenter() { + const overall = Math.round(D.progressStages.reduce((s, x) => s + x.pct, 0) / D.progressStages.length); + return ( +
+ {/* live view + summary */} +
+
+
+
+
+ LIVE +
+
+
+
Live Site Camera
+
Plot B-47 · Cam 1 · 1080p — drop in your RTSP/webcam feed
+
+
+
+
{['Cam 1', 'Cam 2', 'Drone'].map((c, i) => {c})}
+ Updated just now +
+
+
+
+
+ {[['Daily Site Photos', 'image', '42 this month'], ['Weekly Progress Report', 'documents', 'Latest: 06 Jun'], ['Current Stage', 'construction', 'Slab Casting']].map(([l, ic, v]) => ( +
+ {l} + {v} +
+ ))} +
+
+
+ + {/* stage timeline */} +
+

Construction Stages

+
+ {D.progressStages.map((st, i, arr) => { + const state = st.pct === 100 ? 'done' : st.pct > 0 ? 'active' : 'todo'; + const cc = state === 'done' ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : '#c2cedd'; + return ( +
+ {i !== arr.length - 1 && } + + {state === 'done' ? : state === 'active' ? : {i + 1}} + +
+
+ {st.name} +
+ {st.photos > 0 && {st.photos}} + {state === 'done' ? 'Completed' : state === 'active' ? st.pct + '%' : 'Upcoming'} +
+
+ {st.note &&
{st.note}
} +
{st.date}
+ {state === 'active' &&
} +
+
+ ); + })} +
+
+
+ ); + } + + window.MyJourney = MyJourney; +})(); diff --git a/src/Yammuna Express/onboarding.jsx b/src/Yammuna Express/onboarding.jsx new file mode 100644 index 0000000..6802fdb --- /dev/null +++ b/src/Yammuna Express/onboarding.jsx @@ -0,0 +1,263 @@ +/* Registration wizard: Account → Allotment → Verify → Addresses */ +(function () { + const { useState } = React; + const Icon = window.Icon; + const D = window.YEIDA; + const AuthShell = window.AuthShell, VerifyPanel = window.VerifyPanel; + + const STEPS = [ + { k: 'account', label: 'Account', icon: 'user' }, + { k: 'allotment', label: 'Allotment', icon: 'building' }, + { k: 'verify', label: 'Verify', icon: 'shield' }, + { k: 'address', label: 'Address', icon: 'mapPin' }, + ]; + + function Stepper({ step }) { + return ( +
+ {STEPS.map((s, i) => ( + +
+ + {i < step ? : i + 1} + + {s.label} +
+ {i < STEPS.length - 1 && } +
+ ))} +
+ ); + } + + function Register({ go }) { + const [step, setStep] = useState(0); + const [form, setForm] = useState({ + firstName: 'Rajeev', lastName: 'Malhotra', email: 'r.malhotra@email.com', + cc: '+91', mobile: '9876500012', password: '', confirm: '', + relationship: 'Self', terms: true, cookies: true, + allotteeFirst: 'Rajeev', allotteeLast: 'Malhotra', + hasAllotment: true, allotment: 'YEA-654321', sector: 'Sector 18', pocket: 'Pocket B', plot: 'B-47', + }); + const set = (k, v) => setForm(f => ({ ...f, [k]: v })); + const next = () => setStep(s => Math.min(STEPS.length - 1, s + 1)); + const isAllottee = form.relationship === 'Self'; + + return ( + +
+ +
New Allottee Registration
+

+ {['Create your account', 'Identify your plot', 'Verify email & phone', 'Your addresses'][step]} +

+ + +
+ {step === 0 && } + {step === 1 && } + {step === 2 && } + {step === 3 && } +
+ + {step !== 2 && ( + + )} + + {step === 0 && ( +
+ Already registered?{' '} + +
+ )} +
+
+ ); + } + + /* Step 0 — Account creation */ + function AccountStep({ form, set, isAllottee }) { + const pwMismatch = form.confirm.length > 0 && form.password !== form.confirm; + return ( +
+
+
set('firstName', e.target.value)} />
+
set('lastName', e.target.value)} />
+
+
set('email', e.target.value)} placeholder="you@email.com" />
+
+
+ +
set('mobile', e.target.value)} />
+
+
+
+
set('password', e.target.value)} placeholder="Min 8 characters" />
+
set('confirm', e.target.value)} placeholder="Re-enter password" style={pwMismatch ? { borderColor: 'var(--red-500)' } : undefined} />
+
+ {pwMismatch &&
Passwords do not match
} + + {/* Relationship with Allottee */} +
+ + +
+
+ + {isAllottee ? 'Self registration — you are the original allottee (Allottee = User).' : `Invitation registration — you are registering on behalf of the allottee as ${form.relationship} (Allottee ≠ User).`} +
+ + {/* Allottee name — required when registrant is NOT the allottee */} + {!isAllottee && ( +
+
Original Allottee Details
+
+
set('allotteeFirst', e.target.value)} placeholder="As per records" />
+
set('allotteeLast', e.target.value)} placeholder="As per records" />
+
+
+ )} + + + +
+ ); + } + + /* Step 1 — Allotment identification */ + function AllotmentStep({ form, set }) { + return ( +
+

What is your Allotment Number?

+

Enter your allotment number to link your plot, or identify it by sector & plot if you don't have it handy.

+ + {/* toggle */} +
+ {[['have', 'I have my allotment number'], ['forgot', "I don't remember it"]].map(([k, l]) => ( + + ))} +
+ + {form.hasAllotment ? ( +
+ +
set('allotment', e.target.value.toUpperCase())} placeholder="e.g. YEA-654321" />
+ Format: YEA- followed by your 6-digit number +
+ ) : ( +
+
+ Identify your plot below — we'll match it to your allotment record. +
+
+
+ +
+
+ +
+
+
set('plot', e.target.value)} placeholder="3–4 digits" maxLength={5} />
+
+
+ Plot number is typically 3 or 4 digits (e.g. 047 or 1024). +
+ )} +
+ ); + } + + /* Step 2 — Verification */ + function VerifyStep({ form, go }) { + const [both, setBoth] = useState(false); + return ( +
+

Verify both your email and mobile number on this screen. Each requires an OTP.

+ + +
+ ); + } + + /* Step 3 — Addresses (registered + mailing, editable independently) */ + function AddressStep({ form, set }) { + const [sameAsReg, setSameAsReg] = useState(false); + return ( +
+ + + {!sameAsReg && } +
+ ); + } + + function AddressBlock({ title, sub, icon, prefix, form, set }) { + const def = prefix === 'reg' ? D.user.registeredAddress : D.user.mailingAddress; + return ( +
+
+ +
{title}
{sub}
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + /* Standalone allotment lookup (login path — "I don't remember my allotment number") */ + function AllotmentLookup({ go }) { + const [form, setForm] = useState({ sector: 'Sector 18', pocket: 'Pocket B', plot: 'B-47' }); + const [found, setFound] = useState(false); + const set = (k, v) => setForm(f => ({ ...f, [k]: v })); + return ( + +
+ +
Find your allotment
+

Identify your plot

+

No allotment number? Locate your plot by sector, pocket and plot number.

+
+
+
+
+
set('plot', e.target.value)} placeholder="e.g. 047" />
+ {found ? ( +
+
Allotment found · YEA-654321
{form.sector} · {form.pocket} · Plot {form.plot}
+
+ ) : ( + + )} + +
+
+ ); + } + + window.Register = Register; + window.AllotmentLookup = AllotmentLookup; +})(); diff --git a/src/Yammuna Express/payments.jsx b/src/Yammuna Express/payments.jsx new file mode 100644 index 0000000..02c007c --- /dev/null +++ b/src/Yammuna Express/payments.jsx @@ -0,0 +1,257 @@ +/* Payments module */ +(function () { + const { useState } = React; + const Icon = window.Icon, PageHead = window.PageHead, LineChart = window.LineChart, BarChart = window.BarChart; + const D = window.YEIDA, fmt = D.fmtINR; + + function Payments({ go, toast }) { + const [tab, setTab] = useState('dues'); + const [pay, setPay] = useState(null); // due being paid + const totalDue = D.dues.reduce((s, d) => s + d.amount, 0); + + return ( +
+ + + + + + {/* summary cards */} +
+ {[ + ['Outstanding', fmt(totalDue), 'amber', 'rupee', '3 dues pending'], + ['Paid (FY 25-26)', fmt(265370), 'emerald', 'checkCircle', '5 transactions'], + ['Next Due', '30 Jun', 'blue', 'calendar', 'Lease Rent 2026-27'], + ['Total Premium', fmt(D.property.totalValue), 'navy', 'building', '90% paid'], + ].map(([l, v, c, ic, sub]) => { + const cc = { blue: ['var(--blue-50)', 'var(--blue-600)'], emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c]; + return ( +
+
+
{v}
+
{l}
+
{sub}
+
+ ); + })} +
+ + {/* tabs */} +
+ {[['dues', 'Outstanding Dues'], ['history', 'Payment History'], ['receipts', 'Receipts'], ['construction', 'Construction Payments'], ['analytics', 'Financial Analytics']].map(([k, l]) => ( + + ))} +
+ + {tab === 'dues' && toast && toast('e-Challan generated · CH-2026-0606-18')} />} + {tab === 'history' && } + {tab === 'receipts' && } + {tab === 'construction' && } + {tab === 'analytics' && } + + {pay && setPay(null)} onDone={() => { setPay(null); toast && toast('Payment successful · receipt sent to email'); }} />} +
+ ); + } + + function Dues({ onPay, onChallan }) { + return ( +
+ + + + {D.dues.map(d => ( + + + + + + + + + ))} + +
DescriptionTypeDue DateStatusAmount
{d.label}
{d.id}
{d.type}{d.due}{d.status === 'overdue' ? 'Overdue' : d.status === 'due' ? 'Due soon' : 'Upcoming'}{fmt(d.amount)} +
+ + +
+
+
+ ); + } + + function History() { + return ( +
+ + + + {D.payments.map(p => ( + + + + + + + + + + ))} + +
TransactionDateModeChallanStatusAmount
{p.label}
{p.id}
{p.date}{p.mode}{p.challan}{p.status}{fmt(p.amount)}
+
+ ); + } + + function Receipts({ toast }) { + return ( +
+ {D.payments.map(p => ( +
+
+ + Paid +
+
{p.label}
+
{p.challan} · {p.date}
+
{fmt(p.amount)}
+
+ + +
+
+ ))} +
+ ); + } + + function ConstructionPayments({ toast }) { + const CP = D.constructionPayments; + const C = D.construction; + const paid = CP.filter(p => p.status === 'Paid').reduce((s, p) => s + p.amount, 0); + const upcoming = CP.filter(p => p.status !== 'Paid').reduce((s, p) => s + p.amount, 0); + return ( +
+
+ {[['Estimated Build Cost', fmt(C.estimatedCost), 'navy', 'building'], ['Paid to Contractor', fmt(paid), 'emerald', 'checkCircle'], ['Upcoming Milestones', fmt(upcoming), 'amber', 'clock']].map(([l, v, c, ic]) => { + const cc = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c]; + return ( +
+ +
{v}
+
{l}
+
+ ); + })} +
+
+
+

Milestone Payments & Invoices

+ Contractor · {C.contractor.name} +
+ + + + {CP.map(p => ( + + + + + + + + + + ))} + +
MilestoneInvoiceVendorDateStatusAmount
{p.milestone}{p.id}{p.vendor}{p.date}{p.status === 'Paid' ? : null}{p.status}{fmt(p.amount)}{p.status === 'Paid' ? : }
+
+
+ ); + } + + function Analytics() { + return ( +
+
+
+

Cumulative payments

+

Premium + lease rent paid since allotment

+ +
20192021202320252026
+
+
+

By category

+

Lifetime breakdown

+ {[['Location Premium', 82, 'var(--blue-500)'], ['Lease Rent', 11, 'var(--emerald-500)'], ['Fees & Charges', 5, 'var(--amber-500)'], ['Utilities', 2, 'var(--navy-700)']].map(([l, p, c]) => ( +
+
{l}{p}%
+
+
+ ))} +
+
+
+

Yearly payments

+

Total paid per financial year (₹ lakh)

+ +
{['19', '20', '21', '22', '23', '24', '25', '26'].map(y => FY{y})}
+
+
+ ); + } + + function PayModal({ due, onClose, onDone }) { + const [method, setMethod] = useState('upi'); + const [stage, setStage] = useState('select'); + const methods = [['upi', 'UPI', 'phone'], ['card', 'Debit/Credit Card', 'payments'], ['net', 'Net Banking', 'building']]; + const pay = () => { setStage('processing'); setTimeout(() => setStage('done'), 1600); }; + return ( +
+
e.stopPropagation()} className="anim-pop card" style={{ width: 440, maxWidth: '100%', padding: 0, overflow: 'hidden' }}> + {stage !== 'done' ? ( + <> +
+

Complete Payment

+ +
+
+
+
{due.label}
+
{fmt(due.amount)}
+
+
Payment Method
+
+ {methods.map(([k, l, ic]) => ( + + ))} +
+ +
Secured by YEIDA Payment Gateway
+
+ + ) : ( +
+
+

Payment Successful

+

{fmt(due.amount)} paid · Receipt CH-2026-0606-18

+
+ + +
+
+ )} +
+
+ ); + } + + window.Payments = Payments; +})(); diff --git a/src/Yammuna Express/plot.jsx b/src/Yammuna Express/plot.jsx new file mode 100644 index 0000000..b3e6033 --- /dev/null +++ b/src/Yammuna Express/plot.jsx @@ -0,0 +1,143 @@ +/* My Plot */ +(function () { + const Icon = window.Icon, PageHead = window.PageHead, SectorMap = window.SectorMap; + const D = window.YEIDA, fmt = D.fmtINR, P = D.property; + + const timeline = [ + { title: 'Plot Allotted', date: '14 Mar 2019', desc: 'Allotment letter issued for Plot B-47', color: 'emerald', icon: 'check' }, + { title: 'Possession Granted', date: '22 Aug 2019', desc: 'Physical possession handed over', color: 'emerald', icon: 'home' }, + { title: 'Lease Deed Registered', date: '05 Sep 2019', desc: '90-year lease registered at Sub-Registrar', color: 'emerald', icon: 'file' }, + { title: 'Building Plan Sanctioned', date: '18 Feb 2026', desc: 'Construction approval granted', color: 'blue', icon: 'construction' }, + { title: 'Construction In Progress', date: 'Ongoing', desc: 'Ground floor structure — 65% complete', color: 'amber', icon: 'building' }, + ]; + + function InfoRow({ label, value, strong }) { + return ( +
+ {label} + {value} +
+ ); + } + + function MyPlot({ go }) { + return ( +
+ + + + + + {/* hero property card */} +
+
+
+
+
+ +
+

Plot {P.plot}, {P.sector}

+
Allotment {P.allotmentNo}
+
+
+
+
+
+ {[['Plot Size', P.size], ['Category', P.category], ['Facing', P.facing], ['Block', P.block], ['Road Width', P.road], ['Lease Period', P.leasePeriod]].map(([l, v]) => ( +
+
{l}
+
{v}
+
+ ))} +
+
+
+
Total Premium Value
+
{fmt(P.totalValue)}
+
+
+ {fmt(P.paid)} paid + {fmt(P.totalValue - P.paid)} balance +
+
+ Allotted {P.allottedOn} + {P.rate} +
+
+
+
+ + {/* map + info */} +
+
+
+

Plot Location

+ {P.sector}, Yamuna Expressway, Gautam Buddh Nagar +
+ +
+
+

Ownership Information

+ + + + + + + + + +
+
+ + {/* authority status + addresses */} +
+
+

Authority Status

+
+ {[['Lease Deed', 'LD_COMPLETED', 'file'], ['ATL (Allotment Transfer Letter)', 'ATL_PENDING', 'checkCircle'], ['Authority Verification', 'VERIFIED_AUTH', 'shield'], ['Possession', 'POSS_GRANTED', 'home']].map(([l, code, ic]) => ( +
+ {l} + +
+ ))} +
+
+
+
+

Addresses

+ +
+ {[['Registered Address', D.user.registeredAddress, 'building'], ['Mailing / Current Address', D.user.mailingAddress, 'mail']].map(([title, a, ic]) => ( +
+
{title}
+
{a.line1}, {a.line2}
{a.city}, {a.state} — {a.pin}
+
+ ))} +
+
+ + {/* timeline */} +
+

Property Timeline

+
+
+ {timeline.map((t, i) => { + const cc = { emerald: 'var(--emerald-500)', blue: 'var(--blue-500)', amber: 'var(--amber-500)' }[t.color]; + return ( +
+ +
{t.title}
+
{t.date}
+
{t.desc}
+
+ ); + })} +
+
+
+ ); + } + + window.MyPlot = MyPlot; +})(); diff --git a/src/Yammuna Express/sampleplans.jsx b/src/Yammuna Express/sampleplans.jsx new file mode 100644 index 0000000..07eb428 --- /dev/null +++ b/src/Yammuna Express/sampleplans.jsx @@ -0,0 +1,164 @@ +/* Sample Plans & Virtual Tours + First-time Journey ("What would you like to do next?") */ +(function () { + const { useState } = React; + const Icon = window.Icon, PageHead = window.PageHead; + const D = window.YEIDA; + + /* ---------------- SAMPLE PLANS & VIRTUAL TOURS ---------------- */ + const FILTERS = [ + { key: 'all', label: 'All Plans' }, + { key: 'size', label: 'My Plot Size (300 sq.m)', test: p => p.size === '300 sq.m' }, + { key: 'facing', label: 'My Orientation (East)', test: p => p.facing === 'East' }, + { key: 'tour', label: '3D Walkthrough', test: p => p.tour }, + ]; + + function PlanThumb({ accent }) { + return ( + + + + {/* room partitions — schematic floor plan */} + + + + + + + + {/* compass */} + N + + ); + } + + function SamplePlans({ go, toast }) { + const [f, setF] = useState('all'); + const active = FILTERS.find(x => x.key === f); + const list = D.samplePlans.filter(p => !active.test || active.test(p)); + const [tour, setTour] = useState(null); + return ( +
+ + + + + + {/* hero strip */} +
+
+ Plot-specific guidance +

Plans tailored to Plot B-47

+

Based on your 300 sq.m east-facing plot in Sector 18, here are orientation- and size-appropriate designs with 3D walkthroughs and virtual tours.

+
+ {[['Plot size', '300 sq.m'], ['Facing', 'East'], ['Permissible FAR', '1.5'], ['Max height', '15 m']].map(([l, v]) => ( +
{l}
{v}
+ ))} +
+
+
+
+
+
Immersive 3D Walkthroughs
+
Tour {D.samplePlans.filter(p => p.tour).length} designs in virtual reality
+
+
+
+ + {/* filters */} +
+ {FILTERS.map(x => )} +
+ + {/* plan grid */} +
+ {list.map(p => ( +
+
+ + {p.tour && } +
+
+
+

{p.name}

+ {p.style} +
+
+ {p.size} + {p.beds} BHK + {p.floors} +
+
+ + +
+
+
+ ))} +
+ + {tour && setTour(null)} />} +
+ ); + } + + function TourModal({ plan, onClose }) { + return ( +
+
e.stopPropagation()} className="anim-pop card" style={{ width: 760, maxWidth: '100%', padding: 0, overflow: 'hidden' }}> +
+
{plan.name}
{plan.style} · {plan.size} · {plan.facing} facing
+ +
+
+
+
+
+
3D Virtual Walkthrough
+
Immersive tour placeholder — drop in your Matterport / 360° embed
+
+
+
+
{['Living', 'Bedrooms', 'Kitchen', 'Terrace'].map((r, i) => {r})}
+ +
+
+
+ ); + } + + /* ---------------- JOURNEY: "What would you like to do?" (7 action cards) ---------------- */ + function Journey({ go }) { + const COL = { blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], emerald: ['var(--emerald-50)', 'var(--emerald-700)'] }; + return ( +
+
+
+

What would you like to do?

+

Your plot is verified, {D.user.firstName}. Choose an action to get started — you can always come back here.

+
+ +
+ {D.onboardingActions.map((c, i) => { + const cc = COL[c.color]; + const wide = i === 6; // last card spans for 7-card balance + return ( + + ); + })} +
+ +
+ +
+
+ ); + } + + Object.assign(window, { SamplePlans, Journey }); +})(); diff --git a/src/Yammuna Express/shell.jsx b/src/Yammuna Express/shell.jsx new file mode 100644 index 0000000..ecc6d91 --- /dev/null +++ b/src/Yammuna Express/shell.jsx @@ -0,0 +1,120 @@ +/* App shell: sidebar + topbar. Renders children content. */ +(function () { + const { useState } = React; + const Icon = window.Icon, Logo = window.Logo, Avatar = window.Avatar; + const D = window.YEIDA; + + const NAV = [ + { k: 'dashboard', label: 'Dashboard', icon: 'dashboard' }, + { k: 'plot', label: 'My Plot', icon: 'plot' }, + { k: 'myjourney', label: 'My Journey', icon: 'flag' }, + { k: 'sampleplans', label: 'Sample Plans', icon: 'grid' }, + { k: 'construction', label: 'Construction', icon: 'construction' }, + { k: 'payments', label: 'Payments', icon: 'payments', badge: '1' }, + { k: 'documents', label: 'Documents', icon: 'documents' }, + { k: 'notices', label: 'Notices', icon: 'notices', badge: '2' }, + { k: 'sectormaps', label: 'Sector Maps', icon: 'map' }, + { k: 'transfer', label: 'Transfer Plot', icon: 'transfer' }, + { k: 'assistant', label: 'AI Assistant', icon: 'assistant' }, + ]; + const NAV2 = [ + { k: 'support', label: 'Support', icon: 'support' }, + { k: 'profile', label: 'Profile', icon: 'profile' }, + ]; + + function Shell({ route, go, children, dark, setDark }) { + const [q, setQ] = useState(''); + const titleMap = { + dashboard: 'My Plot Dashboard', plot: 'My Plot', construction: 'Construction', + payments: 'Payments', documents: 'Document Center', notices: 'Notices & Announcements', + sectormaps: 'Sector Maps', transfer: 'Transfer Plot', assistant: 'AI Assistant', + support: 'Support Center', profile: 'Profile', sampleplans: 'Sample Plans & Virtual Tours', + journey: 'Get Started', myjourney: 'My Construction Journey', + }; + return ( +
+ {/* ---- Sidebar ---- */} + + + {/* ---- Main ---- */} +
+ {/* topbar */} +
+
{titleMap[route]}
+
+ + setQ(e.target.value)} /> + ⌘K +
+
+ + + +
+ +
+
+ +
+
{children}
+
+
+ {route !== 'assistant' && window.FloatingAssistant && React.createElement(window.FloatingAssistant, { go })} +
+ ); + } + + window.Shell = Shell; +})(); diff --git a/src/Yammuna Express/shots/dash-b.png b/src/Yammuna Express/shots/dash-b.png new file mode 100644 index 0000000..dbfadce Binary files /dev/null and b/src/Yammuna Express/shots/dash-b.png differ diff --git a/src/Yammuna Express/shots/dash-bottom.png b/src/Yammuna Express/shots/dash-bottom.png new file mode 100644 index 0000000..77040d1 Binary files /dev/null and b/src/Yammuna Express/shots/dash-bottom.png differ diff --git a/src/Yammuna Express/shots/dash-cons.png b/src/Yammuna Express/shots/dash-cons.png new file mode 100644 index 0000000..e9a26e9 Binary files /dev/null and b/src/Yammuna Express/shots/dash-cons.png differ diff --git a/src/Yammuna Express/shots/dash-plot.png b/src/Yammuna Express/shots/dash-plot.png new file mode 100644 index 0000000..7caa544 Binary files /dev/null and b/src/Yammuna Express/shots/dash-plot.png differ diff --git a/src/Yammuna Express/shots/dash-pro.png b/src/Yammuna Express/shots/dash-pro.png new file mode 100644 index 0000000..cae5e82 Binary files /dev/null and b/src/Yammuna Express/shots/dash-pro.png differ diff --git a/src/Yammuna Express/shots/dash.png b/src/Yammuna Express/shots/dash.png new file mode 100644 index 0000000..4ed94c3 Binary files /dev/null and b/src/Yammuna Express/shots/dash.png differ diff --git a/src/Yammuna Express/shots/featured.png b/src/Yammuna Express/shots/featured.png new file mode 100644 index 0000000..38691fe Binary files /dev/null and b/src/Yammuna Express/shots/featured.png differ diff --git a/src/Yammuna Express/shots/journey.png b/src/Yammuna Express/shots/journey.png new file mode 100644 index 0000000..f1f02dd Binary files /dev/null and b/src/Yammuna Express/shots/journey.png differ diff --git a/src/Yammuna Express/shots/login-allot.png b/src/Yammuna Express/shots/login-allot.png new file mode 100644 index 0000000..bc64181 Binary files /dev/null and b/src/Yammuna Express/shots/login-allot.png differ diff --git a/src/Yammuna Express/shots/login.png b/src/Yammuna Express/shots/login.png new file mode 100644 index 0000000..0ebe475 Binary files /dev/null and b/src/Yammuna Express/shots/login.png differ diff --git a/src/Yammuna Express/shots/plot.png b/src/Yammuna Express/shots/plot.png new file mode 100644 index 0000000..58aa530 Binary files /dev/null and b/src/Yammuna Express/shots/plot.png differ diff --git a/src/Yammuna Express/shots/plot2.png b/src/Yammuna Express/shots/plot2.png new file mode 100644 index 0000000..71332f7 Binary files /dev/null and b/src/Yammuna Express/shots/plot2.png differ diff --git a/src/Yammuna Express/shots/progress.png b/src/Yammuna Express/shots/progress.png new file mode 100644 index 0000000..e456cb5 Binary files /dev/null and b/src/Yammuna Express/shots/progress.png differ diff --git a/src/Yammuna Express/shots/reg.png b/src/Yammuna Express/shots/reg.png new file mode 100644 index 0000000..1fcb411 Binary files /dev/null and b/src/Yammuna Express/shots/reg.png differ diff --git a/src/Yammuna Express/shots/team.png b/src/Yammuna Express/shots/team.png new file mode 100644 index 0000000..26dadb7 Binary files /dev/null and b/src/Yammuna Express/shots/team.png differ diff --git a/src/Yammuna Express/styles.css b/src/Yammuna Express/styles.css new file mode 100644 index 0000000..15d07c9 --- /dev/null +++ b/src/Yammuna Express/styles.css @@ -0,0 +1,269 @@ +/* ============================================================ + YEIDA Portal — Design System + Premium government-tech: navy / blue / emerald on clean white + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700;800&family=Public+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap'); + +:root { + /* Brand */ + --navy-900: #081a39; + --navy-800: #0b2548; + --navy-700: #103263; + --navy-600: #16407e; + --blue-600: #1d5fbf; + --blue-700: #16407e; + --blue-500: #2670e6; + --blue-400: #5a93f0; + --blue-100: #d8e6fb; + --blue-50: #eef4ff; + --emerald-700: #0a7d56; + --emerald-600: #0f9d6b; + --emerald-500: #16b67e; + --emerald-100: #cdeede; + --emerald-50: #e8f7f0; + --amber-600: #c77a08; + --amber-500: #e8930c; + --amber-50: #fdf3e2; + --red-600: #d23f3f; + --red-500: #e85656; + --red-50: #fdedee; + + /* Neutrals */ + --ink: #0e1b2e; + --ink-2: #2b3a4f; + --muted: #5d6f87; + --faint: #8a99 ac; + --faint: #8a99ac; + --line: #e4eaf1; + --line-2: #eef2f7; + --bg: #f4f7fb; + --bg-2: #eef3f9; + --card: #ffffff; + --ink-on-navy: #eaf1fb; + + /* Type */ + --font-display: 'Schibsted Grotesk', system-ui, sans-serif; + --font-ui: 'Public Sans', system-ui, -apple-system, sans-serif; + + /* Radii */ + --r-sm: 8px; + --r-md: 11px; + --r-lg: 16px; + --r-xl: 22px; + --r-pill: 999px; + + /* Shadows */ + --sh-xs: 0 1px 2px rgba(13,38,76,.05); + --sh-sm: 0 1px 2px rgba(13,38,76,.05), 0 4px 12px -4px rgba(13,38,76,.06); + --sh-md: 0 8px 24px -8px rgba(13,38,76,.13), 0 2px 6px -2px rgba(13,38,76,.05); + --sh-lg: 0 24px 48px -16px rgba(11,37,72,.26), 0 6px 16px -8px rgba(11,37,72,.16); + --sh-glow: 0 10px 28px -8px rgba(29,95,191,.42); + + --sidebar-w: 252px; + --topbar-h: 64px; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: var(--font-ui); + color: var(--ink); + background: var(--bg); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} +#root { min-height: 100vh; } + +h1,h2,h3,h4,h5 { font-family: var(--font-display); margin: 0; color: var(--ink); letter-spacing: -.015em; } +p { margin: 0; } +button { font-family: inherit; cursor: pointer; } +a { color: inherit; text-decoration: none; } +input, select, textarea { font-family: inherit; } +::selection { background: var(--blue-100); } + +.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; } + +/* Scrollbars */ +*::-webkit-scrollbar { width: 10px; height: 10px; } +*::-webkit-scrollbar-thumb { background: #cdd7e4; border-radius: 99px; border: 2px solid transparent; background-clip: padding-box; } +*::-webkit-scrollbar-thumb:hover { background: #b4c2d4; background-clip: padding-box; } +*::-webkit-scrollbar-track { background: transparent; } + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 9px; + font-family: var(--font-ui); font-weight: 600; font-size: 14.5px; + padding: 11px 18px; border-radius: var(--r-md); border: 1px solid transparent; + background: transparent; color: var(--ink); white-space: nowrap; + transition: transform .12s ease, box-shadow .18s ease, background .18s ease, border-color .18s ease, color .15s ease; + line-height: 1; +} +.btn:active { transform: translateY(1px); } +.btn svg { width: 18px; height: 18px; flex: none; } + +.btn-primary { + background: #0077FF; + color: #fff; box-shadow: 0 10px 24px -8px rgba(0,119,255,.5); + border-radius: var(--r-pill) !important; +} +.btn-primary:hover { box-shadow: 0 14px 32px -8px rgba(0,119,255,.6); filter: brightness(1.05); } + +.btn-navy { background: linear-gradient(180deg, var(--navy-700), var(--navy-800)); color: #fff; box-shadow: 0 10px 24px -10px rgba(8,26,57,.6); } +.btn-navy:hover { filter: brightness(1.1); } + +.btn-emerald { background: linear-gradient(180deg, var(--emerald-500), var(--emerald-600)); color: #fff; box-shadow: 0 10px 26px -10px rgba(15,157,107,.55); } +.btn-emerald:hover { filter: brightness(1.05); } + +.btn-outline { background: #fff; border-color: var(--line); color: var(--ink-2); box-shadow: var(--sh-xs); } +.btn-outline:hover { border-color: #c9d6e6; background: #fbfdff; } + +.btn-ghost { color: var(--muted); } +.btn-ghost:hover { background: var(--bg-2); color: var(--ink); } + +.btn-sm { padding: 8px 13px; font-size: 13.5px; border-radius: var(--r-sm); } +.btn-sm svg { width: 16px; height: 16px; } +.btn-lg { padding: 14px 24px; font-size: 16px; } +.btn-block { width: 100%; } + +/* ---------- Cards ---------- */ +.card { + background: var(--card); border: 1px solid var(--line); + border-radius: var(--r-lg); box-shadow: var(--sh-sm); +} +.card-pad { padding: 22px; } +.card-hover { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease; } +.card-hover:hover { box-shadow: var(--sh-md); border-color: #d6e0ee; transform: translateY(-2px); } + +/* ---------- Badges / chips ---------- */ +.badge { + display: inline-flex; align-items: center; gap: 6px; + font-size: 12.5px; font-weight: 600; padding: 4px 10px; border-radius: var(--r-pill); + line-height: 1.3; white-space: nowrap; +} +.badge .dot { width: 6px; height: 6px; border-radius: 99px; background: currentColor; } +.badge-green { background: var(--emerald-50); color: var(--emerald-700); } +.badge-blue { background: var(--blue-50); color: var(--blue-600); } +.badge-amber { background: var(--amber-50); color: var(--amber-600); } +.badge-red { background: var(--red-50); color: var(--red-600); } +.badge-gray { background: var(--bg-2); color: var(--muted); } +.badge-navy { background: #e9eefb; color: var(--navy-700); } + +.chip { + display: inline-flex; align-items: center; gap: 7px; cursor: pointer; + font-size: 13.5px; font-weight: 500; padding: 8px 14px; border-radius: var(--r-pill); + background: #fff; border: 1px solid var(--line); color: var(--ink-2); + transition: all .15s ease; +} +.chip:hover { border-color: #c9d6e6; background: #fbfdff; } +.chip.active { background: var(--navy-800); color: #fff; border-color: var(--navy-800); } + +/* ---------- Inputs ---------- */ +.field { display: flex; flex-direction: column; gap: 7px; } +.field label { font-size: 13px; font-weight: 600; color: var(--ink-2); } +.input, .select { + width: 100%; padding: 12px 14px; font-size: 14.5px; color: var(--ink); + background: #fff; border: 1.5px solid var(--line); border-radius: var(--r-md); + transition: border-color .15s ease, box-shadow .15s ease; outline: none; +} +.input::placeholder { color: #9aa9bc; } +.input:focus, .select:focus { border-color: var(--blue-400); box-shadow: 0 0 0 4px rgba(38,112,230,.12); } +.input-icon { position: relative; } +.input-icon svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #9aa9bc; } +.input-icon .input { padding-left: 42px; } + +/* ---------- Sidebar nav ---------- */ +.nav-item { + display: flex; align-items: center; gap: 12px; width: 100%; + padding: 9.5px 12px; border-radius: var(--r-md); border: none; background: transparent; + color: #b9c7dd; font-size: 14.5px; font-weight: 500; text-align: left; + transition: background .15s ease, color .15s ease; position: relative; +} +.nav-item svg { width: 19px; height: 19px; flex: none; } +.nav-item:hover { background: rgba(255,255,255,.06); color: #eef3fb; } +.nav-item.active { background: rgba(255,255,255,.1); color: #fff; font-weight: 600; } +.nav-item.active::before { + content: ''; position: absolute; left: -12px; top: 50%; transform: translateY(-50%); + width: 3.5px; height: 22px; border-radius: 99px; background: var(--emerald-500); +} +.nav-badge { margin-left: auto; font-size: 11px; font-weight: 700; background: var(--emerald-500); color: #042e1f; padding: 1px 7px; border-radius: 99px; } + +/* ---------- Misc ---------- */ +.divider { height: 1px; background: var(--line); border: none; } +.kicker { font-size: 12.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--blue-600); } +.muted { color: var(--muted); } +.faint { color: #93a2b6; } + +.avatar { border-radius: 99px; object-fit: cover; background: linear-gradient(135deg,var(--blue-500),var(--navy-700)); display: inline-flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; } + +.progress { height: 8px; background: var(--bg-2); border-radius: 99px; overflow: hidden; } +.progress > span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg,var(--emerald-500),var(--emerald-600)); transition: width .6s cubic-bezier(.2,.7,.3,1); } + +/* Animations are OPT-IN via body.anim-on (added by JS only when the document is visible). + Default/hidden/print state shows content fully so nothing ever sticks at opacity:0. */ +@keyframes fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes popIn { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } } +@keyframes shimmer { 100% { transform: translateX(100%); } } +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: .35; } } +body.anim-on .anim-fade-up { animation: fadeUp .5s cubic-bezier(.2,.7,.3,1) both; } +body.anim-on .anim-fade-in { animation: fadeIn .4s ease both; } +body.anim-on .anim-pop { animation: popIn .35s cubic-bezier(.2,.8,.3,1) both; } +body.anim-on .stagger > * { animation: fadeUp .5s cubic-bezier(.2,.7,.3,1) both; } +.stagger > *:nth-child(1){animation-delay:.03s} +.stagger > *:nth-child(2){animation-delay:.08s} +.stagger > *:nth-child(3){animation-delay:.13s} +.stagger > *:nth-child(4){animation-delay:.18s} +.stagger > *:nth-child(5){animation-delay:.23s} +.stagger > *:nth-child(6){animation-delay:.28s} +.stagger > *:nth-child(7){animation-delay:.33s} +.stagger > *:nth-child(8){animation-delay:.38s} + +/* Tables */ +.tbl { width: 100%; border-collapse: collapse; } +.tbl th { text-align: left; font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); padding: 12px 16px; border-bottom: 1px solid var(--line); } +.tbl td { padding: 14px 16px; border-bottom: 1px solid var(--line-2); font-size: 14px; color: var(--ink-2); } +.tbl tbody tr { transition: background .12s ease; } +.tbl tbody tr:hover { background: #fafcfe; } +.tbl tbody tr:last-child td { border-bottom: none; } + +/* Utility layout */ +.row { display: flex; align-items: center; } +.col { display: flex; flex-direction: column; } +.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.gap-5{gap:20px}.gap-6{gap:24px} +.grow{flex:1}.center{align-items:center;justify-content:center} +.spread{justify-content:space-between} + +/* Tooltip-ish */ +.icon-btn { + width: 38px; height: 38px; border-radius: var(--r-md); border: 1px solid transparent; + display: inline-flex; align-items: center; justify-content: center; background: transparent; color: var(--muted); + transition: background .15s, color .15s, border-color .15s; position: relative; +} +.icon-btn:hover { background: var(--bg-2); color: var(--ink); } +.icon-btn svg { width: 20px; height: 20px; } +.notif-dot { position: absolute; top: 7px; right: 8px; width: 8px; height: 8px; border-radius: 99px; background: var(--red-500); border: 2px solid #fff; } + +/* ---------- Dashboard polish ---------- */ +/* premium stat/status card: hairline top accent + refined hover */ +.stat-card { position: relative; overflow: hidden; } +.stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--accent, var(--blue-500)); opacity: .9; } +.stat-card { transition: box-shadow .22s ease, transform .22s ease, border-color .22s ease; } +.stat-card:hover { box-shadow: var(--sh-md); transform: translateY(-2px); border-color: #d6e0ee; } + +/* section eyebrow heading */ +.sec-eyebrow { display: inline-flex; align-items: center; gap: 8px; font-size: 11.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--blue-600); } +.sec-eyebrow::before { content: ''; width: 22px; height: 2px; border-radius: 2px; background: var(--blue-500); } + +/* refined module switch card */ +.mod-switch { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease, background .2s ease; } +.mod-switch:hover { transform: translateY(-2px); box-shadow: var(--sh-md); } + +/* subtle metric tile */ +.metric-tile { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease; } +.metric-tile:hover { box-shadow: var(--sh-md); transform: translateY(-2px); border-color: #d6e0ee; } + +/* table refinement on dashboard */ +.tbl.tbl-clean th { background: var(--bg); } + diff --git a/src/Yammuna Express/transfer.jsx b/src/Yammuna Express/transfer.jsx new file mode 100644 index 0000000..57bdec4 --- /dev/null +++ b/src/Yammuna Express/transfer.jsx @@ -0,0 +1,208 @@ +/* Transfer Plot module */ +(function () { + const { useState } = React; + const Icon = window.Icon, PageHead = window.PageHead; + const D = window.YEIDA, fmt = D.fmtINR; + + const STEPS = ['Eligibility', 'Requirements', 'Fee Calculator', 'Application', 'Review']; + + function Transfer({ go, toast }) { + const [step, setStep] = useState(0); + const feeTotal = D.transferFees.reduce((s, f) => s + f.amount, 0); + + return ( +
+ + + {/* progress */} +
+ {STEPS.map((s, i) => ( + + + {i < STEPS.length - 1 && } + + ))} +
+ +
+ {step === 0 && setStep(1)} />} + {step === 1 && } + {step === 2 && } + {step === 3 && } + {step === 4 && } +
+ +
+ + {step < 4 && } +
+
+ ); + } + + function Eligibility({ next }) { + const checks = [ + ['5-year lock-in period completed', true, 'Allotted Mar 2019 · 7 years held'], + ['No outstanding transfer restrictions', true, 'Clear'], + ['Lease deed registered', true, 'Registered Sep 2019'], + ['Pending dues cleared before transfer', false, '₹1,47,585 to be cleared first'], + ]; + const eligible = true; + return ( +
+
+

Eligibility Check

+

We verified your plot against transfer policy 2026.

+
+ {checks.map(([l, ok, note], i) => ( +
+
+ +
{l}
{note}
+
+ {ok ? 'Passed' : 'Action needed'} +
+ ))} +
+
+
+ +

You're eligible to transfer

+

Your plot meets the core eligibility criteria. Clear pending dues to proceed to fee payment and application.

+ +
+
+ ); + } + + function Requirements() { + const reqs = [ + ['Transferor (Seller) Documents', ['PAN & Aadhaar', 'Original allotment letter', 'Latest dues clearance', 'No-objection affidavit']], + ['Transferee (Buyer) Documents', ['PAN & Aadhaar', 'Photographs (2)', 'Address proof', 'Acceptance undertaking']], + ['Joint / Common', ['Registered transfer deed', 'Sale agreement', 'Stamp duty receipt', 'Transfer fee challan']], + ]; + return ( +
+ {reqs.map(([title, items], i) => ( +
+

{title}

+
+ {items.map(it =>
{it}
)} +
+
+ ))} +
+ ); + } + + function FeeCalc({ fees, total }) { + const [value, setValue] = useState(7725000); + const transfer = Math.round(value * 0.025); + const computed = transfer + 5000 + 2500 + 1350; + return ( +
+
+

Fee Calculator

+
+ +
setValue(+e.target.value || 0)} />
+ setValue(+e.target.value)} style={{ accentColor: 'var(--blue-600)', marginTop: 10 }} /> +
₹50 L₹1.2 Cr
+
+
+
Transfer Charges (2.5%)
+
{fmt(transfer)}
+
+
+
+

Fee Breakdown

+
+ {[['Transfer Charges (2.5% of value)', transfer], ['Processing Fee', 5000], ['Documentation Charges', 2500], ['GST (18% on fees)', 1350]].map(([l, a], i) => ( +
+ {l}{fmt(a)} +
+ ))} +
+ Total Payable + {fmt(computed)} +
+
+ +
+
+ ); + } + + function Application() { + const fields = [ + ['Transferee Full Name', 'user', 'Enter buyer name'], + ['Transferee Mobile', 'phone', '+91'], + ['Transferee Email', 'mail', 'buyer@email.com'], + ['Transferee PAN', 'file', 'ABCDE1234F'], + ['Relationship to Transferor', 'user', 'e.g. Third party'], + ['Reason for Transfer', 'info', 'e.g. Sale'], + ]; + return ( +
+
+

Transfer Application

+
+ {fields.map(([l, ic, ph]) => ( +
+ ))} +
+
+
+

Upload Documents

+
+ +
Upload transfer deed & KYC
+
PDF · up to 25 MB
+
+ {['Transfer Deed', 'Buyer KYC', 'Sale Agreement'].map(d => ( +
+ {d} + +
+ ))} +
+
+ ); + } + + function Review({ total, go, toast }) { + const [submitted, setSubmitted] = useState(false); + if (submitted) return ( +
+
+

Transfer application submitted

+

Application TR-2026-18-0472 is now with the Authority for review. You can track approval status from your dashboard.

+
+
+ ); + return ( +
+
+

Review & Submit

+ {[['Plot', 'B-47, Sector 18'], ['Transferor', 'Rajeev Malhotra'], ['Transferee', 'New Allottee'], ['Transfer Value', fmt(7725000)], ['Total Fees', fmt(201975)], ['Documents', '3 uploaded']].map(([l, v]) => ( +
{l}{v}
+ ))} + +
+
+

Approval Tracking

+

After submission your application moves through:

+ {['Document verification', 'Dues clearance check', 'Authority approval', 'Transfer completion'].map((s, i) => ( +
{i + 1}{s}
+ ))} + +
+
+ ); + } + + window.Transfer = Transfer; +})(); diff --git a/src/Yammuna Express/ui.jsx b/src/Yammuna Express/ui.jsx new file mode 100644 index 0000000..7933366 --- /dev/null +++ b/src/Yammuna Express/ui.jsx @@ -0,0 +1,143 @@ +/* Shared UI primitives — Logo, charts, helpers. Exposed on window. */ +(function () { + const { useState, useEffect, useRef } = React; + const Icon = window.Icon; + + /* YEIDA logo mark */ + function Logo({ size = 38, light = false, showText = true, sub = true }) { + const fg = light ? '#fff' : 'var(--navy-800)'; + return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 11 } }, + React.createElement('img', { + src: 'assets/yeida-logo.png', alt: 'YEIDA', + style: { + width: size * 1.08, height: size * 1.08, flex: 'none', objectFit: 'contain', display: 'block', + filter: light ? 'drop-shadow(0 2px 6px rgba(0,0,0,.35))' : 'drop-shadow(0 2px 5px rgba(16,49,99,.25))', + } + }), + showText && React.createElement('div', { style: { lineHeight: 1.05 } }, + React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size * 0.46, color: fg, letterSpacing: '-.01em' } }, 'YEIDA'), + sub && React.createElement('div', { style: { fontSize: size * 0.235, fontWeight: 600, color: light ? 'rgba(234,241,251,.7)' : 'var(--muted)', letterSpacing: '.02em', marginTop: 1 } }, 'Yamuna Expressway Authority') + ) + ); + } + + /* Avatar */ + function Avatar({ initials = 'RM', size = 38, style }) { + return React.createElement('div', { + className: 'avatar', + style: { width: size, height: size, fontSize: size * 0.4, ...style } + }, initials); + } + + /* Line chart with gradient fill + animated draw */ + function LineChart({ data, w = 520, h = 150, color = 'var(--blue-500)', fill = 'rgba(38,112,230,.12)', pad = 8 }) { + const max = Math.max(...data) * 1.12, min = Math.min(...data) * 0.85; + const range = max - min || 1; + const stepX = (w - pad * 2) / (data.length - 1); + const pts = data.map((d, i) => [pad + i * stepX, pad + (h - pad * 2) * (1 - (d - min) / range)]); + const line = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); + const area = `${line} L${pts[pts.length - 1][0]},${h - pad} L${pts[0][0]},${h - pad} Z`; + const ref = useRef(null); + useEffect(() => { + if (ref.current) { + const len = ref.current.getTotalLength(); + ref.current.style.strokeDasharray = len; + ref.current.style.strokeDashoffset = len; + ref.current.getBoundingClientRect(); + ref.current.style.transition = 'stroke-dashoffset 1.1s cubic-bezier(.3,.7,.3,1)'; + ref.current.style.strokeDashoffset = 0; + } + }, []); + return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, preserveAspectRatio: 'none', style: { display: 'block' } }, + React.createElement('path', { d: area, fill }), + React.createElement('path', { ref, d: line, fill: 'none', stroke: color, strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round' }), + pts.map((p, i) => React.createElement('circle', { key: i, cx: p[0], cy: p[1], r: i === pts.length - 1 ? 4 : 0, fill: '#fff', stroke: color, strokeWidth: 2.5 })) + ); + } + + /* Mini bar chart */ + function BarChart({ data, w = 240, h = 70, color = 'var(--emerald-500)', gap = 5 }) { + const max = Math.max(...data) || 1; + const bw = (w - gap * (data.length - 1)) / data.length; + return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, style: { display: 'block' } }, + data.map((d, i) => { + const bh = Math.max(3, (d / max) * h); + return React.createElement('rect', { + key: i, x: i * (bw + gap), y: h - bh, width: bw, height: bh, rx: 3, + fill: i === data.length - 1 ? color : 'var(--blue-100)', + style: { animation: `growBar .6s ${i * 0.05}s both` } + }); + }) + ); + } + + /* Donut progress */ + function Donut({ value, size = 116, stroke = 12, color = 'var(--emerald-500)', track = 'var(--bg-2)', label, sub }) { + const r = (size - stroke) / 2, c = 2 * Math.PI * r; + const target = c - (value / 100) * c; + const [off, setOff] = useState(typeof document !== 'undefined' && document.hidden ? target : c); + useEffect(() => { const t = setTimeout(() => setOff(target), 100); return () => clearTimeout(t); }, [target]); + return React.createElement('div', { style: { position: 'relative', width: size, height: size } }, + React.createElement('svg', { width: size, height: size, style: { transform: 'rotate(-90deg)' } }, + React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: track, strokeWidth: stroke }), + React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: color, strokeWidth: stroke, strokeLinecap: 'round', strokeDasharray: c, strokeDashoffset: off, style: { transition: 'stroke-dashoffset 1s cubic-bezier(.3,.7,.3,1)' } }) + ), + React.createElement('div', { style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center' } }, + React.createElement('div', null, + React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size * 0.26, color: 'var(--ink)' } }, label != null ? label : value + '%'), + sub && React.createElement('div', { style: { fontSize: 11.5, color: 'var(--muted)', fontWeight: 600, marginTop: 1 } }, sub) + ) + ) + ); + } + + /* Section header with kicker */ + function PageHead({ title, subtitle, children }) { + return React.createElement('div', { className: 'row spread', style: { marginBottom: 24, gap: 16, flexWrap: 'wrap' } }, + React.createElement('div', null, + React.createElement('h1', { style: { fontSize: 27, fontWeight: 800 } }, title), + subtitle && React.createElement('p', { className: 'muted', style: { fontSize: 14.5, marginTop: 5 } }, subtitle) + ), + children && React.createElement('div', { className: 'row gap-3' }, children) + ); + } + + /* Toast host */ + function useToast() { + const [toasts, setToasts] = useState([]); + const push = (msg, kind = 'success') => { + const id = Math.random(); + setToasts(t => [...t, { id, msg, kind }]); + setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3200); + }; + const node = React.createElement('div', { style: { position: 'fixed', bottom: 24, right: 24, display: 'flex', flexDirection: 'column', gap: 10, zIndex: 9000 } }, + toasts.map(t => React.createElement('div', { + key: t.id, className: 'anim-pop', + style: { + display: 'flex', alignItems: 'center', gap: 11, padding: '13px 17px', borderRadius: 12, + background: 'var(--navy-800)', color: '#fff', boxShadow: 'var(--sh-lg)', fontSize: 14, fontWeight: 500, maxWidth: 360, + } + }, + React.createElement('span', { style: { display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 99, background: t.kind === 'success' ? 'var(--emerald-500)' : 'var(--blue-500)', flex: 'none' } }, + React.createElement(Icon, { name: t.kind === 'success' ? 'check' : 'info', size: 14 })), + t.msg + )) + ); + return [node, push]; + } + + /* Authority status badge — maps a backend status code to a labelled, coloured chip */ + function StatusBadge({ code, size, withIcon = true }) { + const D = window.YEIDA; + const s = (D && D.STATUS_CODES && D.STATUS_CODES[code]) || { label: code, tone: 'gray', icon: 'info' }; + return React.createElement('span', { className: 'badge badge-' + s.tone, style: size === 'lg' ? { fontSize: 13, padding: '6px 12px' } : undefined }, + withIcon && React.createElement(Icon, { name: s.icon, size: 13 }), s.label); + } + + Object.assign(window, { Logo, Avatar, LineChart, BarChart, Donut, PageHead, useToast, StatusBadge }); +})(); + +/* keyframes for bars */ +const __s = document.createElement('style'); +__s.textContent = '@keyframes growBar{from{transform:scaleY(0);transform-origin:bottom}to{transform:scaleY(1);transform-origin:bottom}}'; +document.head.appendChild(__s); diff --git a/src/Yammuna Express/uploads/image 39.png b/src/Yammuna Express/uploads/image 39.png new file mode 100644 index 0000000..a6b65fb Binary files /dev/null and b/src/Yammuna Express/uploads/image 39.png differ diff --git a/src/Yammuna Express/uploads/pasted-1780921787069-0.png b/src/Yammuna Express/uploads/pasted-1780921787069-0.png new file mode 100644 index 0000000..fe589de Binary files /dev/null and b/src/Yammuna Express/uploads/pasted-1780921787069-0.png differ diff --git a/src/Yammuna Express/uploads/pasted-1780995167628-0.png b/src/Yammuna Express/uploads/pasted-1780995167628-0.png new file mode 100644 index 0000000..bc16a86 Binary files /dev/null and b/src/Yammuna Express/uploads/pasted-1780995167628-0.png differ diff --git a/src/app/(portal)/layout.tsx b/src/app/(portal)/layout.tsx new file mode 100644 index 0000000..22d67b0 --- /dev/null +++ b/src/app/(portal)/layout.tsx @@ -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 ( + + + {/* Same fonts as the main website. Loaded via (not @import) so + they survive Next's CSS processing. */} + + + + + {children} + + ); +} diff --git a/src/app/(portal)/portal/(app)/assistant/page.tsx b/src/app/(portal)/portal/(app)/assistant/page.tsx new file mode 100644 index 0000000..41e1c26 --- /dev/null +++ b/src/app/(portal)/portal/(app)/assistant/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/construction/page.tsx b/src/app/(portal)/portal/(app)/construction/page.tsx new file mode 100644 index 0000000..0ee0c86 --- /dev/null +++ b/src/app/(portal)/portal/(app)/construction/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/dashboard/page.tsx b/src/app/(portal)/portal/(app)/dashboard/page.tsx new file mode 100644 index 0000000..939e359 --- /dev/null +++ b/src/app/(portal)/portal/(app)/dashboard/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/documents/page.tsx b/src/app/(portal)/portal/(app)/documents/page.tsx new file mode 100644 index 0000000..9cb79b9 --- /dev/null +++ b/src/app/(portal)/portal/(app)/documents/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/journey/page.tsx b/src/app/(portal)/portal/(app)/journey/page.tsx new file mode 100644 index 0000000..1d710a8 --- /dev/null +++ b/src/app/(portal)/portal/(app)/journey/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/layout.tsx b/src/app/(portal)/portal/(app)/layout.tsx new file mode 100644 index 0000000..a355e9e --- /dev/null +++ b/src/app/(portal)/portal/(app)/layout.tsx @@ -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 ( + + + {children} + + {toastNode} + + ); +} diff --git a/src/app/(portal)/portal/(app)/myjourney/page.tsx b/src/app/(portal)/portal/(app)/myjourney/page.tsx new file mode 100644 index 0000000..c62275a --- /dev/null +++ b/src/app/(portal)/portal/(app)/myjourney/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/notices/page.tsx b/src/app/(portal)/portal/(app)/notices/page.tsx new file mode 100644 index 0000000..1f0178d --- /dev/null +++ b/src/app/(portal)/portal/(app)/notices/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/payments/page.tsx b/src/app/(portal)/portal/(app)/payments/page.tsx new file mode 100644 index 0000000..683ad79 --- /dev/null +++ b/src/app/(portal)/portal/(app)/payments/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/plot/page.tsx b/src/app/(portal)/portal/(app)/plot/page.tsx new file mode 100644 index 0000000..4d08903 --- /dev/null +++ b/src/app/(portal)/portal/(app)/plot/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/profile/page.tsx b/src/app/(portal)/portal/(app)/profile/page.tsx new file mode 100644 index 0000000..8ebe6b4 --- /dev/null +++ b/src/app/(portal)/portal/(app)/profile/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/sampleplans/page.tsx b/src/app/(portal)/portal/(app)/sampleplans/page.tsx new file mode 100644 index 0000000..374cb82 --- /dev/null +++ b/src/app/(portal)/portal/(app)/sampleplans/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/sectormaps/page.tsx b/src/app/(portal)/portal/(app)/sectormaps/page.tsx new file mode 100644 index 0000000..62cb715 --- /dev/null +++ b/src/app/(portal)/portal/(app)/sectormaps/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/support/page.tsx b/src/app/(portal)/portal/(app)/support/page.tsx new file mode 100644 index 0000000..9a32b71 --- /dev/null +++ b/src/app/(portal)/portal/(app)/support/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/(app)/transfer/page.tsx b/src/app/(portal)/portal/(app)/transfer/page.tsx new file mode 100644 index 0000000..b27d9cd --- /dev/null +++ b/src/app/(portal)/portal/(app)/transfer/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/login/allotment/page.tsx b/src/app/(portal)/portal/login/allotment/page.tsx new file mode 100644 index 0000000..3b4c3a2 --- /dev/null +++ b/src/app/(portal)/portal/login/allotment/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/login/page.tsx b/src/app/(portal)/portal/login/page.tsx new file mode 100644 index 0000000..cabd490 --- /dev/null +++ b/src/app/(portal)/portal/login/page.tsx @@ -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 ( + <> +
+ + + ); +} diff --git a/src/app/(portal)/portal/maps/page.tsx b/src/app/(portal)/portal/maps/page.tsx new file mode 100644 index 0000000..36a43aa --- /dev/null +++ b/src/app/(portal)/portal/maps/page.tsx @@ -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 ( +
+
+ +
+ + +
+
+
+ + +
+
+ ); +} diff --git a/src/app/(portal)/portal/page.tsx b/src/app/(portal)/portal/page.tsx new file mode 100644 index 0000000..934676e --- /dev/null +++ b/src/app/(portal)/portal/page.tsx @@ -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'); +} diff --git a/src/app/(portal)/portal/register/page.tsx b/src/app/(portal)/portal/register/page.tsx new file mode 100644 index 0000000..814bb84 --- /dev/null +++ b/src/app/(portal)/portal/register/page.tsx @@ -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 ; +} diff --git a/src/app/(portal)/portal/verify/page.tsx b/src/app/(portal)/portal/verify/page.tsx new file mode 100644 index 0000000..8e13f01 --- /dev/null +++ b/src/app/(portal)/portal/verify/page.tsx @@ -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 ; +} diff --git a/src/app/about/page.tsx b/src/app/(site)/about/page.tsx similarity index 80% rename from src/app/about/page.tsx rename to src/app/(site)/about/page.tsx index da87385..ed52bc4 100644 --- a/src/app/about/page.tsx +++ b/src/app/(site)/about/page.tsx @@ -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() {
- {LEADERS[0].name} +
{LEADERS[0].name}
-

{LEADERS[0].name}

- {LEADERS[0].role} +

{LEADERS[0].name}

+ {LEADERS[0].role}
{LEADERS[0].email}
- Registered Contractor + Registered
Contractor
- {LEADERS[1].name} +
{LEADERS[1].name}
-

{LEADERS[1].name}

- {LEADERS[1].role} +

{LEADERS[1].name}

+ {LEADERS[1].role}
{LEADERS[1].email}
@@ -145,9 +146,12 @@ export default function AboutPage() { {TEAM.map((m) => (
{m.name}
-

{m.name}

- {m.role} +
+
+

{m.name}

+ {m.role}
{m.email} +
))}
@@ -157,7 +161,7 @@ export default function AboutPage() { {/* Experts */}
-
+

Expert Team

Meet Our Experts

Award-winning architects, experienced engineers, and dedicated construction professionals committed to excellence

@@ -170,15 +174,18 @@ export default function AboutPage() {

{e.t}

{e.d}

- +
))}
{BADGES.map((b) => (
- {b.n} - {b.t} +
+ {b.n} + {b.t} +
+
))}
@@ -195,12 +202,19 @@ export default function AboutPage() {
{STEPS.map((s, i) => ( -
- {i === 3 && } - {s.n} -

{s.t}

-

{s.d}

-
+ + {i === 5 && ( +
+ +
+ )} +
+ {i === 3 && } + {s.n} +

{s.t}

+

{s.d}

+
+
))}
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/(site)/blog/[slug]/page.tsx similarity index 100% rename from src/app/blog/[slug]/page.tsx rename to src/app/(site)/blog/[slug]/page.tsx diff --git a/src/app/blog/page.tsx b/src/app/(site)/blog/page.tsx similarity index 100% rename from src/app/blog/page.tsx rename to src/app/(site)/blog/page.tsx diff --git a/src/app/(site)/contact/ContactForm.tsx b/src/app/(site)/contact/ContactForm.tsx new file mode 100644 index 0000000..af77d0b --- /dev/null +++ b/src/app/(site)/contact/ContactForm.tsx @@ -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) => + 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 ( + <> +
+

+ Let’s connect! +

+

+ Have questions or need assistance?
We’re here to help! +

+
+
+ + + + +
+
+ + + + +
+
+ +
+
+ + + + +
+
+ + + + +
+
+ +
+ + + + +
+ +
+ + + +