/* global React, ReactDOM, Icon, useTweaks, TweaksPanel, TweakSection, TweakColor, TweakToggle, NAV_URLS */ const { useState, useEffect } = React; const TW_DEFAULTS = /*EDITMODE-BEGIN*/{ "brand": ["#0F623F", "#07432A"], "dark": false }/*EDITMODE-END*/; /* ---------------- backend API helpers ---------------- */ function csrfToken() { const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.content : ""; } async function apiPost(url, body) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", "X-CSRF-TOKEN": csrfToken(), }, body: JSON.stringify(body), }); let data = {}; try { data = await res.json(); } catch (e) { /* no body */ } return { ok: res.ok, status: res.status, data }; } /* chuẩn hoá + validate SĐT VN */ function normalizePhone(raw) { return raw.replace(/[\s.\-]/g, ""); } function validatePhone(raw) { const p = normalizePhone(raw); if (!p) return { ok: false, msg: "Vui lòng nhập số điện thoại" }; if (!/^0\d{9}$/.test(p)) return { ok: false, msg: "SĐT phải gồm 10 số, bắt đầu bằng 0" }; if (!/^(03|05|07|08|09)\d{8}$/.test(p)) return { ok: false, msg: "Đầu số không hợp lệ (03/05/07/08/09)" }; return { ok: true }; } function validateEmail(raw) { if (!raw.trim()) return { ok: false, msg: "Vui lòng nhập email" }; return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw.trim()) ? { ok: true } : { ok: false, msg: "Email không hợp lệ" }; } /* Ngày sinh: nhập tay dd/mm/yyyy, tự chèn dấu "/" */ function formatDobInput(raw) { const d = raw.replace(/\D/g, "").slice(0, 8); if (d.length <= 2) return d; if (d.length <= 4) return d.slice(0, 2) + "/" + d.slice(2); return d.slice(0, 2) + "/" + d.slice(2, 4) + "/" + d.slice(4); } function validateDob(raw) { if (!raw.trim()) return { ok: true }; // optional const m = raw.match(/^(\d{2})\/(\d{2})\/(\d{4})$/); if (!m) return { ok: false, msg: "Nhập đủ ngày sinh theo dạng dd/mm/yyyy" }; const d = +m[1], mo = +m[2], y = +m[3]; const dt = new Date(y, mo - 1, d); const valid = dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d; if (!valid || y < 1900 || dt >= new Date()) return { ok: false, msg: "Ngày sinh không hợp lệ" }; return { ok: true }; } function dobToISO(raw) { const m = raw.match(/^(\d{2})\/(\d{2})\/(\d{4})$/); return m ? `${m[3]}-${m[2]}-${m[1]}` : ""; } /* Bảng chọn ngày sinh: 3 ô chọn Ngày / Tháng / Năm — chọn thẳng năm, không phải lùi lịch */ function DobPicker({ value, onPick, onClose }) { const thisYear = new Date().getFullYear(); const parsed = (() => { const m = (value || "").match(/^(\d{2})\/(\d{2})\/(\d{4})$/); return m ? { d: +m[1], mo: +m[2], y: +m[3] } : { d: 1, mo: 1, y: 2000 }; })(); const [d, setD] = useState(parsed.d); const [mo, setMo] = useState(parsed.mo); const [y, setY] = useState(parsed.y); const daysInMonth = new Date(y, mo, 0).getDate(); const day = Math.min(d, daysInMonth); const years = []; for (let yy = thisYear; yy >= 1925; yy--) years.push(yy); const selStyle = { flex: 1, padding: "11px 8px", borderRadius: 10, border: "1.5px solid var(--line, #ddd)", background: "var(--card, #fff)", color: "inherit", fontSize: 16, fontFamily: "inherit", outline: "none" }; const confirm = () => { onPick(`${String(day).padStart(2, "0")}/${String(mo).padStart(2, "0")}/${y}`); onClose(); }; return ( <>
e.stopPropagation()}>
); } function validatePassword(raw) { if (!raw) return { ok: false, msg: "Vui lòng nhập mật khẩu" }; if (raw.length < 6) return { ok: false, msg: "Mật khẩu phải có ít nhất 6 ký tự" }; return { ok: true }; } function validatePasswordConfirm(pw, confirm) { if (!confirm) return { ok: false, msg: "Vui lòng nhập lại mật khẩu" }; if (pw !== confirm) return { ok: false, msg: "Mật khẩu xác nhận không khớp" }; return { ok: true }; } /* ---------------- Step 1: form ---------------- */ function FormStep({ data, setData, onNext }) { const [touched, setTouched] = useState({}); const [serverError, setServerError] = useState(""); const [loading, setLoading] = useState(false); const [showPw, setShowPw] = useState(false); const [showPw2, setShowPw2] = useState(false); const [dobPicker, setDobPicker] = useState(false); const phoneRes = validatePhone(data.phone); const emailRes = validateEmail(data.email); const dobRes = validateDob(data.dob); const pwRes = validatePassword(data.password); const pw2Res = validatePasswordConfirm(data.password, data.password_confirmation); const nameOk = data.name.trim().length >= 2; const canSubmit = phoneRes.ok && emailRes.ok && dobRes.ok && nameOk && pwRes.ok && pw2Res.ok; const submit = async () => { setTouched({ phone: true, name: true, email: true, dob: true, password: true, password_confirmation: true }); setServerError(""); if (!canSubmit) return; setLoading(true); const { ok, data: res } = await apiPost("/register", { phone: normalizePhone(data.phone), name: data.name, email: data.email, dob: dobToISO(data.dob), password: data.password, password_confirmation: data.password_confirmation, }); setLoading(false); if (!ok) { setServerError(res.message || "Có lỗi xảy ra, vui lòng thử lại."); return; } onNext(res.redirect); }; return ( <>

Tạo tài khoản

Đăng ký thành viên Laboong để tích điểm và đổi quà.

{ setData({ ...data, phone: e.target.value.replace(/[^\d\s.\-]/g, "") }); setServerError(""); }} onBlur={() => setTouched(t => ({ ...t, phone: true }))} /> {phoneRes.ok && data.phone && !serverError && }
{serverError ?
{serverError}
: touched.phone && !phoneRes.ok ?
{phoneRes.msg}
:
Dùng để đăng nhập vào tài khoản của bạn.
}
setData({ ...data, name: e.target.value })} onBlur={() => setTouched(t => ({ ...t, name: true }))} />
{touched.name && !nameOk &&
Vui lòng nhập họ tên
}
setData({ ...data, password: e.target.value })} onBlur={() => setTouched(t => ({ ...t, password: true }))} />
{touched.password && !pwRes.ok &&
{pwRes.msg}
}
setData({ ...data, password_confirmation: e.target.value })} onBlur={() => setTouched(t => ({ ...t, password_confirmation: true }))} />
{touched.password_confirmation && !pw2Res.ok &&
{pw2Res.msg}
}
setData({ ...data, email: e.target.value })} onBlur={() => setTouched(t => ({ ...t, email: true }))} /> {emailRes.ok && data.email && }
{touched.email && !emailRes.ok &&
{emailRes.msg}
}
setDobPicker(true)} onChange={e => setData({ ...data, dob: formatDobInput(e.target.value) })} onBlur={() => setTouched(t => ({ ...t, dob: true }))} /> {dobRes.ok && data.dob.length === 10 && }
{dobPicker && ( { setData({ ...data, dob: v }); setTouched(t => ({ ...t, dob: true })); }} onClose={() => setDobPicker(false)} /> )} {touched.dob && !dobRes.ok ?
{dobRes.msg}
:
Bấm để chọn ngày hoặc gõ trực tiếp · Nhận quà sinh nhật từ Laboong 🎂
}
Bằng việc đăng ký, bạn đồng ý với Điều khoản dịch vụChính sách bảo mật của Laboong.
); } /* ---------------- Step 2: success ---------------- */ function SuccessStep({ data, redirect }) { useEffect(() => { const id = setTimeout(() => { location.href = redirect || NAV_URLS.home; }, 2200); return () => clearTimeout(id); }, [redirect]); return (

Chào mừng, {data.name.trim().split(/\s+/).slice(-1)[0]}! 🎉

Tài khoản của bạn đã được tạo thành công.

Quà chào mừng thành viên mới
+50 điểm
Đang tự động đăng nhập…
); } /* ---------------- Push notification demo ---------------- */ function PushDemo({ onClose }) { return (
L Laboong bây giờ
Voucher sắp hết hạn ⏰
Voucher “Giảm 30.000đ” của bạn sẽ hết hạn sau 3 ngày. Mở app để sử dụng ngay nhé!
); } /* ---------------- App ---------------- */ function App() { const [tw, setTweak] = useTweaks(TW_DEFAULTS); const [step, setStep] = useState(0); const [data, setData] = useState({ phone: "", name: "", email: "", dob: "", password: "", password_confirmation: "" }); const [redirect, setRedirect] = useState(null); const [push, setPush] = useState(false); useEffect(() => { const r = document.documentElement; const [b, d] = Array.isArray(tw.brand) ? tw.brand : [tw.brand, tw.brand]; r.style.setProperty("--brand", b); r.style.setProperty("--brand-deep", d); r.setAttribute("data-theme", tw.dark ? "dark" : "light"); }, [tw.brand, tw.dark]); const STEPS = ["Thông tin", "Hoàn tất"]; return (
L
Laboong
Victoria Văn Phú · Thẻ thành viên
{STEPS.map((s, i) => (
{i < step ? : i + 1} {s}
{i < STEPS.length - 1 && }
))}
{step === 0 && { setRedirect(redirectUrl); setStep(1); }} />} {step === 1 && }
{step === 0 &&
Đã có tài khoản? Đăng nhập
}
{push && setPush(false)} />} setTweak("brand", v)} /> setTweak("dark", v)} />
); } ReactDOM.createRoot(document.getElementById("root")).render();