783c7da7a1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
264 lines
18 KiB
React
264 lines
18 KiB
React
/* 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 & 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 & 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="3–4 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 (3–4 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;
|
||
})();
|