diff --git a/html/app.js b/html/app.js index 538ce61..eef4b8d 100644 --- a/html/app.js +++ b/html/app.js @@ -15,6 +15,7 @@ document.addEventListener("DOMContentLoaded", () => { delete: false, }, registerData: { + // will be overridden to a sensible default in mounted() date: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().substr(0, 10), firstname: undefined, lastname: undefined, @@ -31,7 +32,36 @@ document.addEventListener("DOMContentLoaded", () => { customNationality: false, nationalities: [], }, + + computed: { + // Allowed DOB window: [today - 100 years, today - 8 years] + minDOB() { return this.yearsAgo(100); }, + maxDOB() { return this.yearsAgo(8); }, + }, + + watch: { + // Clamp any manual edits/pastes into the allowed range + "registerData.date"(val) { + if (!val) return; + if (val < this.minDOB) this.registerData.date = this.minDOB; + else if (val > this.maxDOB) this.registerData.date = this.maxDOB; + }, + }, + methods: { + // --- helpers for safe, local YYYY-MM-DD strings --- + localISO(date) { + return new Date(date.getTime() - date.getTimezoneOffset() * 60000) + .toISOString() + .slice(0, 10); + }, + yearsAgo(years) { + const d = new Date(); + d.setFullYear(d.getFullYear() - years); + return this.localISO(d); + }, + + // --- existing handlers (unchanged logic) --- click_character: function (idx, type) { this.selectedCharacter = idx; @@ -94,7 +124,8 @@ document.addEventListener("DOMContentLoaded", () => { this.show.characters = false; this.show.register = true; this.registerData = { - date: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().substr(0, 10), + // sensible default (21yo) that’s within the allowed window + date: this.yearsAgo(21), firstname: undefined, lastname: undefined, nationality: undefined, @@ -144,8 +175,13 @@ document.addEventListener("DOMContentLoaded", () => { return translationManager.translate(key); }, }, + mounted() { initializeValidator(); + + // Force a safe default DOB when the app boots + this.registerData.date = this.yearsAgo(21); + var loadingProgress = 0; var loadingDots = 0; window.addEventListener("message", (event) => { diff --git a/html/index.html b/html/index.html index 04740f6..6b9b869 100644 --- a/html/index.html +++ b/html/index.html @@ -106,7 +106,7 @@ - + {{translate('cancel')}} {{translate('confirm')}} diff --git a/html/translations.js b/html/translations.js index 1ae1d67..7366427 100644 --- a/html/translations.js +++ b/html/translations.js @@ -41,6 +41,8 @@ class TranslationManager { invalid_date: "Please enter a valid date of birth.", date: "Date of Birth", field: "Field", + age_too_young: "Birthdate must make your character at least 8 years old.", + age_too_old: "Birthdate cannot make your character older than 100 years.", }; } diff --git a/html/validation.js b/html/validation.js index b9a14d9..946c3c7 100644 --- a/html/validation.js +++ b/html/validation.js @@ -4,111 +4,116 @@ */ class CharacterValidator { - constructor(profanityRegex) { - this.profanityRegex = profanityRegex; - this.validators = { - firstname: [ - { test: (value) => !!value, message: "forgotten_field" }, - { test: (value) => value.length >= 2, message: "firstname_too_short" }, - { test: (value) => value.length <= 16, message: "firstname_too_long" }, - { test: (value) => !this.profanityRegex.test(value), message: "profanity" }, - ], - lastname: [ - { test: (value) => !!value, message: "forgotten_field" }, - { test: (value) => value.length >= 2, message: "lastname_too_short" }, - { test: (value) => value.length <= 16, message: "lastname_too_long" }, - { test: (value) => !this.profanityRegex.test(value), message: "profanity" }, - ], - nationality: [ - { test: (value) => !!value, message: "forgotten_field" }, - { test: (value) => !this.profanityRegex.test(value), message: "profanity" }, - ], - gender: [{ test: (value) => !!value, message: "forgotten_field" }], - date: [ - { test: (value) => !!value, message: "forgotten_field" }, - { test: (value) => this.isValidDate(value), message: "invalid_date" }, - ], - }; - } - - /** - * Validate a complete character form - * @param {Object} character - Character data object - * @returns {Object} Result with isValid flag and any error message - */ - validateCharacter(character) { - // Check each field - for (const field in this.validators) { - if (this.validators.hasOwnProperty(field)) { - const result = this.validateField(field, character[field]); - if (!result.isValid) { - return result; - } - } - } + constructor(profanityRegex) { + this.profanityRegex = profanityRegex; - // Additional cross-field validations could go here + this.validators = { + firstname: [ + { test: (value) => !!value, message: "forgotten_field" }, + { test: (value) => value.length >= 2, message: "firstname_too_short" }, + { test: (value) => value.length <= 16, message: "firstname_too_long" }, + { test: (value) => !this.profanityRegex.test(value), message: "profanity" }, + ], + lastname: [ + { test: (value) => !!value, message: "forgotten_field" }, + { test: (value) => value.length >= 2, message: "lastname_too_short" }, + { test: (value) => value.length <= 16, message: "lastname_too_long" }, + { test: (value) => !this.profanityRegex.test(value), message: "profanity" }, + ], + nationality: [ + { test: (value) => !!value, message: "forgotten_field" }, + { test: (value) => !this.profanityRegex.test(value), message: "profanity" }, + ], + gender: [{ test: (value) => !!value, message: "forgotten_field" }], + date: [ + { test: (value) => !!value, message: "forgotten_field" }, + { test: (value) => this.isValidDate(value), message: "invalid_date" }, + // enforce age range: min 8 years, max 100 years + { test: (value) => !this.isTooYoung(value, 8), message: "age_too_young" }, + { test: (value) => !this.isTooOld(value, 100), message: "age_too_old" }, + ], + }; + } - return { isValid: true }; + /** + * Validate a complete character form + * @param {Object} character - Character data object + * @returns {Object} Result with isValid flag and any error message + */ + validateCharacter(character) { + for (const field in this.validators) { + if (this.validators.hasOwnProperty(field)) { + const result = this.validateField(field, character[field]); + if (!result.isValid) { + return result; + } + } } + return { isValid: true }; + } - /** - * Validate a single field - * @param {string} fieldName - Name of the field to validate - * @param {string} value - Value to validate - * @returns {Object} Result with isValid flag and any error message - */ - validateField(fieldName, value) { - const fieldValidators = this.validators[fieldName]; + /** + * Validate a single field + * @param {string} fieldName - Name of the field to validate + * @param {string} value - Value to validate + * @returns {Object} Result with isValid flag and any error message + */ + validateField(fieldName, value) { + const fieldValidators = this.validators[fieldName]; + if (!fieldValidators) return { isValid: true }; - if (!fieldValidators) { - return { isValid: true }; - } - - for (const validator of fieldValidators) { - if (!validator.test(value)) { - return { - isValid: false, - field: fieldName, - message: validator.message, - }; - } - } - - return { isValid: true }; + for (const validator of fieldValidators) { + if (!validator.test(value)) { + return { isValid: false, field: fieldName, message: validator.message }; + } } + return { isValid: true }; + } - /** - * Check if a date string is valid - * @param {string} dateString - Date in YYYY-MM-DD format - * @returns {boolean} True if valid - */ - isValidDate(dateString) { - // Basic format check - if (!/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { - return false; - } + // ----- date helpers ----- + parseISODate(dateString) { + const [y, m, d] = (dateString || "").split("-").map(Number); + if (!y || !m || !d) return new Date("Invalid Date"); + return new Date(Date.UTC(y, m - 1, d)); // UTC midnight + } - // Parse the date - const parts = dateString.split("-"); - const year = parseInt(parts[0], 10); - const month = parseInt(parts[1], 10) - 1; // Months are 0-based - const day = parseInt(parts[2], 10); + todayUTC() { + const now = new Date(); + return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); + } - // Create date and check if valid - const date = new Date(year, month, day); - if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { - return false; - } + isTooYoung(dateString, minYears = 8) { + const dob = this.parseISODate(dateString); + if (isNaN(dob)) return true; + const today = this.todayUTC(); + const cutoff = new Date(Date.UTC(today.getUTCFullYear() - minYears, today.getUTCMonth(), today.getUTCDate())); + return dob > cutoff; // later than (today - minYears) + } - // Check reasonable range - const currentYear = new Date().getFullYear(); - if (year < 1900 || year > currentYear) { - return false; - } + isTooOld(dateString, maxYears = 100) { + const dob = this.parseISODate(dateString); + if (isNaN(dob)) return true; + const today = this.todayUTC(); + const cutoff = new Date(Date.UTC(today.getUTCFullYear() - maxYears, today.getUTCMonth(), today.getUTCDate())); + return dob < cutoff; // earlier than (today - maxYears) + } - return true; - } + /** + * Strict calendar validation + * Accepts only real dates; age bounds handled separately + */ + isValidDate(dateString) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(dateString)) return false; + const [y, m, d] = dateString.split("-").map(Number); + const date = new Date(y, m - 1, d); + return ( + date instanceof Date && + !isNaN(date) && + date.getFullYear() === y && + date.getMonth() === m - 1 && + date.getDate() === d + ); + } } // Create a global instance for the application @@ -116,13 +121,13 @@ let characterValidator; // This function must be called after profanity.js is loaded function initializeValidator() { - if (typeof profList !== "undefined") { - const re = "(" + profList.join("|") + ")\\b"; - const profanityRegex = new RegExp(re, "i"); - characterValidator = new CharacterValidator(profanityRegex); - } else { - console.error("Profanity list not found. Validation may not work correctly."); - // Create with empty regex as fallback - characterValidator = new CharacterValidator(/^$/); - } + if (typeof profList !== "undefined") { + const re = "(" + profList.join("|") + ")\\b"; + const profanityRegex = new RegExp(re, "i"); + characterValidator = new CharacterValidator(profanityRegex); + } else { + console.error("Profanity list not found. Validation may not work correctly."); + // Create with empty regex as fallback + characterValidator = new CharacterValidator(/^$/); + } }