diff --git a/package-lock.json b/package-lock.json index 821115081..5844bd070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "nanoid": "^4.0.0", "ng-click-outside": "^9.0.1", "ngx-autosize": "^2.0.4", + "ngx-image-cropper": "^9.1.5", "ngx-mask": "^13.0.3", "rxjs": "~7.5.0", "snakecase-keys": "^5.4.2", @@ -14735,6 +14736,19 @@ "@angular/core": ">12.0.0" } }, + "node_modules/ngx-image-cropper": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/ngx-image-cropper/-/ngx-image-cropper-9.1.5.tgz", + "integrity": "sha512-I8MAjMREtXDALgYyeMIvcqE+WpXA4IA9Z59JsujDuqOnjFY/Mpb+8JUyVVuOa0Irp20ukDPQJZmECR4v96hTrg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=17.3.0", + "@angular/core": ">=17.3.0" + } + }, "node_modules/ngx-mask": { "version": "13.2.2", "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-13.2.2.tgz", @@ -30921,6 +30935,14 @@ "tslib": ">2.0.0" } }, + "ngx-image-cropper": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/ngx-image-cropper/-/ngx-image-cropper-9.1.5.tgz", + "integrity": "sha512-I8MAjMREtXDALgYyeMIvcqE+WpXA4IA9Z59JsujDuqOnjFY/Mpb+8JUyVVuOa0Irp20ukDPQJZmECR4v96hTrg==", + "requires": { + "tslib": "^2.3.0" + } + }, "ngx-mask": { "version": "13.2.2", "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-13.2.2.tgz", diff --git a/package.json b/package.json index f668fb1c5..4dae92da2 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "nanoid": "^4.0.0", "ng-click-outside": "^9.0.1", "ngx-autosize": "^2.0.4", + "ngx-image-cropper": "^9.1.5", "ngx-mask": "^13.0.3", "rxjs": "~7.5.0", "snakecase-keys": "^5.4.2", diff --git a/projects/core/src/consts/list-years.ts b/projects/core/src/consts/list-years.ts deleted file mode 100644 index 6024d80ed..000000000 --- a/projects/core/src/consts/list-years.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** @format */ - -export const yearList = [ - { - value: "2000 год", - id: 0, - label: "2000 год", - }, - { - value: "2001 год", - id: 1, - label: "2001 год", - }, - { - value: "2002 год", - id: 2, - label: "2002 год", - }, - { - value: "2003 год", - id: 3, - label: "2003 год", - }, - { - value: "2004 год", - id: 4, - label: "2004 год", - }, - { - value: "2005 год", - id: 5, - label: "2005 год", - }, - { - value: "2006 год", - id: 6, - label: "2006 год", - }, - { - value: "2007 год", - id: 7, - label: "2007 год", - }, - { - value: "2008 год", - id: 8, - label: "2008 год", - }, - { - value: "2009 год", - id: 9, - label: "2009 год", - }, - { - value: "2010 год", - id: 10, - label: "2010 год", - }, - { - value: "2011 год", - id: 11, - label: "2011 год", - }, - { - value: "2012 год", - id: 12, - label: "2012 год", - }, - { - value: "2013 год", - id: 13, - label: "2013 год", - }, - { - value: "2014 год", - id: 14, - label: "2014 год", - }, - { - value: "2015 год", - id: 15, - label: "2015 год", - }, - { - value: "2016 год", - id: 16, - label: "2016 год", - }, - { - value: "2017 год", - id: 17, - label: "2017 год", - }, - { - value: "2018 год", - id: 18, - label: "2018 год", - }, - { - value: "2019 год", - id: 19, - label: "2019 год", - }, - { - value: "2020 год", - id: 20, - label: "2020 год", - }, - { - value: "2021 год", - id: 21, - label: "2021 год", - }, - { - value: "2022 год", - id: 22, - label: "2022 год", - }, - { - value: "2023 год", - id: 23, - label: "2023 год", - }, - { - value: "2024 год", - id: 24, - label: "2024 год", - }, - { - value: "2025 год", - id: 25, - label: "2025 год", - }, - { - value: "2026 год", - id: 26, - label: "2026 год", - }, - { - value: "2027 год", - id: 27, - label: "2027 год", - }, - { - value: "2028 год", - id: 28, - label: "2028 год", - }, - { - value: "2029 год", - id: 29, - label: "2029 год", - }, - { - value: "2030 год", - id: 30, - label: "2030 год", - }, - { - value: "2031 год", - id: 31, - label: "2031 год", - }, - { - value: 2025, - id: 25, - label: "настоящее время", - }, -]; diff --git a/projects/core/src/lib/services/validation.service.ts b/projects/core/src/lib/services/validation.service.ts index bd358a0dc..ceff06c13 100644 --- a/projects/core/src/lib/services/validation.service.ts +++ b/projects/core/src/lib/services/validation.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms"; +import { PasswordValidationErrors } from "@auth/models/password-errors.model"; import * as dayjs from "dayjs"; import * as cpf from "dayjs/plugin/customParseFormat"; import * as relativeTime from "dayjs/plugin/relativeTime"; @@ -58,7 +59,6 @@ export class ValidationService { return group => { const controls = [group.get(left), group.get(right)]; - // Проверяем существование контролов if (!controls.every(Boolean)) { throw new Error(`No control with name ${left} or ${right}`); } @@ -66,16 +66,13 @@ export class ValidationService { const isMatching = controls[0]?.value === controls[1]?.value; if (!isMatching) { - // Устанавливаем ошибку для обоих контролов controls.forEach(c => c?.setErrors({ ...(c.errors || {}), unMatch: true })); return { unMatch: true }; } - // Удаляем ошибку если поля совпадают controls.forEach(c => { if (c?.errors) { delete c.errors?.["unMatch"]; - // Если других ошибок нет, очищаем errors полностью if (!Object.keys(c.errors).length) { c.setErrors(null); } @@ -86,6 +83,123 @@ export class ValidationService { }; } + /** + * Валидатор для проверки силы пароля + * + * Требования к паролю: + * - Минимум 8 символов + * - Минимум одна заглавная буква (A-Z) + * - Минимум одна строчная буква (a-z) + * - Минимум одна цифра (0-9) + * - Минимум один специальный символ (!@#$%^&*()_+-=[]{}|;:,.<>?) + * - Не должен содержать пробелы + * - Не должен содержать последовательности типа "123456" или "abcdef" + * - Не должен содержать повторяющиеся символы более 2 раз подряд + */ + usePasswordValidator(minLength = 6): ValidatorFn { + return (control: AbstractControl): PasswordValidationErrors | null => { + const value: string = control.value; + + if (!value) { + return null; + } + + const errors: PasswordValidationErrors = {}; + + if (value.length < minLength) { + errors.passwordTooShort = { + requiredLength: minLength, + actualLength: value.length, + }; + } + + if (!/[A-Z]/.test(value)) { + errors.passwordNoUppercase = { + message: "Пароль должен содержать минимум одну заглавную букву", + }; + } + + if (!/[a-z]/.test(value)) { + errors.passwordNoLowercase = { + message: "Пароль должен содержать минимум одну строчную букву", + }; + } + + if (!/[0-9]/.test(value)) { + errors.passwordNoNumber = { + message: "Пароль должен содержать минимум одну цифру", + }; + } + + if (!/[!@#$%^&*()_+\-=[\]{}|;:,.<>?]/.test(value)) { + errors.passwordNoSpecialChar = { + message: "Пароль должен содержать минимум один специальный символ (!@#$%^&* и т.д.)", + }; + } + + if (/\s/.test(value)) { + errors.passwordHasSpaces = { + message: "Пароль не должен содержать пробелы", + }; + } + + if (this.hasSequence(value)) { + errors.passwordHasSequence = { + message: "Пароль не должен содержать последовательности символов", + }; + } + + if (this.hasRepeatingChars(value)) { + errors.passwordHasRepeating = { + message: "Пароль не должен содержать более 2 одинаковых символов подряд", + }; + } + + return Object.keys(errors).length > 0 ? errors : null; + }; + } + + /** + * Проверяет наличие последовательностей в пароле + */ + private hasSequence(password: string): boolean { + const sequences = [ + "01234567890", + "09876543210", + "abcdefghijklmnopqrstuvwxyz", + "zyxwvutsrqponmlkjihgfedcba", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "ZYXWVUTSRQPONMLKJIHGFEDCBA", + "qwertyuiopasdfghjklzxcvbnm", + "йцукенгшщзхъфывапролджэячсмитьбю", + ]; + + const lowerPassword = password.toLowerCase(); + + for (const sequence of sequences) { + for (let i = 0; i <= sequence.length - 4; i++) { + const subSequence = sequence.substring(i, i + 4); + if (lowerPassword.includes(subSequence)) { + return true; + } + } + } + + return false; + } + + /** + * Проверяет наличие более 2 повторяющихся символов подряд + */ + private hasRepeatingChars(password: string): boolean { + for (let i = 0; i < password.length - 2; i++) { + if (password[i] === password[i + 1] && password[i + 1] === password[i + 2]) { + return true; + } + } + return false; + } + /** * Валидатор для проверки формата даты DD.MM.YYYY * @param control - Контрол формы для валидации @@ -101,10 +215,8 @@ export class ValidationService { */ useDateFormatValidator(control: AbstractControl): ValidationErrors | null { try { - // Строгая проверка формата DD.MM.YYYY const value = dayjs(control.value, "DD.MM.YYYY", true); - // Проверяем что дата не в будущем и валидна if (control.value && (value.fromNow().includes("in") || !value.isValid())) { return { invalidDateFormat: true }; } @@ -140,13 +252,49 @@ export class ValidationService { useAgeValidator(age = 14): ValidatorFn { return control => { const value = dayjs(control.value, "DD.MM.YYYY", true); + const difference = dayjs().diff(value, "year"); - if (value.isValid()) { - const difference = dayjs().diff(value, "year"); - return difference >= age ? null : { tooYoung: { requiredAge: age } }; - } + const isInvalidDate = !value.isValid() || value.year() < 1900; + const isTooYoung = difference < age; + const isTooOld = difference > 100; - return null; + return isInvalidDate + ? { invalidDateFormat: { requiredAge: 100 } } + : isTooYoung + ? { tooYoung: { requiredAge: age } } + : isTooOld + ? { tooOld: { requiredAge: 100 } } + : null; + }; + } + + /** + * Создает валидатор для проверки валидности полного email + * @returns ValidatorFn для проверки возраста + * + * Применение: + * - Проверка валидности + * - Валидация полного email + * + * Логика: + * 1. Создаем регулрку для email + * 2. Тестируем подходит ли она нам + * + * Пример использования: + * email: ['', [ + * Validators.required, + * this.validationService.useEmailValidator() + * ]] + */ + useEmailValidator(): ValidatorFn { + return control => { + const value = control.value; + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (regex.test(value)) { + return null; + } else { + return { invalidEmail: {} }; + } }; } diff --git a/projects/skills/src/app/shared/sidebar-profile/sidebar-profile.component.html b/projects/skills/src/app/shared/sidebar-profile/sidebar-profile.component.html index 37c8266a6..37c6c2922 100644 --- a/projects/skills/src/app/shared/sidebar-profile/sidebar-profile.component.html +++ b/projects/skills/src/app/shared/sidebar-profile/sidebar-profile.component.html @@ -20,13 +20,13 @@
Ошибка в доступе к программе!
++ {{ showProgramModalErrorMessage() }} +
+ } + +{{ project.name }}
- @if (industryService.industries | async; as industries) { -- @if (industryService.getIndustry(industries, project.industry); as industry) { - - {{ industry ? industry.name : "Error" }} - +
{{ project.name }}
+ @if (industryService.industries | async; as industries) { ++ @if (industryService.getIndustry(industries, project.industry); as industry) { + + {{ industry ? industry.name : "Error" }} + + } +
} - - } -{{ project.shortDescription }}
-Компания
-{{ project.shortDescription }}
+Компания
+Черновик
diff --git a/projects/social_platform/src/app/office/shared/project-card/project-card.component.scss b/projects/social_platform/src/app/office/shared/project-card/project-card.component.scss index 751048c6f..7fb742dda 100644 --- a/projects/social_platform/src/app/office/shared/project-card/project-card.component.scss +++ b/projects/social_platform/src/app/office/shared/project-card/project-card.component.scss @@ -7,6 +7,9 @@ flex-direction: column; &__body { + display: flex; + flex-direction: column; + justify-content: space-between; height: 200px; padding: 24px 15px; background-color: var(--white); @@ -41,6 +44,12 @@ border-radius: 15px 15px 0 0; } + &__content { + display: flex; + flex-direction: column; + flex-grow: 1; + } + &__head { display: flex; align-items: center; @@ -70,7 +79,7 @@ display: flex; gap: 15px; align-items: center; - margin-top: 22px; + margin-top: 15px; color: var(--dark-grey); &--single { @@ -128,10 +137,10 @@ display: flex; align-items: center; justify-content: space-between; - margin-top: 16px; + margin-top: auto; &:empty { - margin-top: 0; + margin-top: auto; } } } diff --git a/projects/social_platform/src/app/office/vacancies/list/list.component.ts b/projects/social_platform/src/app/office/vacancies/list/list.component.ts index d1f49f2e1..46a59679e 100644 --- a/projects/social_platform/src/app/office/vacancies/list/list.component.ts +++ b/projects/social_platform/src/app/office/vacancies/list/list.component.ts @@ -113,14 +113,14 @@ export class VacanciesListComponent { // Подписка на изменения параметров запроса для фильтрации const queryParams$ = this.route.queryParams .pipe( - debounceTime(200), // Задержка для избежания частых запросов + debounceTime(200), // Задержка tap(params => { // Извлечение параметров фильтрации из URL const requiredExperience = params["required_experience"] ? params["required_experience"] : undefined; - // Установка значения поиска без вызова события + // Установка значения поиска this.searchForm .get("search") ?.setValue(params["role_contains"] || "", { emitEvent: false }); @@ -137,7 +137,7 @@ export class VacanciesListComponent { this.salaryMin.set(salaryMin); this.salaryMax.set(salaryMax); }), - switchMap(() => this.onFetch(0, 20)) // Загрузка данных с новыми фильтрами + switchMap(() => this.onFetch(0, 20)) ) .subscribe((result: any) => { this.vacancyList.set(result.results); diff --git a/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.html b/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.html index bf2e7f4a0..98d605901 100644 --- a/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.html +++ b/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.html @@ -6,11 +6,6 @@