/* @jsx React.createElement */ /* ============================================================ INDIE Music — Les Musik di Rumah (IMS Home Private) untuk murid/ortu. React 18 UMD + Babel standalone, tanpa build. Fungsi bersama di hp-common.jsx (window.HP, dimuat duluan). Data & aturan dari ims-api /api/public/hp/* (routes/hp-public.js): GET /config, GET /teachers?lat&lng&instrument&age&day, POST /request. Saklar hp_public_enabled mati -> semua endpoint 404 -> halaman tetap informatif, pencarian & formulir diganti blok "Segera hadir" + WhatsApp admin. Harga TIDAK di-hardcode: hanya tampil (dan dihitung) bila /config menjawab. Pratinjau: ?demo=1 (data contoh, lihat hp-demo.js). TAHAP 3 (hp5): "Cari guru" = wizard 3 langkah (lokasi → alat & usia → hasil peta+daftar); permintaan = lembar 4 langkah (murid → jadwal → kontak → paket & kirim). Tiap langkah ada jalan ke WA admin. Kontrak POST /request TIDAK berubah (field & tipe sama persis dengan tahap 2). ============================================================ */ const { useState, useEffect, useRef, useMemo } = React; const { waLink, rupiah, DEMO, PILOT_KM, PHONE_RE, HHMM, normPhone, pilotCheck, hpApi, friendlyError, useHpConfig, PREFERS_REDUCED, Reveal, smoothScrollTo, HpMap, cellKey, DemoBanner, DemoTag, HpNav, HpFooter, WAFloat, WaIcon, ComingSoon, Faq, StickyCta, SuccessScreen, InstIcon, areaText, useGps, useWizOn, WizardProgress, WaHelp, Sheet, Segmented, Stepper, AreaChooser, } = window.HP; const LEVEL_LABEL = { pemula: 'Pemula (belum pernah)', menengah: 'Menengah', lanjut: 'Lanjut' }; const LEVEL_SHORT = { pemula: 'Pemula', menengah: 'Menengah', lanjut: 'Lanjut' }; // Fallback hanya untuk teks informasi saat /config tidak menjawab — sama dengan INSTRUMENTS ims-api/src/hp.js. const INSTRUMENT_TEXT = 'gitar, gitar listrik (amp kecil), piano/keyboard (alat milik murid), vokal, biola, dan drum pad / practice kit'; const shortInst = (label) => String(label || '').split(' (')[0]; const DAY_SHORT = { mon: 'Sen', tue: 'Sel', wed: 'Rab', thu: 'Kam', fri: 'Jum', sat: 'Sab', sun: 'Min' }; // Pilihan waktu → jam mulai slot. Batas jam kontrak POST /request: 06:00–21:00 (hp-public.js). // /config belum mengirim batas jam; bila kelak mengirim slot_start_min/max, dipakai otomatis. const TIMES = [ { k: 'pagi', label: 'Pagi', start: '09:00' }, { k: 'siang', label: 'Siang', start: '13:00' }, { k: 'sore', label: 'Sore', start: '16:00' }, { k: 'malam', label: 'Malam', start: '19:00' }, ]; const timesFor = (cfg) => { const lo = HHMM.test(cfg.slot_start_min || '') ? cfg.slot_start_min : '06:00'; const hi = HHMM.test(cfg.slot_start_max || '') ? cfg.slot_start_max : '21:00'; return TIMES.filter((t) => t.start >= lo && t.start <= hi); }; // Petunjuk per alat — hanya fakta SPEC §3 / label INSTRUMENTS ims-api. const INST_NOTE = { gitar_listrik: 'Pakai amp kecil, disiapkan murid.', piano: 'Pakai piano/keyboard milik murid.', drum_pad: 'Drum kit penuh tidak tersedia untuk les di rumah — hanya drum pad / practice kit. Mau drum kit? Kelas di studio IMS.', }; const instName = (cfg, k) => { const i = cfg && cfg.instruments.find((x) => x.key === k); return i ? shortInst(i.label) : k; }; const dayName = (cfg, k) => { const d = cfg && cfg.days.find((x) => x.key === k); return d ? d.label : k; }; const RQ_INIT = { instrument: '', student_name: '', student_age: '', level: 'pemula', days: [], time: 'sore', area_label: '', address: '', kind: 'paket', parent_name: '', parent_phone: '', note: '', consent: false, website: '', }; /* Pesan WA ke admin dari data yang SUDAH diisi — bahasa natural, TANPA koordinat, tanpa alamat/nomor. */ function lesWaText({ cfg, q, loc, rq, picked }) { const parts = ['Halo IMS! Aku mau cari guru les musik di rumah, lebih gampang lewat chat.']; const inst = (rq && rq.instrument) || (q && q.instrument); if (inst) parts.push('Alat musik: ' + instName(cfg, inst) + '.'); const area = areaText(loc); if (area) parts.push('Area: ' + area + '.'); if (rq && rq.area_label.trim()) parts.push('Kelurahan/kecamatan: ' + rq.area_label.trim().slice(0, 80) + '.'); const age = (rq && rq.student_age) || (q && q.age); if (age) parts.push('Usia murid: ' + age + ' tahun.'); if (rq && rq.student_name.trim()) parts.push('Nama murid: ' + rq.student_name.trim().slice(0, 60) + '.'); if (picked) parts.push('Guru pilihan dari daftar: ' + picked.name + '.'); if (rq && rq.days.length) { const t = TIMES.find((x) => x.k === rq.time); parts.push('Hari yang bisa: ' + cfg.days.filter((d) => rq.days.includes(d.key)).map((d) => d.label).join(', ') + (t ? ', ' + t.label.toLowerCase() + ' (' + t.start + ')' : '') + '.'); } if (rq && rq.parent_name.trim()) parts.push('Nama saya: ' + rq.parent_name.trim().slice(0, 60) + '.'); if (!inst && !area) parts.push('Area aku: '); return parts.join(' '); } /* ───────────────────────────────────────────── Pemilih alat musik (kartu besar). ───────────────────────────────────────────── */ function InstrumentPicker({ cfg, value, onChange, allowAll = true, idPrefix = 'cari-inst', labelId, invalid, errId }) { const opts = [...(allowAll ? [{ key: '', label: 'Semua alat' }] : []), ...cfg.instruments]; const note = value && INST_NOTE[value]; return (
{opts.map((i) => ( ))}

{note ? note + ' ' : ''}Alat musik disiapkan murid.

); } /* Cek jangkauan — dihitung di perangkat dari pusat area pilot, lokasi tidak dikirim ke server. */ function CoverageCheck({ loc, maxR }) { const c = pilotCheck(loc, maxR); if (!c) return null; const where = c.area.short; if (c.tier === 'in') { return (
✓ Masuk area pilot

{c.km < 0.5 ? 'Di sekitar pusat ' + where : '±' + c.km + ' km dari pusat ' + where}. Lanjut pilih alat musik.

); } const wa = 'Halo IMS! Aku tertarik les musik di rumah, tapi lokasiku di luar area pilot (sekitar ' + Math.round(c.km) + ' km dari ' + where + '). Kabari aku kalau areaku sudah dibuka ya. Area aku: '; if (c.tier === 'edge') { return (
~ Pinggir area pilot

±{c.km} km dari pusat {where}. Masih mungkin terjangkau bila ada guru yang radiusnya sampai — lanjut saja, atau tetap kirim permintaan.

); } return (
! Belum terjangkau

±{c.km} km dari area pilot terdekat ({where}). Tinggalkan nomor WA ke admin — kami kabari saat areamu dibuka.

Kabari aku via WhatsApp
); } function SkeletonCards() { return (
{[0, 1, 2].map((i) => (
))}
); } /* Kartu guru (daftar) — nomor = badge di peta. */ function TeacherCard({ t, num, cfg, hl, onHl, onAsk }) { const k = cellKey(t.area); return (
onHl(k)} onMouseLeave={() => onHl(null)} onFocus={() => onHl(k)} onClick={() => onHl(k)}>
{num} {t.photo_url ? {'Foto : }

{t.name}

{t.area_label || 'Area sekitar'} · ±{t.distance_km} km
{t.rating != null &&
★ {Number(t.rating).toFixed(1)} rating ortu{DEMO ? ' (contoh)' : ''}
}
{(t.instruments || []).map((x) => {instName(cfg, x)})} {t.kids_ok && Bisa ajar anak}
{t.days && t.days.length > 0 &&

Biasanya bisa: {t.days.map((d) => DAY_SHORT[d] || d).join(', ')}

}
); } /* ───────────────────────────────────────────── WIZARD PENCARIAN — 3 langkah. Langkah 3 = GET /teachers (lokasi tidak disimpan server). ───────────────────────────────────────────── */ function Finder({ cfg, onClosed }) { const [step, setStep] = useState(1); const [loc, setLoc] = useState(null); const [q, setQ] = useState({ instrument: '', age: '', day: '' }); const [stepErr, setStepErr] = useState(null); const [showMap1, setShowMap1] = useState(false); const [res, setRes] = useState(null); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); const [retry, setRetry] = useState(0); const [hl, setHl] = useState(null); const [sel, setSel] = useState(null); const [view, setView] = useState('peta'); const [fitKey, setFitKey] = useState(0); const [sheet, setSheet] = useState(false); const [picked, setPicked] = useState(null); const [rq, setRq] = useState(RQ_INIT); const [sentNote, setSentNote] = useState(null); const top = useRef(null); const head = useRef(null); const listRef = useRef(null); const lastCtl = useRef(null); const first = useRef(true); const gps = useGps((l) => { setLoc(l); setStepErr(null); }); useWizOn(top); // Langkah berganti -> fokus ke judul langkah (pembaca layar) + gulir ke atas wizard bila perlu. useEffect(() => { if (first.current) { first.current = false; return; } if (head.current) head.current.focus({ preventScroll: true }); const r = top.current && top.current.getBoundingClientRect(); if (r && (r.top < 0 || r.top > window.innerHeight * 0.4)) smoothScrollTo(top.current); }, [step]); const ageN = q.age === '' ? null : Number(q.age); const ageBad = ageN !== null && (!Number.isInteger(ageN) || ageN < 3 || ageN > 90); useEffect(() => { if (step !== 3 || !loc || ageBad) return undefined; const p = new URLSearchParams({ lat: String(loc.lat), lng: String(loc.lng) }); if (q.instrument) p.set('instrument', q.instrument); if (ageN !== null) p.set('age', String(ageN)); if (q.day) p.set('day', q.day); if (lastCtl.current) lastCtl.current.abort(); const ctl = new AbortController(); lastCtl.current = ctl; setBusy(true); setErr(null); setHl(null); setSel(null); hpApi('/teachers?' + p.toString(), { signal: ctl.signal }).then((r) => { if (ctl.signal.aborted) return; setBusy(false); if (r.status === 404) { onClosed(); return; } if (!r.ok || !r.body || !Array.isArray(r.body.teachers)) { setErr(friendlyError(r)); return; } const list = r.body.teachers.slice().sort((a, b) => (a.distance_km || 0) - (b.distance_km || 0)); setRes({ teachers: list, max_radius_km: r.body.max_radius_km }); setFitKey((k) => k + 1); }); return () => ctl.abort(); }, [step, loc && loc.lat, loc && loc.lng, q.instrument, q.age, q.day, retry]); const teachers = res ? res.teachers : []; const n = teachers.length; // Sel unik (beberapa guru bisa berbagi sel ±1 km) + nomor kartu yang sama dengan daftar. const markers = useMemo(() => { const m = new Map(); teachers.forEach((t, i) => { if (!t.area) return; const k = cellKey(t.area); if (!m.has(k)) m.set(k, { key: k, lat: t.area.lat, lng: t.area.lng, nums: [], names: [], inst: q.instrument || (t.instruments || [])[0] }); const x = m.get(k); x.nums.push(i + 1); x.names.push(t.name + ' ±' + t.distance_km + ' km'); }); return [...m.values()].map((x) => ({ ...x, label: 'Area guru nomor ' + x.nums.join(' dan ') + ': ' + x.names.join('; ') + '. Area kira-kira ±1 km, bukan alamat.' })); }, [res]); const goStep = (s) => { setStepErr(null); setStep(s); }; const next1 = () => { if (!loc) { setStepErr('Pilih lokasi dulu — pakai lokasi kamu, pilih area, atau ketuk peta.'); const b = document.getElementById('wz-gps'); if (b) b.focus(); return; } goStep(2); }; const next2 = () => { if (ageBad) { setStepErr('Usia murid 3–90 tahun (atau kosongkan).'); const a = document.getElementById('wz-age'); if (a) a.focus(); return; } goStep(3); }; const ask = (t) => { setPicked(t || null); setRq((x) => ({ ...x, instrument: x.instrument || q.instrument || (t && t.instruments && t.instruments[0]) || '', student_age: x.student_age || q.age, area_label: x.area_label || (loc && loc.source === 'area' ? loc.label : ''), })); setSheet(true); }; const onMarker = (k) => { setSel(k); setHl(k); if (window.innerWidth > 980) { const c = listRef.current && listRef.current.querySelector('[data-cell="' + k + '"]'); if (c) c.scrollIntoView({ behavior: PREFERS_REDUCED ? 'auto' : 'smooth', block: 'nearest' }); } }; const clear = { instrument: { label: () => instName(cfg, q.instrument), go: () => setQ({ ...q, instrument: '' }), hint: 'Semua alat musik' }, age: { label: () => 'Usia ' + q.age, go: () => setQ({ ...q, age: '' }), hint: 'Tanpa filter usia' }, day: { label: () => dayName(cfg, q.day), go: () => setQ({ ...q, day: '' }), hint: 'Hari apa saja' }, }; const active = ['instrument', 'age', 'day'].filter((k) => (k === 'age' ? q.age !== '' : q[k])); const waText = lesWaText({ cfg, q, loc, rq: null, picked: null }); const selTeachers = sel ? teachers.map((t, i) => ({ t, num: i + 1 })).filter((x) => cellKey(x.t.area) === sel) : []; const ageHint = ageN > 0 && ageN < 17 ? 'Di bawah 17 th: hanya guru yang bersedia mengajar anak yang tampil, dan orang dewasa wajib ada di rumah selama sesi.' : 'Boleh dikosongkan. Di bawah 17 th hanya guru yang bersedia mengajar anak yang tampil.'; const TITLES = ['Di mana lokasi lesnya?', 'Alat musik & untuk siapa?', n ? n + ' guru bisa datang ke sana' : busy || !res ? 'Mencari guru terdekat…' : 'Belum ada guru yang cocok']; return (
{sentNote &&
{sentNote}
}

{TITLES[step - 1]}

{step === 1 && (

Kami tampilkan guru yang radius mengajarnya (maks {cfg.max_radius_km} km) sampai ke sana.

{ setLoc(l); setStepErr(null); }} idPrefix="wz" invalid={!!stepErr} errId="wz-err" mapSlot={(
{showMap1 && (
{ setLoc({ ...p, source: 'map', label: 'Titik yang kamu ketuk di peta' }); setStepErr(null); }} label="Peta: ketuk untuk memilih lokasi les. Pilihan area di atas berisi informasi yang sama." describedBy="wz-map1-legend" legend={[{ sw: 'pin', text: 'Lokasi kamu' }, { sw: 'pilot', text: 'Area pilot' }]} />
)}
)} /> {gps.err &&
{gps.err}
} {loc && (
Lokasi: {loc.label}{loc.source === 'area' ? ' (titik tengah area)' : ''}
)}

Lokasi dibulatkan ±100 m di perangkatmu dan tidak disimpan saat mencari. Guru tampil hanya dengan area kira-kira ±1 km — tanpa alamat.

)} {step === 2 && (
Alat musik setQ({ ...q, instrument: k })} idPrefix="wz-inst" labelId="wz-inst-l" />
{ setQ({ ...q, age: v }); setStepErr(null); }} min={3} max={90} label="usia murid" placeholder="mis. 8" describedBy="wz-age-h" invalid={ageBad} /> {ageHint}
)} {step === 3 && (
Filter lanjutan{q.day ? ' · ' + dayName(cfg, q.day) : ''}
Hari les
{cfg.days.map((d) => )}
setSel(null)} pilotZone fitKey={fitKey} onLocate={gps.run} locating={gps.busy} label="Peta hasil: badge bernomor = area kira-kira guru (±1 km, bukan alamat), sama dengan nomor di daftar." describedBy="wz-map-legend" legend={[{ sw: 'pin', text: 'Lokasi kamu' }, { sw: 'zone', text: 'Area guru ±1 km (bukan alamat)' }, { sw: 'pilot', text: 'Area pilot' }]}> {selTeachers.length > 0 && (
{selTeachers.map(({ t, num }) => (
{num}
{t.name} ±{t.distance_km} km · {(t.instruments || []).map((x) => instName(cfg, x)).join(', ')}
))}
)}
{err && (
{err}
Tanya admin di WhatsApp
)} {busy &&
Mencari guru di sekitar kamu…
} {res && !busy && !err && (

Urut dari terdekat · jarak dibulatkan · nomor sama dengan di peta

{!n && (

{active.length ? 'Belum ada guru aktif yang cocok dengan semua pilihan ini.' : 'Belum ada guru aktif yang radiusnya sampai ke titik ini.'}

{active.length > 0 && (

Coba longgarkan:

{active.map((k) => )}
)}

Atau tetap kirim permintaan — tim IMS yang mencarikan guru.

Tanya admin di WhatsApp
)}
{teachers.map((t, i) => )}
)}
)} {stepErr && }
{step > 1 && } {step === 1 && } {step === 2 && } {step === 3 && }
setPicked(null)} rq={rq} setRq={setRq} onClosed={onClosed} onClose={(done) => { setSheet(false); if (done) { setSentNote('Permintaan terkirim. Tim IMS akan menghubungi lewat WhatsApp setelah guru yang cocok ditemukan.'); setRq(RQ_INIT); setPicked(null); } }} />
); } /* ───────────────────────────────────────────── LEMBAR PERMINTAAN — 4 langkah → POST /request (kontrak createHpRequest di hp-public.js, tidak berubah). ───────────────────────────────────────────── */ function RequestSheet({ open, onClose, cfg, loc, setLoc, q, picked, clearPicked, rq, setRq, onClosed }) { const [step, setStep] = useState(1); const [errors, setErrors] = useState({}); const [formErr, setFormErr] = useState(null); const [busy, setBusy] = useState(false); const [done, setDone] = useState(null); const [editInst, setEditInst] = useState(false); const head = useRef(null); const body = useRef(null); const set = (k, v) => { setRq((x) => ({ ...x, [k]: v })); if (errors[k]) setErrors((e) => { const c = { ...e }; delete c[k]; return c; }); }; const times = timesFor(cfg); const ps = cfg.package_sessions || 4; const hasPrice = cfg.prices && (Number(cfg.prices.paket) > 0 || Number(cfg.prices.eceran) > 0); const TITLES = ['Siapa yang mau les?', 'Kapan bisanya?', 'Ke mana guru datang?', 'Cek & kirim']; useEffect(() => { if (open) { setStep(1); setErrors({}); setFormErr(null); setDone(null); setEditInst(false); } }, [open]); useEffect(() => { if (!open) return; if (body.current) body.current.scrollTop = 0; const t = setTimeout(() => head.current && head.current.focus({ preventScroll: true }), 30); return () => clearTimeout(t); }, [open, step]); const validate = (s) => { const e = {}; if (s === 1) { if (!cfg.instruments.some((i) => i.key === rq.instrument)) e.instrument = 'Pilih alat musik.'; if (rq.student_name.trim().length < 2) e.student_name = 'Tulis nama murid (minimal 2 huruf).'; const age = Number(rq.student_age); if (rq.student_age === '' || !Number.isInteger(age) || age < 3 || age > 90) e.student_age = 'Usia murid 3–90 tahun.'; } else if (s === 2) { if (rq.days.length < 2) e.days = rq.days.length ? 'Pilih 1 hari lagi — minimal 2 hari supaya guru lebih mudah dicocokkan.' : 'Pilih minimal 2 hari yang bisa.'; if (!times.some((t) => t.k === rq.time)) e.time = 'Pilih waktu les.'; } else if (s === 3) { if (rq.parent_name.trim().length < 2) e.parent_name = 'Tulis nama orang tua/wali (minimal 2 huruf).'; if (!PHONE_RE.test(normPhone(rq.parent_phone))) e.parent_phone = rq.parent_phone.trim() ? 'Nomor WhatsApp belum benar. Contoh: 0812 3456 7890.' : 'Isi nomor WhatsApp untuk dihubungi admin.'; if (rq.address.trim().length < 10) e.address = 'Tulis alamat lengkap (jalan, nomor, cluster) — minimal 10 karakter.'; if (!loc) e.loc = 'Lokasi les belum dipilih. Kembali ke pencarian untuk memilih lokasi.'; } else if (s === 4) { if (!['paket', 'eceran'].includes(rq.kind)) e.kind = 'Pilih paket atau eceran.'; if (!rq.consent) e.consent = 'Centang persetujuan data pribadi untuk mengirim.'; } return e; }; const FOCUS = { instrument: 'rq-inst', student_name: 'rq-name', student_age: 'rq-age', days: 'rq-days', time: 'rq-time', parent_name: 'rq-pname', parent_phone: 'rq-phone', address: 'rq-addr', loc: 'rq-addr', kind: 'rq-kind', consent: 'rq-consent' }; const check = (s) => { const e = validate(s); setErrors(e); const keys = Object.keys(e); if (keys.length) { if (keys.includes('instrument')) setEditInst(true); setTimeout(() => { const el = document.getElementById(FOCUS[keys[0]]); if (el) el.focus(); }, 0); return false; } return true; }; const next = () => { if (check(step)) { setErrors({}); setStep(step + 1); } }; const back = () => { setErrors({}); setFormErr(null); if (step > 1) setStep(step - 1); else onClose(false); }; const slots = cfg.days.filter((d) => rq.days.includes(d.key)).map((d) => ({ day: d.key, start: (times.find((t) => t.k === rq.time) || {}).start })); const submit = async () => { setFormErr(null); for (const s of [1, 2, 3, 4]) { if (!check(s)) { setStep(s); return; } } setBusy(true); const payload = { instrument: rq.instrument, student_name: rq.student_name.trim(), student_age: Number(rq.student_age), level: rq.level, lat: loc.lat, lng: loc.lng, area_label: rq.area_label.trim(), address: rq.address.trim(), slots: slots.map((s) => ({ day: s.day, start: s.start })), kind: rq.kind, parent_name: rq.parent_name.trim(), parent_phone: rq.parent_phone.trim(), note: rq.note.trim(), preferred_teacher_id: picked ? picked.id : null, consent: true, consent_version: cfg.parent_consent.version, website: rq.website, // honeypot — manusia tidak pernah mengisi }; const r = await hpApi('/request', { method: 'POST', body: payload, timeout: 20000 }); setBusy(false); if (r.ok && r.body && r.body.ok) { setDone(r.body.message || 'Permintaan terkirim.'); return; } if (r.status === 404) { onClose(false); onClosed(); return; } setFormErr(friendlyError(r)); }; const waText = lesWaText({ cfg, q, loc, rq, picked }); const Err = ({ k, id }) => (errors[k] ?
{errors[k]}
: null); const inv = (k, id, hint) => ({ id, 'aria-invalid': errors[k] ? true : undefined, 'aria-describedby': errors[k] ? id + '-err' : hint }); const age = Number(rq.student_age); const timeObj = times.find((t) => t.k === rq.time); const summary = [ ['Murid', rq.student_name.trim() + (rq.student_age ? ', ' + rq.student_age + ' th' : '')], ['Alat musik', instName(cfg, rq.instrument) + ' · ' + (LEVEL_SHORT[rq.level] || rq.level)], ['Jadwal', cfg.days.filter((d) => rq.days.includes(d.key)).map((d) => d.label).join(', ') + (timeObj ? ' · ' + timeObj.label + ' ' + timeObj.start : '')], ['Lokasi', areaText(loc) + (rq.area_label.trim() && rq.area_label.trim() !== (loc && loc.label) ? ' · ' + rq.area_label.trim() : '')], ['Guru pilihan', picked ? picked.name : 'Dicarikan tim IMS'], ['WhatsApp', rq.parent_phone.trim()], ]; return ( onClose(!!done)} labelledBy="rq-title">
Permintaan les di rumah {DEMO && pratinjau}
{done ? (
) : (

{TITLES[step - 1]}

{step === 1 && (
{picked && (
Guru pilihan: {picked.name} — kami prioritaskan, tetap tergantung kesediaan guru.
)} {rq.instrument && !editInst ? (

Alat musik: {instName(cfg, rq.instrument)}

) : (
Alat musik { set('instrument', k); }} allowAll={false} idPrefix="rq-inst" labelId="rq-inst-l" invalid={!!errors.instrument} errId="rq-inst-err" />
)}
set('student_name', e.target.value)} autoComplete="off" enterKeyHint="next" />
set('student_age', v)} min={3} max={90} label="usia murid" invalid={!!errors.student_age} describedBy={errors.student_age ? 'rq-age-err' : undefined} /> {age > 0 && age < 17 && Murid di bawah 17 tahun: orang dewasa wajib ada di rumah selama sesi.}
Level
{(cfg.levels || ['pemula']).map((l) => )}
)} {step === 2 && (
Hari yang bisa (pilih minimal 2)
{cfg.days.map((d) => ( ))}
Jam berapa?
{times.map((t) => ( ))}

Sesi {cfg.session_minutes} menit. Jadwal pasti dikonfirmasi admin lewat WhatsApp — ini hanya pilihan awal.

)} {step === 3 && (

Lokasi les: {areaText(loc) || 'belum dipilih'}

{errors.loc &&
{errors.loc}
}
set('parent_name', e.target.value)} autoComplete="name" enterKeyHint="next" />
set('parent_phone', e.target.value)} placeholder="0812 3456 7890" autoComplete="tel" enterKeyHint="next" /> {errors.parent_phone ? : Admin IMS menghubungi lewat nomor ini. Tidak diberikan ke guru.}