Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion html/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
<template v-slot:activator="{ on, attrs }">
<v-text-field v-model="registerData.date" :label="translate('birthdate')" readonly v-bind="attrs" v-on="on" solo></v-text-field>
</template>
<v-date-picker v-model="registerData.date" min="1900-01-01" max="2100-12-31">
<v-date-picker v-model="registerData.date" :min="minDOB" :max="maxDOB" >
<v-spacer></v-spacer>
<v-btn text class="date-cancel-btn" @click="dataPickerMenu = false">{{translate('cancel')}}</v-btn>
<v-btn text class="date-confirm-btn" @click="$refs.dialog.save(registerData.date)">{{translate('confirm')}}</v-btn>
Expand Down
2 changes: 2 additions & 0 deletions html/translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
};
}

Expand Down
211 changes: 108 additions & 103 deletions html/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,125 +4,130 @@
*/

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