Merge pull request 'update(model): update validate model, update acctio click close on ousite model' (#3) from zelda.fix-model into main

Reviewed-on: #3
This commit is contained in:
zelda 2026-05-20 13:13:45 +10:00
commit fd2b80685f
3 changed files with 169 additions and 18 deletions

View File

@ -2364,7 +2364,7 @@ body::before {
/* ============ CENTRAL INSTITUTIONAL LAYOUT ============ */
.official-seal-large {
width: 260px; /* To hơn một chút theo yêu cầu */
width: 260px; /* Slightly larger as requested */
height: auto;
display: block;
margin: 0 auto 30px auto;
@ -2664,7 +2664,7 @@ body::before {
}
.modal-box {
background: linear-gradient(145deg, #2A3B54, #1E293B); /* Sáng hơn một chút với dải màu nhẹ */
background: linear-gradient(145deg, #2A3B54, #1E293B); /* Slightly brighter with a subtle gradient */
border: 1px solid rgba(255,255,255,0.15);
border-radius: 20px;
padding: 40px;
@ -2710,7 +2710,7 @@ body::before {
.modal-input {
width: 100%;
background: rgba(255, 255, 255, 0.06); /* Sáng hơn, trong suốt trên nền xanh đậm */
background: rgba(255, 255, 255, 0.06); /* Slightly brighter, transparent on dark blue background */
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 14px 16px;
@ -2729,7 +2729,7 @@ body::before {
}
.modal-input::placeholder {
color: #64748B; /* Placeholder sáng hơn xíu để dễ đọc */
color: #64748B; /* Slightly lighter placeholder for readability */
}
@ -2743,7 +2743,7 @@ body::before {
/* Tablet (1024px and below) */
@media (max-width: 1024px) {
html {
scroll-snap-type: none; /* Tắt snap để cuộn mượt hơn trên máy tính bảng */
scroll-snap-type: none; /* Disable snap for smoother scrolling on tablets */
}
.section {
@ -2773,7 +2773,7 @@ body::before {
.section {
height: auto;
min-height: auto; /* Cho phép co giãn hoàn toàn theo nội dung */
min-height: auto; /* Allow full flex according to content */
padding: 80px 0 40px 0;
}

View File

@ -149,8 +149,8 @@ document.addEventListener("DOMContentLoaded", () => {
runTypewriter();
// Đã hủy bỏ bộ hẹn giờ auto-focus ở đây.
// Việc auto-focus sau 19s khiến trình duyệt tự động cuộn giật ngược lên đầu trang nếu user đang đọc ở dưới.
// Auto-focus timer removed here.
// Auto-focusing after 19s caused the browser to scroll back to the top while the user was reading below.
// Switch between Source (Buy) and Sell Modes
window.switchTab = function (mode) {

View File

@ -2,8 +2,113 @@
// ── API Config ──
const API_ENDPOINT = window.PROLOGY_CONFIG.API_URL;
// ── Per-field error messages ──
const FIELD_ERROR_MSG = {
name: "Please enter a valid name.",
email: "Please enter a valid email address.",
phone: "Please enter a valid Australian mobile number (04xx xxx xxx).",
message: "Please enter at least 10 characters.",
email_or_phone_required: "Please provide an email or phone number.",
};
// ── Client-side validators (mirror server rules) ──
const RX_EMAIL = /^[\w.+-]+@[\w-]+(\.[\w-]+)+$/;
const RX_NAME = /^[\p{L}\p{M}'\-\s.]{2,80}$/u;
function isValidAuMobile(phone) {
const raw = (phone || "").trim();
if (!raw || !/^[\d\s\-()]+$/.test(raw)) return false;
const digits = raw.replace(/\D/g, "");
return /^04\d{8}$/.test(digits);
}
function validateClient(fields) {
const bad = [];
if ("name" in fields && (!fields.name || !RX_NAME.test(fields.name.trim())))
bad.push("name");
if ("email" in fields && (!fields.email || !RX_EMAIL.test(fields.email.trim())))
bad.push("email");
if ("phone" in fields && !isValidAuMobile(fields.phone))
bad.push("phone");
if ("message" in fields && (!fields.message || fields.message.trim().length < 10))
bad.push("message");
return bad;
}
function clearFieldErrors(sr) {
if (!sr) return;
sr.querySelectorAll(".field-error").forEach((el) => el.remove());
sr.querySelectorAll(".invalid").forEach((el) => el.classList.remove("invalid"));
}
function showFieldErrors(sr, badKeys, fieldMap) {
if (!sr || !fieldMap) return false;
let shown = 0;
badKeys.forEach((key) => {
let names = fieldMap[key];
if (!names) return;
if (!Array.isArray(names)) names = [names];
names.forEach((name) => {
const input = sr.querySelector('[name="' + name + '"]');
if (!input) return;
input.classList.add("invalid");
const field = input.closest(".field");
if (field && !field.querySelector(".field-error")) {
const err = document.createElement("div");
err.className = "field-error";
err.textContent = FIELD_ERROR_MSG[key] || "Invalid value.";
field.appendChild(err);
shown++;
}
});
});
return shown > 0;
}
function bindClearOnInput(sr) {
if (!sr || sr._clearBound) return;
sr._clearBound = true;
sr.querySelectorAll("input, textarea, select").forEach((el) => {
el.addEventListener("input", () => {
if (!el.classList.contains("invalid")) return;
el.classList.remove("invalid");
const field = el.closest(".field");
const err = field && field.querySelector(".field-error");
if (err) err.remove();
});
});
}
function showButtonError(btnEl, msg) {
const origHTML = btnEl.dataset.origHtml || btnEl.innerHTML;
btnEl.dataset.origHtml = origHTML;
btnEl.innerHTML = "<span>&#x26A0; " + msg + "</span>";
setTimeout(() => {
btnEl.innerHTML = origHTML;
btnEl.disabled = false;
delete btnEl.dataset.origHtml;
}, 3000);
}
// ── Shared submit function ──
async function submitToAPI(payload, btnEl, onSuccess) {
async function submitToAPI(payload, btnEl, onSuccess, opts) {
const { sr, validateFields, fieldMap } = opts || {};
if (sr) {
clearFieldErrors(sr);
bindClearOnInput(sr);
}
// Client-side validation — avoid burning an API call on obvious errors
if (validateFields) {
const bad = validateClient(validateFields);
if (bad.length) {
const shown = showFieldErrors(sr, bad, fieldMap);
if (!shown) showButtonError(btnEl, "Please check your inputs and try again.");
return;
}
}
btnEl.disabled = true;
const origHTML = btnEl.innerHTML;
btnEl.innerHTML = "<span>Sending...</span>";
@ -33,8 +138,17 @@
if (res.ok && data.ok) {
onSuccess(restore);
} else {
const msg =
data.error || "Something went wrong. Please try again.";
let msg = data.error || "Something went wrong. Please try again.";
if (res.status === 429) {
msg = "Too many requests. Please try again later.";
} else if (/validation failed/i.test(msg)) {
const shown = showFieldErrors(sr, data.fields || [], fieldMap);
if (shown) {
restore();
return;
}
msg = "Please check your inputs and try again.";
}
btnEl.innerHTML = "<span>&#x26A0; " + msg + "</span>";
setTimeout(restore, 3000);
}
@ -125,6 +239,16 @@
border-color: #1A73E8;
background: rgba(255,255,255,0.09);
}
input.invalid, textarea.invalid, select.invalid {
border-color: #f87171;
background: rgba(248,113,113,0.06);
}
.field-error {
color: #f87171;
font-size: 0.78rem;
margin-top: 6px;
line-height: 1.35;
}
input::placeholder, textarea::placeholder { color: #64748B; }
textarea { min-height: 120px; resize: vertical; }
select {
@ -190,11 +314,6 @@
this.shadowRoot
.querySelector(".close")
.addEventListener("click", () => this.close());
this.shadowRoot
.querySelector(".overlay")
.addEventListener("click", (e) => {
if (e.target.classList.contains("overlay")) this.close();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && this.classList.contains("open"))
this.close();
@ -261,6 +380,11 @@
restore();
}, 1800);
},
{
sr,
validateFields: { email, message: inventory },
fieldMap: { email: "email", message: "inventory" },
},
);
});
}
@ -325,6 +449,11 @@
restore();
}, 1800);
},
{
sr,
validateFields: { email },
fieldMap: { email: "email", message: "parts" },
},
);
});
}
@ -348,7 +477,7 @@
</div>
<div class="field-row">
<div class="field"><label>Company (Optional)</label><input type="text" name="company" placeholder="Company Ltd."></div>
<div class="field"><label>Phone Number <span class="req">*</span></label><input type="tel" name="phone" placeholder="+1 234 567 8900" required></div>
<div class="field"><label>Phone Number <span class="req">*</span></label><input type="tel" name="phone" placeholder="04xx xxx xxx" inputmode="tel" autocomplete="tel" required></div>
</div>
<div class="field">
<label>Equipment Requirements <span class="req">*</span></label>
@ -395,6 +524,17 @@
restore();
}, 1800);
},
{
sr,
validateFields: { name, email, phone, message: requirements },
fieldMap: {
name: "name",
email: "email",
phone: "phone",
message: "requirements",
email_or_phone_required: ["email", "phone"],
},
},
);
});
}
@ -418,7 +558,7 @@
</div>
<div class="field-row">
<div class="field"><label>Organization / Agency <span class="req">*</span></label><input type="text" name="org" placeholder="Organization Name" required></div>
<div class="field"><label>Contact Number <span class="req">*</span></label><input type="tel" name="phone" placeholder="+1 234 567 8900" required></div>
<div class="field"><label>Contact Number <span class="req">*</span></label><input type="tel" name="phone" placeholder="04xx xxx xxx" inputmode="tel" autocomplete="tel" required></div>
</div>
<div class="field">
<label>Enquiry Details <span class="req">*</span></label>
@ -456,6 +596,17 @@
restore();
}, 1800);
},
{
sr,
validateFields: { name, email, phone, message: details },
fieldMap: {
name: "name",
email: "email",
phone: "phone",
message: "details",
email_or_phone_required: ["email", "phone"],
},
},
);
});
}