Files
Yamunaexpress/src/Yammuna Express/onboarding.jsx
T
Goutam0612 783c7da7a1 Add owner portal, i18n, contact API, and region/footer assets
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 01:59:41 +05:30

264 lines
18 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 (
<div className="row gap-2" style={{ marginBottom: 24 }}>
{STEPS.map((s, i) => (
<React.Fragment key={s.k}>
<div className="row gap-2" style={{ flex: 'none' }}>
<span style={{ width: 30, height: 30, borderRadius: 99, display: 'grid', placeItems: 'center', fontWeight: 700, fontSize: 13, background: i < step ? 'var(--emerald-500)' : i === step ? 'var(--blue-600)' : 'var(--bg-2)', color: i <= step ? '#fff' : 'var(--faint)', transition: 'all .25s' }}>
{i < step ? <Icon name="check" size={15} /> : i + 1}
</span>
<span style={{ fontSize: 12.5, fontWeight: i === step ? 700 : 500, color: i === step ? 'var(--ink)' : 'var(--muted)' }}>{s.label}</span>
</div>
{i < STEPS.length - 1 && <span style={{ flex: 1, height: 2, borderRadius: 99, background: i < step ? 'var(--emerald-500)' : 'var(--line)' }} />}
</React.Fragment>
))}
</div>
);
}
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 (
<AuthShell wide>
<div className="anim-fade-up">
<button className="btn btn-ghost btn-sm" style={{ marginBottom: 14, marginLeft: -8 }} onClick={() => step === 0 ? go('login') : setStep(s => s - 1)}><Icon name="chevL" size={16} />Back</button>
<div className="kicker" style={{ marginBottom: 8 }}>New Allottee Registration</div>
<h1 style={{ fontSize: 25, fontWeight: 800, marginBottom: 20 }}>
{['Create your account', 'Identify your plot', 'Verify email & phone', 'Your addresses'][step]}
</h1>
<Stepper step={step} />
<div key={step} className="anim-fade-in">
{step === 0 && <AccountStep form={form} set={set} isAllottee={isAllottee} />}
{step === 1 && <AllotmentStep form={form} set={set} />}
{step === 2 && <VerifyStep form={form} go={next} />}
{step === 3 && <AddressStep form={form} set={set} />}
</div>
{step !== 2 && (
<button className="btn btn-primary btn-block btn-lg" style={{ marginTop: 22 }}
onClick={() => step === STEPS.length - 1 ? go('app') : next()}>
{step === 0 ? 'Create Account & Verify' : step === 3 ? 'Finish & Enter Portal' : 'Continue'}
<Icon name="arrowR" size={19} />
</button>
)}
{step === 0 && (
<div style={{ textAlign: 'center', marginTop: 16, fontSize: 14, color: 'var(--muted)' }}>
Already registered?{' '}
<button onClick={() => go('login')} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 14 }}>Sign in</button>
</div>
)}
</div>
</AuthShell>
);
}
/* Step 0 — Account creation */
function AccountStep({ form, set, isAllottee }) {
const pwMismatch = form.confirm.length > 0 && form.password !== form.confirm;
return (
<div style={{ display: 'grid', gap: 15 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<div className="field"><label>First Name</label><div className="input-icon"><Icon name="user" /><input className="input" value={form.firstName} onChange={e => set('firstName', e.target.value)} /></div></div>
<div className="field"><label>Last Name</label><div className="input-icon"><Icon name="user" /><input className="input" value={form.lastName} onChange={e => set('lastName', e.target.value)} /></div></div>
</div>
<div className="field"><label>Email Address</label><div className="input-icon"><Icon name="mail" /><input className="input" value={form.email} onChange={e => set('email', e.target.value)} placeholder="you@email.com" /></div></div>
<div className="field"><label>Mobile Number</label>
<div className="row gap-2">
<select className="select" value={form.cc} onChange={e => set('cc', e.target.value)} style={{ width: 100, flex: 'none' }}>{D.countryCodes.map(c => <option key={c.code} value={c.code}>{c.label} {c.code}</option>)}</select>
<div className="input-icon" style={{ flex: 1 }}><Icon name="phone" /><input className="input" value={form.mobile} onChange={e => set('mobile', e.target.value)} /></div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<div className="field"><label>Password</label><div className="input-icon"><Icon name="lock" /><input className="input" type="password" value={form.password} onChange={e => set('password', e.target.value)} placeholder="Min 8 characters" /></div></div>
<div className="field"><label>Confirm Password</label><div className="input-icon"><Icon name="lock" /><input className="input" type="password" value={form.confirm} onChange={e => set('confirm', e.target.value)} placeholder="Re-enter password" style={pwMismatch ? { borderColor: 'var(--red-500)' } : undefined} /></div></div>
</div>
{pwMismatch && <div className="row gap-2" style={{ fontSize: 12, color: 'var(--red-600)', marginTop: -6 }}><Icon name="alert" size={14} />Passwords do not match</div>}
{/* Relationship with Allottee */}
<div className="field">
<label>Relationship with Allottee</label>
<select className="select" value={form.relationship} onChange={e => set('relationship', e.target.value)}>
{D.relationshipOptions.map(o => <option key={o} value={o}>{o}</option>)}
</select>
</div>
<div className="row gap-3" style={{ padding: '12px 14px', borderRadius: 12, background: isAllottee ? 'var(--emerald-50)' : 'var(--blue-50)', fontSize: 12.5, color: isAllottee ? 'var(--emerald-700)' : 'var(--blue-700)' }}>
<Icon name={isAllottee ? 'shield' : 'info'} size={17} style={{ flex: 'none' }} />
<span>{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).`}</span>
</div>
{/* Allottee name — required when registrant is NOT the allottee */}
{!isAllottee && (
<div className="anim-fade-in" style={{ display: 'grid', gap: 12, padding: 16, borderRadius: 14, border: '1px dashed var(--blue-100)', background: '#fbfdff' }}>
<div className="row gap-2" style={{ fontWeight: 700, fontSize: 13.5, color: 'var(--ink-2)' }}><Icon name="user" size={16} style={{ color: 'var(--blue-600)' }} />Original Allottee Details</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<div className="field"><label>Allottee First Name</label><input className="input" value={form.allotteeFirst} onChange={e => set('allotteeFirst', e.target.value)} placeholder="As per records" /></div>
<div className="field"><label>Allottee Last Name</label><input className="input" value={form.allotteeLast} onChange={e => set('allotteeLast', e.target.value)} placeholder="As per records" /></div>
</div>
</div>
)}
<label className="row gap-3" style={{ fontSize: 13.5, color: 'var(--muted)', cursor: 'pointer' }}>
<input type="checkbox" checked={form.terms} onChange={e => set('terms', e.target.checked)} style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />
<span>I accept YEIDA's <b style={{ color: 'var(--ink-2)' }}>Terms &amp; Conditions</b> and <b style={{ color: 'var(--ink-2)' }}>Privacy Policy</b></span>
</label>
<label className="row gap-3" style={{ fontSize: 13.5, color: 'var(--muted)', cursor: 'pointer', marginTop: -4 }}>
<input type="checkbox" checked={form.cookies} onChange={e => set('cookies', e.target.checked)} style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />
<span>I consent to <b style={{ color: 'var(--ink-2)' }}>cookies</b> for a secure, personalised portal experience</span>
</label>
</div>
);
}
/* Step 1 — Allotment identification */
function AllotmentStep({ form, set }) {
return (
<div>
<h3 style={{ fontSize: 19, fontWeight: 800 }}>What is your Allotment Number?</h3>
<p className="muted" style={{ fontSize: 13.5, marginTop: 5, marginBottom: 18 }}>Enter your allotment number to link your plot, or identify it by sector &amp; plot if you don't have it handy.</p>
{/* toggle */}
<div className="row gap-2" style={{ background: 'var(--bg-2)', padding: 4, borderRadius: 12, marginBottom: 18, width: 'fit-content' }}>
{[['have', 'I have my allotment number'], ['forgot', "I don't remember it"]].map(([k, l]) => (
<button key={k} onClick={() => set('hasAllotment', k === 'have')}
style={{ padding: '8px 15px', borderRadius: 9, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', background: (form.hasAllotment === (k === 'have')) ? '#fff' : 'transparent', color: (form.hasAllotment === (k === 'have')) ? 'var(--ink)' : 'var(--muted)', boxShadow: (form.hasAllotment === (k === 'have')) ? 'var(--sh-xs)' : 'none' }}>{l}</button>
))}
</div>
{form.hasAllotment ? (
<div className="field">
<label>Allotment Number</label>
<div className="input-icon"><Icon name="building" /><input className="input" value={form.allotment} maxLength={10} onChange={e => set('allotment', e.target.value.toUpperCase())} placeholder="e.g. YEA-654321" /></div>
<span className="faint" style={{ fontSize: 12 }}>Format: YEA- followed by your 6-digit number</span>
</div>
) : (
<div>
<div className="row gap-3" style={{ padding: '11px 14px', borderRadius: 11, background: 'var(--amber-50)', fontSize: 12.5, color: 'var(--amber-600)', marginBottom: 16 }}>
<Icon name="info" size={16} style={{ flex: 'none' }} />Identify your plot below we'll match it to your allotment record.
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
<div className="field"><label>Sector</label>
<select className="select" value={form.sector} onChange={e => set('sector', e.target.value)}>{D.sectors.map(s => <option key={s.id} value={s.name}>{s.name}</option>)}</select>
</div>
<div className="field"><label>Pocket</label>
<select className="select" value={form.pocket} onChange={e => set('pocket', e.target.value)}>{['Pocket A', 'Pocket B', 'Pocket C', 'Pocket D'].map(p => <option key={p}>{p}</option>)}</select>
</div>
<div className="field"><label>Plot Number</label>
<div className="input-icon"><Icon name="plot" /><input className="input" value={form.plot} onChange={e => set('plot', e.target.value)} placeholder="34 digits" maxLength={5} /></div>
</div>
</div>
<span className="faint" style={{ fontSize: 12, marginTop: 8, display: 'block' }}>Plot number is typically 3 or 4 digits (e.g. 047 or 1024).</span>
</div>
)}
</div>
);
}
/* Step 2 — Verification */
function VerifyStep({ form, go }) {
const [both, setBoth] = useState(false);
return (
<div>
<p className="muted" style={{ fontSize: 13.5, marginTop: -6, marginBottom: 18 }}>Verify both your email and mobile number on this screen. Each requires an OTP.</p>
<VerifyPanel email={form.email} phone={form.mobile} onChange={setBoth} />
<button className="btn btn-primary btn-block btn-lg" style={{ marginTop: 20, opacity: both ? 1 : .55 }} disabled={!both} onClick={go}>{both ? 'Continue' : 'Verify email & phone to continue'}<Icon name="arrowR" size={19} /></button>
</div>
);
}
/* Step 3 — Addresses (registered + mailing, editable independently) */
function AddressStep({ form, set }) {
const [sameAsReg, setSameAsReg] = useState(false);
return (
<div style={{ display: 'grid', gap: 18 }}>
<AddressBlock title="Registered Address" sub="Official authority / allotment address" icon="building" prefix="reg" form={form} set={set} />
<label className="row gap-3" style={{ fontSize: 13.5, color: 'var(--muted)', cursor: 'pointer' }}>
<input type="checkbox" checked={sameAsReg} onChange={e => setSameAsReg(e.target.checked)} style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />
<span>My mailing address is the same as my registered address</span>
</label>
{!sameAsReg && <AddressBlock title="Mailing / Current Address" sub="Where we send communications" icon="mail" prefix="mail" form={form} set={set} />}
</div>
);
}
function AddressBlock({ title, sub, icon, prefix, form, set }) {
const def = prefix === 'reg' ? D.user.registeredAddress : D.user.mailingAddress;
return (
<div className="card" style={{ padding: 18, boxShadow: 'none', border: '1px solid var(--line)' }}>
<div className="row gap-3" style={{ marginBottom: 14 }}>
<span style={{ width: 36, height: 36, borderRadius: 9, display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name={icon} size={18} /></span>
<div><div style={{ fontWeight: 700, fontSize: 14.5 }}>{title}</div><div className="faint" style={{ fontSize: 12 }}>{sub}</div></div>
</div>
<div style={{ display: 'grid', gap: 12 }}>
<div className="field"><label>Address Line 1</label><input className="input" defaultValue={def.line1} /></div>
<div className="field"><label>Address Line 2</label><input className="input" defaultValue={def.line2} /></div>
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr .8fr', gap: 12 }}>
<div className="field"><label>City</label><input className="input" defaultValue={def.city} /></div>
<div className="field"><label>State</label><input className="input" defaultValue={def.state} /></div>
<div className="field"><label>PIN</label><input className="input num" defaultValue={def.pin} /></div>
</div>
</div>
</div>
);
}
/* 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 (
<AuthShell>
<div className="anim-fade-up">
<button className="btn btn-ghost btn-sm" style={{ marginBottom: 14, marginLeft: -8 }} onClick={() => go('login')}><Icon name="chevL" size={16} />Back</button>
<div className="kicker" style={{ marginBottom: 8 }}>Find your allotment</div>
<h1 style={{ fontSize: 25, fontWeight: 800 }}>Identify your plot</h1>
<p className="muted" style={{ fontSize: 14, marginTop: 6, marginBottom: 20 }}>No allotment number? Locate your plot by sector, pocket and plot number.</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 14 }}>
<div className="field"><label>Sector</label><select className="select" value={form.sector} onChange={e => set('sector', e.target.value)}>{D.sectors.map(s => <option key={s.id} value={s.name}>{s.name}</option>)}</select></div>
<div className="field"><label>Pocket</label><select className="select" value={form.pocket} onChange={e => set('pocket', e.target.value)}>{['Pocket A', 'Pocket B', 'Pocket C', 'Pocket D'].map(p => <option key={p}>{p}</option>)}</select></div>
</div>
<div className="field" style={{ marginBottom: 18 }}><label>Plot Number (34 digits)</label><div className="input-icon"><Icon name="plot" /><input className="input" value={form.plot} onChange={e => set('plot', e.target.value)} placeholder="e.g. 047" /></div></div>
{found ? (
<div className="anim-pop" style={{ padding: '14px 16px', borderRadius: 12, background: 'var(--emerald-50)', border: '1px solid #bfe6d3', marginBottom: 18 }}>
<div className="row gap-3"><Icon name="checkCircle" size={20} style={{ color: 'var(--emerald-600)' }} /><div><div style={{ fontWeight: 700, fontSize: 14 }}>Allotment found · YEA-654321</div><div className="muted" style={{ fontSize: 12.5 }}>{form.sector} · {form.pocket} · Plot {form.plot}</div></div></div>
</div>
) : (
<button className="btn btn-outline btn-block" style={{ marginBottom: 18 }} onClick={() => setFound(true)}><Icon name="search" size={17} />Find my allotment</button>
)}
<button className="btn btn-primary btn-block btn-lg" disabled={!found} style={{ opacity: found ? 1 : .55 }} onClick={() => go('verify')}>Continue to Verification<Icon name="arrowR" size={19} /></button>
</div>
</AuthShell>
);
}
window.Register = Register;
window.AllotmentLookup = AllotmentLookup;
})();