Pinned 2026 toolchains (Go 1.26, Rust 1.98/edition 2024, Python 3.14 + uv, Node 24, Zig 0.16, NixOS 26.05), postgres 18 / mongo 8, lockfiles built from, non-root runtimes, .dockerignore, per-project LICENSE, READMEs with the git.devai.io clone line, checkout@v7 CI. Security fixes in the legacy Rust APIs (any-password login, self-assigned admin, hard-coded JWT secret), JWT alg/exp/sub enforcement across the blog series, safe markdown links in the frontends, and many smaller bugs — every project was built, run and exercised end to end. Adds scripts/publish.sh + a CI publish job that splits every folder into its own repo at git.devai.io/templates/<folder>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01128fhuZbgivaSJvtMf4s1G
58 lines
2.3 KiB
JavaScript
58 lines
2.3 KiB
JavaScript
const form = document.getElementById("form");
|
|
const successEl = document.getElementById("success");
|
|
|
|
// Every field is described in one place: its input, the <p> for its error, and
|
|
// a validate rule that returns an error message — or "" when the value is fine.
|
|
const fields = {
|
|
name: {
|
|
input: document.getElementById("name"),
|
|
error: document.getElementById("nameError"),
|
|
validate: (value) => (value.trim() ? "" : "Please enter your name."),
|
|
},
|
|
email: {
|
|
input: document.getElementById("email"),
|
|
error: document.getElementById("emailError"),
|
|
// Deliberately simple: something@something.something. The real test of an
|
|
// address is sending it an email.
|
|
validate: (value) =>
|
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? "" : "That doesn't look like an email.",
|
|
},
|
|
password: {
|
|
input: document.getElementById("password"),
|
|
error: document.getElementById("passwordError"),
|
|
validate: (value) => (value.length >= 8 ? "" : "Use at least 8 characters."),
|
|
},
|
|
confirm: {
|
|
input: document.getElementById("confirm"),
|
|
error: document.getElementById("confirmError"),
|
|
validate: (value) =>
|
|
value === fields.password.input.value ? "" : "Passwords don't match.",
|
|
},
|
|
};
|
|
|
|
// Run one rule and show the result right under its field. aria-invalid marks
|
|
// the input as wrong for screen readers, and the input's aria-describedby
|
|
// (in index.html) makes them read the message out too.
|
|
function checkField(field) {
|
|
const message = field.validate(field.input.value);
|
|
field.error.textContent = message;
|
|
field.input.setAttribute("aria-invalid", message ? "true" : "false");
|
|
return message === "";
|
|
}
|
|
|
|
// Check a field when you leave it ("blur"): early feedback, without nagging
|
|
// while you're still typing.
|
|
for (const field of Object.values(fields)) {
|
|
field.input.addEventListener("blur", () => checkField(field));
|
|
}
|
|
|
|
// On submit, check every field so ALL the errors show at once, then move the
|
|
// cursor to the first one that needs fixing.
|
|
form.addEventListener("submit", (event) => {
|
|
event.preventDefault(); // this demo only validates; nothing is sent anywhere
|
|
|
|
const invalid = Object.values(fields).filter((field) => !checkField(field));
|
|
|
|
successEl.textContent = invalid.length === 0 ? "All good! ✓" : "";
|
|
if (invalid.length > 0) invalid[0].input.focus();
|
|
});
|