update(model): update validate model, update acctio click close on ousite model
This commit is contained in:
parent
81b437d6b8
commit
6e60415341
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>⚠ " + 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>⚠ " + 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"],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue