diff --git a/W1/app.js b/W1/app.js new file mode 100644 index 0000000..02e41e0 --- /dev/null +++ b/W1/app.js @@ -0,0 +1,106 @@ +// Phone format +const PHONE_RE = /^\+?\d[\d\s()-]{7,}$/; + +const form = document.getElementById('signup-form'); +const fields = { + name: { + el: document.getElementById('name'), + err: document.getElementById('err-name'), + validate: (v) => v.trim() ? null : 'Name is required.', + }, + phone: { + el: document.getElementById('phone'), + err: document.getElementById('err-phone'), + validate: (v) => { + const t = v.trim(); + if (!t) return 'Phone number is required.'; + if (!PHONE_RE.test(t)) return 'Enter a valid phone number (e.g. +61 123 456 789).'; + return null; + }, + }, + dulux: { + el: document.getElementById('dulux'), + err: document.getElementById('err-dulux'), + validate: () => null, + }, +}; + +function debounce(fn, wait = 250){ + let t; + return function debounced(...args){ + clearTimeout(t); + t = setTimeout(() => fn.apply(this, args), wait); + }; +} + +/*Error*/ +function setError(el, errEl, message){ + const bad = Boolean(message); + el.classList.toggle('is-error', bad); + el.setAttribute('aria-invalid', bad ? 'true' : 'false'); + + if (bad) { + el.setAttribute('aria-describedby', errEl.id); + errEl.textContent = message; + errEl.classList.add('is-visible'); + } else { + el.removeAttribute('aria-describedby'); + errEl.classList.remove('is-visible'); + errEl.textContent = ''; + } +} + +function validateField(key){ + const { el, err, validate } = fields[key]; + const msg = validate(el.value); + setError(el, err, msg); + return !msg; +} + +function validateAll(){ + let firstInvalid = null; + const ok = Object.keys(fields).every((k) => { + const valid = validateField(k); + if (!valid && !firstInvalid) firstInvalid = fields[k].el; + return valid; + }); + return { ok, firstInvalid }; +} + +/*Toast*/ +function showToast(message, type = 'info', duration = 4200){ + const root = document.getElementById('toast-root'); + if (!root) return; + const item = document.createElement('div'); + item.className = `toast toast--${type}`; + item.role = 'status'; + item.textContent = message; + root.appendChild(item); + + const remove = () => item.parentNode && root.removeChild(item); + const timer = setTimeout(remove, duration + 220); + item.addEventListener('click', () => { clearTimeout(timer); remove(); }); +} + + +Object.values(fields).forEach(({ el }) => { + el.addEventListener('blur', () => validateField(el.id)); + el.addEventListener( + 'input', + debounce(() => { + if (el.classList.contains('is-error')) validateField(el.id); + }, 250) + ); +}); + +form.addEventListener('submit', (e) => { + e.preventDefault(); + const { ok, firstInvalid } = validateAll(); + if (!ok) { + firstInvalid?.focus(); + showToast('Please fix the highlighted fields.', 'error'); + return; + } + // Mockup submit successful + showToast('Signed up successfully!', 'success'); +}); \ No newline at end of file diff --git a/W1/bg.png b/W1/bg.png new file mode 100644 index 0000000..4212c30 Binary files /dev/null and b/W1/bg.png differ diff --git a/W1/index.html b/W1/index.html new file mode 100644 index 0000000..602ef2e --- /dev/null +++ b/W1/index.html @@ -0,0 +1,60 @@ + + + + + + Sign Up • Paint Quote System + + + +
+
+

Welcome to Paint Quote System

+

+ Consequat adipisicing ea do labore irure adipisicing occaecat cupidatat + excepteur duis mo +

+
+ +
+
+

Sign Up

+

Enter details to create your account

+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +

+ Already have an account? + Sign in +

