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(/^$/);
+ }
}