/* global React, ReactDOM, Icon, useTweaks, TweaksPanel, TweakSection, TweakColor, TweakToggle, NAV_URLS */ const { useState, useEffect, useRef } = 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 }; } function normPhone(raw) { return raw.replace(/[\s.\-]/g, ""); } function validPhone(raw) { const p = normPhone(raw); if (!p) return { ok: false, msg: "Vui lòng nhập số điện thoại" }; if (!/^(03|05|07|08|09)\d{8}$/.test(p)) return { ok: false, msg: "Số điện thoại không đúng định dạng" }; return { ok: true }; } function prettyPhone(raw) { return normPhone(raw).replace(/^0(\d{3})(\d{3})(\d{3})$/, "0$1 $2 $3"); } function fmtTime(s) { return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`; } /* ---------------- Forgot Password Flow ---------------- */ function ForgotFlow({ onBack }) { const [step, setStep] = useState("phone"); // phone | done const [phone, setPhone] = useState(""); const [maskedEmail, setMaskedEmail] = useState(""); const [touched, setTouched] = useState(false); const [err, setErr] = useState(""); const [sending, setSending] = useState(false); const pRes = validPhone(phone); const sendNewPassword = async () => { setTouched(true); setErr(""); if (!pRes.ok || sending) return; setSending(true); const { ok, data } = await apiPost("/forgot-password/send-new-password", { phone: normPhone(phone) }); setSending(false); if (!ok) { setErr(data.message || "Có lỗi xảy ra"); return; } setMaskedEmail(data.email || ""); setStep("done"); }; if (step === "done") return (

Đã gửi mật khẩu mới! 📧

Mật khẩu mới đã được gửi tới email {maskedEmail ? <> {maskedEmail} : " của bạn"}.
Kiểm tra hộp thư (cả mục Spam) rồi dùng mật khẩu đó để đăng nhập.
Sau khi vào được, hãy đổi mật khẩu riêng trong mục Tài khoản nhé.

); // step === "phone" return ( <>

Quên mật khẩu

Nhập số điện thoại đã đăng ký — mật khẩu mới sẽ được gửi về email của bạn.

{ setPhone(e.target.value.replace(/[^\d\s.\-]/g, "")); setErr(""); }} onKeyDown={e => { if (e.key === "Enter") sendNewPassword(); }} />
{touched && !pRes.ok &&
{pRes.msg}
} {err &&
{err}
}
); } /* ---------------- Success ---------------- */ function SuccessStep({ redirect }) { useEffect(() => { const id = setTimeout(() => { location.href = redirect || NAV_URLS.home; }, 1900); return () => clearTimeout(id); }, [redirect]); return (

Đăng nhập thành công! 👋

Chào mừng bạn quay lại Laboong. Đang chuyển đến trang chủ…

Đang tải tài khoản của bạn…
); } /* ---------------- App ---------------- */ function App() { const [tw, setTweak] = useTweaks(TW_DEFAULTS); const [phase, setPhase] = useState("login"); // login | forgot | success const [phone, setPhone] = useState(""); const [pw, setPw] = useState(""); const [showPw, setShowPw] = useState(false); const [remember, setRemember] = useState(true); const [touched, setTouched] = useState(false); const [pwErr, setPwErr] = useState(""); const [notReg, setNotReg] = useState(false); const [redirect, setRedirect] = useState(null); const [sending, setSending] = 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 pRes = validPhone(phone); const loginPw = async () => { setTouched(true); setNotReg(false); setPwErr(""); if (!pRes.ok || sending) return; setSending(true); const { ok, data } = await apiPost("/login/password", { phone: normPhone(phone), password: pw, remember }); setSending(false); if (!ok) { if (data.not_registered) { setNotReg(true); return; } setPwErr(data.message || "Mật khẩu không đúng."); return; } setRedirect(data.redirect || null); setPhase("success"); }; return (
L
Laboong
Victoria Văn Phú · Thẻ thành viên
{phase === "login" && ( <>

Đăng nhập

Chào mừng bạn quay lại! Đăng nhập để tích điểm và đổi quà.

{ setPhone(e.target.value.replace(/[^\d\s.\-]/g, "")); setNotReg(false); }} onKeyDown={e => { if (e.key === "Enter") loginPw(); }} />
{touched && !pRes.ok &&
{pRes.msg}
}
{ setPw(e.target.value); setPwErr(""); }} onKeyDown={e => { if (e.key === "Enter") loginPw(); }} />
{pwErr &&
{pwErr}
}
{notReg && (
Số {prettyPhone(phone)} chưa có tài khoản. Đăng ký ngay →
)} )} {phase === "forgot" && setPhase("login")} />} {phase === "success" && }
{(phase === "login") &&
Chưa có tài khoản? Đăng ký
}
setTweak("brand", v)} /> setTweak("dark", v)} />
); } ReactDOM.createRoot(document.getElementById("root")).render();