diff --git a/src/frontend/src/app/shared/index.ts b/src/frontend/src/app/shared/index.ts index b93d7e5..4dd3953 100644 --- a/src/frontend/src/app/shared/index.ts +++ b/src/frontend/src/app/shared/index.ts @@ -7,3 +7,7 @@ export * from './components/ui/connectivity-overlay/connectivity-overlay.compone // Pipes export * from './pipes/credits.pipe'; export * from './pipes/initials.pipe'; + +// Validators +export * from './validators/email.validator'; +export * from './validators/name.validator'; diff --git a/src/frontend/src/app/shared/validators/email.validator.ts b/src/frontend/src/app/shared/validators/email.validator.ts new file mode 100644 index 0000000..1dedd52 --- /dev/null +++ b/src/frontend/src/app/shared/validators/email.validator.ts @@ -0,0 +1,30 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +/** + * Email validation pattern that matches standard email format. + * More strict than HTML5 default email validation. + */ +const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + +/** + * Custom email validator that provides stricter validation than Angular's built-in. + * Validates format: local@domain.tld + * + * @returns ValidatorFn that returns null if valid, or { email: true } if invalid + * + * @example + * ```typescript + * this.form = this.fb.group({ + * email: ['', [Validators.required, emailValidator()]] + * }); + * ``` + */ +export function emailValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; // Let required validator handle empty values + } + const isValid = EMAIL_PATTERN.test(control.value); + return isValid ? null : { email: true }; + }; +} diff --git a/src/frontend/src/app/shared/validators/index.ts b/src/frontend/src/app/shared/validators/index.ts new file mode 100644 index 0000000..5fd0d4c --- /dev/null +++ b/src/frontend/src/app/shared/validators/index.ts @@ -0,0 +1,2 @@ +export * from './email.validator'; +export * from './name.validator'; diff --git a/src/frontend/src/app/shared/validators/name.validator.ts b/src/frontend/src/app/shared/validators/name.validator.ts new file mode 100644 index 0000000..40a92ac --- /dev/null +++ b/src/frontend/src/app/shared/validators/name.validator.ts @@ -0,0 +1,64 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +/** + * Name validation options. + */ +export interface NameValidatorOptions { + /** Minimum length required (default: 3) */ + minLength?: number; + /** Maximum length allowed (default: 100) */ + maxLength?: number; + /** Whether to allow numbers (default: false) */ + allowNumbers?: boolean; +} + +const DEFAULT_OPTIONS: Required = { + minLength: 3, + maxLength: 100, + allowNumbers: false, +}; + +/** + * Custom name validator with configurable options. + * Validates that name contains only letters, spaces, and common punctuation. + * + * @param options - Validation options + * @returns ValidatorFn that returns validation errors or null + * + * @example + * ```typescript + * this.form = this.fb.group({ + * name: ['', [Validators.required, nameValidator({ minLength: 2 })]] + * }); + * ``` + */ +export function nameValidator(options: NameValidatorOptions = {}): ValidatorFn { + const opts = { ...DEFAULT_OPTIONS, ...options }; + + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; // Let required validator handle empty values + } + + const value = control.value.trim(); + + if (value.length < opts.minLength) { + return { minlength: { requiredLength: opts.minLength, actualLength: value.length } }; + } + + if (value.length > opts.maxLength) { + return { maxlength: { requiredLength: opts.maxLength, actualLength: value.length } }; + } + + // Pattern: letters (including accented), spaces, hyphens, apostrophes + const pattern = opts.allowNumbers + ? /^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ0-9\s'-]+$/ + : /^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\s'-]+$/; + + if (!pattern.test(value)) { + return { pattern: true }; + } + + return null; + }; +}