+
+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/W1/styles.css b/W1/styles.css new file mode 100644 index 0000000..32c7c89 --- /dev/null +++ b/W1/styles.css @@ -0,0 +1,207 @@ +:root{ + --bg: #f5f3ef; + --ink: #0f172a; + --muted: #64748b; + --brand: #0d6ea8; + --brand-weak: #e0f2ff; + --panel: #ffffff; + --border: #e5e7eb; + --danger: #dc2626; + --danger-weak: #fee2e2; + --radius: 16px; + --shadow: 0 14px 40px rgba(2, 8, 23, .15); +} + +*, +*::before, +*::after{ box-sizing: border-box; } + +html, body{ + height: 100%; + margin: 0; + color: var(--ink); + background: var(--bg); + font: 16px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol"; +} + +.bg-photo{ + background-image: url("bg.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +.shell{ + min-height: 100dvh; + display: grid; + grid-template-columns: 1.15fr 0.8fr; + align-items: center; + gap: 5px; + padding: 48px clamp(30px, 6vw, 64px); + backdrop-filter: saturate(1.05); +} + +.hero{ + max-width: 620px; + justify-self: start; +} +.hero-title{ + font-weight: 600; + font-size: clamp(28px, 2.2vw, 42px); + letter-spacing: .2px; + position: relative; + margin: 0; + display: inline-block; + color: #173b47; +} +.hero-title::after { + content: ""; + display: block; + height: 1.8px; + margin: 15px 0 15px; + width: 100%; + background: currentColor; + opacity: .35; + border-radius: 2px; +} +.hero-desc{ + margin: 0; + color: #173b47; + max-width: 46ch; +} + +.card{ + justify-self: end; + width: min(560px, 100%); + background: var(--panel); + border-radius: var(--radius); + box-shadow: var(--shadow); + border: 1px solid rgba(15,23,42,.05); + margin-right: 6vw; +} +.card-inner{ + padding: 36px clamp(24px, 4vw, 44px); +} +.card-title{ + margin: 0 0 6px; + text-align: center; + font-size: clamp(22px, 2.2vw, 30px); + font-weight: 600; +} +.card-subtitle{ + margin: 0 0 24px; + text-align: center; + color: var(--muted); +} + +.form{ display: grid; gap: 16px; } +.field{ display: grid; gap: 8px; } +.field label{ font-size: 14px; color: #374151; } + +.field input{ + width: 100%; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid var(--border); + outline: none; + background: #fff; + font-size: 15px; + transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease; +} +.field input::placeholder{ color: #9aa4b2; } +.field input:focus-visible{ + border-color: var(--brand); + box-shadow: 0 0 0 3px var(--brand-weak); +} + +.error{ + display: none; + margin-top: 6px; + font-size: 13px; + color: var(--danger); +} +.error.is-visible{ display: block; } + +.field input.is-error{ + border-color: var(--danger); + box-shadow: 0 0 0 3px var(--danger-weak); +} + +.btn{ + appearance: none; + border: 0; + border-radius: 10px; + padding: 14px 18px; + font-weight: 600; + font-size: 16px; + cursor: pointer; + margin-top: 2vh; +} +.btn-primary{ + background: var(--brand); + color: #fff; + box-shadow: 0 10px 22px rgba(13,110,168,.25); + transition: filter .15s ease, transform .02s ease, box-shadow .15s ease; +} +.btn-primary:hover{ filter: brightness(1.03); } +.btn-primary:active{ transform: translateY(1px); } + +.footnote{ + margin: 12px 0 0; + text-align: center; + color: var(--muted); + font-size: 14px; +} +.link{ color: var(--brand); text-decoration: none; } +.link:hover{ text-decoration: underline; } + +/* 响应式 */ +@media (max-width: 900px){ + .shell{ + grid-template-columns: 1fr; + gap: 24px; + padding: 28px 18px 40px; + backdrop-filter: none; + } + .hero{ order: 1; max-width: 680px; justify-self: center; } + .card{ order: 2; justify-self: center; } +} + +/* Toast */ +#toast-root{ + position: fixed; + right: 20px; + bottom: 20px; + display: grid; + gap: 10px; + z-index: 9999; +} +.toast{ + min-width: 260px; + max-width: 360px; + padding: 12px 14px; + border-radius: 10px; + color: #0f172a; + background: #ffffff; + border: 1px solid rgba(15,23,42,.1); + box-shadow: 0 10px 24px rgba(2,8,23,.16); + font: 14px/1.4 ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial; + opacity: 0; + transform: translateY(8px); + animation: toast-in .18s ease-out forwards, toast-out .2s ease-in forwards 4.2s; +} +.toast--success{ border-color: #05966933; box-shadow: 0 10px 24px rgba(5,150,105,.18); } +.toast--error{ border-color: #dc262633; box-shadow: 0 10px 24px rgba(220,38,38,.18); } + +@keyframes toast-in{ + to{ opacity:1; transform: translateY(0); } +} +@keyframes toast-out{ + to{ opacity:0; transform: translateY(8px); } +} + +@media (prefers-reduced-motion: reduce){ + .toast{ animation: none; opacity:1; transform:none; } +} \ No newline at end of file