From 98915b34d9fc84061d7f930de17c40b1201c0353 Mon Sep 17 00:00:00 2001 From: Awakich Date: Mon, 18 Aug 2025 17:10:05 +0300 Subject: [PATCH 01/22] fix register, stage-zero onboarding, shared modules --- projects/core/src/consts/list-years.ts | 2 +- .../src/lib/services/validation.service.ts | 46 +++++++++-- .../sidebar-profile.component.html | 4 +- .../app/auth/register/register.component.html | 17 +++- .../app/auth/register/register.component.ts | 5 +- .../src/app/error/models/error-message.ts | 1 + .../stage-two/stage-two.component.scss | 11 +++ .../stage-zero/stage-zero.component.html | 81 ++++++++++--------- .../stage-zero/stage-zero.component.scss | 16 +++- .../stage-zero/stage-zero.component.ts | 80 +++++++++++------- .../office/profile/edit/edit.component.html | 4 +- .../shared/news-card/news-card.component.html | 10 +-- .../projects/detail/info/info.component.html | 2 +- .../office/projects/edit/edit.component.scss | 2 +- .../shared/news-card/news-card.component.html | 10 +-- .../ui/components/input/input.component.html | 7 +- .../ui/components/input/input.component.scss | 3 +- .../ui/components/input/input.component.ts | 7 +- .../components/search/search.component.scss | 2 +- .../components/select/select.component.scss | 2 +- .../profile-control-panel.component.scss | 1 + .../profile-info/profile-info.component.html | 4 +- 22 files changed, 214 insertions(+), 103 deletions(-) diff --git a/projects/core/src/consts/list-years.ts b/projects/core/src/consts/list-years.ts index 6024d80ed..d0923b012 100644 --- a/projects/core/src/consts/list-years.ts +++ b/projects/core/src/consts/list-years.ts @@ -164,6 +164,6 @@ export const yearList = [ { value: 2025, id: 25, - label: "настоящее время", + label: "по наст. вр.", }, ]; diff --git a/projects/core/src/lib/services/validation.service.ts b/projects/core/src/lib/services/validation.service.ts index bd358a0dc..bc42943d9 100644 --- a/projects/core/src/lib/services/validation.service.ts +++ b/projects/core/src/lib/services/validation.service.ts @@ -140,13 +140,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"); + + const isInvalidDate = !value.isValid() || value.year() < 1900; + const isTooYoung = difference < age; + const isTooOld = difference > 100; + + return isInvalidDate + ? { invalidDateFormat: { requiredAge: 100 } } + : isTooYoung + ? { tooYoung: { requiredAge: age } } + : isTooOld + ? { tooOld: { requiredAge: 100 } } + : null; + }; + } - if (value.isValid()) { - const difference = dayjs().diff(value, "year"); - return difference >= age ? null : { tooYoung: { requiredAge: age } }; + /** + * Создает валидатор для проверки валидности полного 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: {} }; } - - return null; }; } 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 @@
{{ user()?.firstName }} {{ user()?.lastName }}
- @if (user()?.verificationDate; as verificationDate) { +
+ } @if (birthday | controlError: "tooOld") { +
+ @if (birthday.errors) { + {{ errorMessage.MAXIMAL_AGE }} + {{ birthday.errors["tooOld"]["requiredAge"] }} лет } +
} @if (birthday | controlError: "invalidDateFormat") {
{{ errorMessage.INVALID_DATE }} @@ -251,7 +257,16 @@

>Нажимая на кнопку подтверждаете, что вам больше 14 лет

- + Далее } @else if (step === "info") { diff --git a/projects/social_platform/src/app/auth/register/register.component.ts b/projects/social_platform/src/app/auth/register/register.component.ts index afc94dd4d..7e426b1b6 100644 --- a/projects/social_platform/src/app/auth/register/register.component.ts +++ b/projects/social_platform/src/app/auth/register/register.component.ts @@ -69,7 +69,10 @@ export class RegisterComponent implements OnInit { this.validationService.useAgeValidator(), ], ], - email: ["", [Validators.required, Validators.email]], + email: [ + "", + [Validators.required, Validators.email, this.validationService.useEmailValidator()], + ], password: ["", [Validators.required, Validators.minLength(6)]], repeatedPassword: ["", [Validators.required]], }, diff --git a/projects/social_platform/src/app/error/models/error-message.ts b/projects/social_platform/src/app/error/models/error-message.ts index 22d44b8f0..b937ad447 100644 --- a/projects/social_platform/src/app/error/models/error-message.ts +++ b/projects/social_platform/src/app/error/models/error-message.ts @@ -30,6 +30,7 @@ export enum ErrorMessage { VALIDATION_TOO_SHORT = "Минимальная длина:", VALIDATION_REQUIRED = "Обязательное поле", MINIMAL_AGE = "Минимальный возраст", + MAXIMAL_AGE = "Максимальный возраст", INVALID_DATE = "Неправильный формат даты", VALIDATION_LANGUAGE = "Используйте символы кириллического алфавита", VALIDATION_EMAIL = "Введенное значение не соответствует формату email", diff --git a/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.scss b/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.scss index 9df1c4526..93adf18a5 100644 --- a/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.scss +++ b/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.scss @@ -1,4 +1,5 @@ @use "styles/responsive"; +@use "styles/typography"; .auth { &__greeting { @@ -80,6 +81,16 @@ &__skills { display: flex; flex-direction: column; + + ::ng-deep { + app-autocomplete-input { + .field__input { + padding: 12px 20px; + + @include typography.body-16; + } + } + } } &__left { diff --git a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.html b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.html index e0e8ab457..bccb6d15f 100644 --- a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.html +++ b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.html @@ -135,7 +135,7 @@

Привет, {{ profile.firstName }} {{ profile.lastNam @@ -166,7 +166,7 @@

Привет, {{ profile.firstName }} {{ profile.lastNam } @if (stageForm.get("educationStatus"); as educationStatus) {
- + Привет, {{ profile.firstName }} {{ profile.lastNam class="edit" appIcon icon="edit-pen" - appSquare="20" + appSquare="24" (click)="editEducation($index)" > @@ -525,43 +525,48 @@

Привет, {{ profile.firstName }} {{ profile.lastNam >{{ tooltipLanguageText }} -
- @if (stageForm.get("language"); as language) { -
- - - - +
+
+ @if (stageForm.get("language"); as language) { +
+ + + + - @if (language | controlError: "required") { -
- {{ errorMessage.VALIDATION_REQUIRED }} -
- } -
- } @if (stageForm.get("languageLevel"); as languageLevel) { -
- - - + @if (language | controlError: "required") { +
+ {{ errorMessage.VALIDATION_REQUIRED }} +
+ } +
+ } @if (stageForm.get("languageLevel"); as languageLevel) { +
+ + + - @if (languageLevel | controlError: "required") { -
- {{ errorMessage.VALIDATION_REQUIRED }} -
+ @if (languageLevel | controlError: "required") { +
+ {{ errorMessage.VALIDATION_REQUIRED }} +
+ } +
} -
- } +
+ Количество добавляемых языков не более 4-х Привет, {{ profile.firstName }} {{ profile.lastNam
-

Произошла ошибка при редактировании!

+

Произошла ошибка при отправке данных!

{{ isModalErrorYearText() }}.

diff --git a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.scss b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.scss index ced455749..a6470607e 100644 --- a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.scss +++ b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.scss @@ -47,11 +47,17 @@ display: flex; gap: 20px; align-items: center; + margin-bottom: 10px; + margin-top: 10px; .years__left, .years__right { width: 50%; } + + &--attention { + color: var(--dark-grey); + } } } @@ -82,7 +88,7 @@ position: absolute; top: 65%; left: 38%; - z-index: 100; + z-index: 10; display: none; width: 250px; padding: 12px; @@ -125,15 +131,16 @@ gap: 20px; align-items: center; justify-content: space-between; - width: 90%; padding: 12px; overflow: hidden; border: 1px solid var(--medium-grey-for-outline); border-radius: 15px; + width: 100%; } &__text { color: var(--dark-grey); + width: 90%; } &__remove { @@ -163,7 +170,6 @@ } .edit { - width: 10%; color: var(--dark-grey); cursor: pointer; } @@ -172,6 +178,10 @@ color: var(--red); } +i { + width: 24px; +} + .cancel { display: flex; flex-direction: column; diff --git a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts index d88fff928..ca0977d10 100644 --- a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts +++ b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts @@ -652,42 +652,60 @@ export class OnboardingStageZeroComponent implements OnInit, OnDestroy { } addLanguage() { - ["language", "languageLevel"].forEach(name => this.stageForm.get(name)?.clearValidators()); - ["language", "languageLevel"].forEach(name => - this.stageForm.get(name)?.setValidators([Validators.required]) - ); - ["language", "languageLevel"].forEach(name => - this.stageForm.get(name)?.updateValueAndValidity() - ); - ["language", "languageLevel"].forEach(name => this.stageForm.get(name)?.markAsTouched()); + const languageValue = this.stageForm.get("language")?.value; + const languageLevelValue = this.stageForm.get("languageLevel")?.value; - const languageItem = this.fb.group({ - language: this.stageForm.get("language")?.value, - languageLevel: this.stageForm.get("languageLevel")?.value, + ["language", "languageLevel"].forEach(name => { + this.stageForm.get(name)?.clearValidators(); }); - if (this.editIndex() !== null) { - this.languageItems.update(items => { - const updatedItems = [...items]; - updatedItems[this.editIndex()!] = languageItem.value; - - this.userLanguages.at(this.editIndex()!).patchValue(languageItem.value); - return updatedItems; + if ((languageValue && !languageLevelValue) || (!languageValue && languageLevelValue)) { + ["language", "languageLevel"].forEach(name => { + this.stageForm.get(name)?.setValidators([Validators.required]); }); - this.editIndex.set(null); - } else { - this.languageItems.update(items => [...items, languageItem.value]); - this.userLanguages.push(languageItem); } + ["language", "languageLevel"].forEach(name => { - this.stageForm.get(name)?.reset(); - this.stageForm.get(name)?.setValue(""); - this.stageForm.get(name)?.clearValidators(); - this.stageForm.get(name)?.markAsPristine(); this.stageForm.get(name)?.updateValueAndValidity(); + this.stageForm.get(name)?.markAsTouched(); + }); + + const isLanguageValid = this.stageForm.get("language")?.valid; + const isLanguageLevelValid = this.stageForm.get("languageLevel")?.valid; + + if (!isLanguageValid || !isLanguageLevelValid) { + return; + } + + const languageItem = this.fb.group({ + language: languageValue, + languageLevel: languageLevelValue, }); - this.editLanguageClick = false; + if (languageValue && languageLevelValue) { + if (this.editIndex() !== null) { + this.languageItems.update(items => { + const updatedItems = [...items]; + updatedItems[this.editIndex()!] = languageItem.value; + + this.userLanguages.at(this.editIndex()!).patchValue(languageItem.value); + return updatedItems; + }); + this.editIndex.set(null); + } else { + this.languageItems.update(items => [...items, languageItem.value]); + this.userLanguages.push(languageItem); + } + ["language", "languageLevel"].forEach(name => { + this.stageForm.get(name)?.reset(); + this.stageForm.get(name)?.setValue(null); + this.stageForm.get(name)?.clearValidators(); + this.stageForm.get(name)?.markAsPristine(); + this.stageForm.get(name)?.updateValueAndValidity(); + }); + + this.editLanguageClick = false; + } } editLanguage(index: number) { @@ -763,7 +781,13 @@ export class OnboardingStageZeroComponent implements OnInit, OnDestroy { .pipe(concatMap(() => this.authService.setOnboardingStage(1))) .subscribe({ next: () => this.completeRegistration(1), - error: () => this.stageSubmitting.set(false), + error: error => { + this.stageSubmitting.set(false); + this.isModalErrorYear.set(true); + if (error.error.language) { + this.isModalErrorYearText.set(error.error.language); + } + }, }); } diff --git a/projects/social_platform/src/app/office/profile/edit/edit.component.html b/projects/social_platform/src/app/office/profile/edit/edit.component.html index 01611cc13..439e622f4 100644 --- a/projects/social_platform/src/app/office/profile/edit/edit.component.html +++ b/projects/social_platform/src/app/office/profile/edit/edit.component.html @@ -287,7 +287,7 @@ [selectedId]="selectedComplitionYearEducationId()" formControlName="completionYear" placeholder="2023 год" - [options]="yearListEducation.slice(5)" + [options]="yearListEducation" > @@ -347,7 +347,7 @@

} @if (profileForm.get("educationStatus"); as educationStatus) {
- + {{ newsItem.likesCount }} - diff --git a/projects/social_platform/src/app/office/projects/detail/info/info.component.html b/projects/social_platform/src/app/office/projects/detail/info/info.component.html index 9ad6b85bc..c800f2630 100644 --- a/projects/social_platform/src/app/office/projects/detail/info/info.component.html +++ b/projects/social_platform/src/app/office/projects/detail/info/info.component.html @@ -286,7 +286,7 @@

Вакансии

} @if(profileId === project.leader){
Добавить вакансию diff --git a/projects/social_platform/src/app/office/projects/edit/edit.component.scss b/projects/social_platform/src/app/office/projects/edit/edit.component.scss index f7b04193a..9ae74823e 100644 --- a/projects/social_platform/src/app/office/projects/edit/edit.component.scss +++ b/projects/social_platform/src/app/office/projects/edit/edit.component.scss @@ -114,7 +114,7 @@ &__save { @include responsive.apply-desktop { position: absolute; - right: 24px; + right: 48px; bottom: 48px; display: flex; gap: 10px; diff --git a/projects/social_platform/src/app/office/shared/news-card/news-card.component.html b/projects/social_platform/src/app/office/shared/news-card/news-card.component.html index dab30ed48..f8a5a7803 100644 --- a/projects/social_platform/src/app/office/shared/news-card/news-card.component.html +++ b/projects/social_platform/src/app/office/shared/news-card/news-card.component.html @@ -98,14 +98,14 @@ {{ feedItem.likesCount }} - diff --git a/projects/social_platform/src/app/ui/components/input/input.component.html b/projects/social_platform/src/app/ui/components/input/input.component.html index de4e0f9ae..5d5d4e083 100644 --- a/projects/social_platform/src/app/ui/components/input/input.component.html +++ b/projects/social_platform/src/app/ui/components/input/input.component.html @@ -19,7 +19,12 @@ @if (error) { } -
+
diff --git a/projects/social_platform/src/app/ui/components/input/input.component.scss b/projects/social_platform/src/app/ui/components/input/input.component.scss index 08215c8bb..f44fa86b3 100644 --- a/projects/social_platform/src/app/ui/components/input/input.component.scss +++ b/projects/social_platform/src/app/ui/components/input/input.component.scss @@ -48,8 +48,7 @@ &__right-icon { position: absolute; - top: 20%; - right: 24px; + top: 27%; display: flex; align-items: center; justify-content: center; diff --git a/projects/social_platform/src/app/ui/components/input/input.component.ts b/projects/social_platform/src/app/ui/components/input/input.component.ts index f42b3f330..80734f233 100644 --- a/projects/social_platform/src/app/ui/components/input/input.component.ts +++ b/projects/social_platform/src/app/ui/components/input/input.component.ts @@ -1,7 +1,8 @@ /** @format */ -import { Component, EventEmitter, forwardRef, Input, type OnInit, Output } from "@angular/core"; -import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from "@angular/core"; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { IconComponent } from "@ui/components"; import { NgxMaskModule } from "ngx-mask"; @@ -35,7 +36,7 @@ import { NgxMaskModule } from "ngx-mask"; }, ], standalone: true, - imports: [NgxMaskModule, IconComponent], + imports: [CommonModule, NgxMaskModule, IconComponent], }) export class InputComponent implements OnInit, ControlValueAccessor { constructor() {} diff --git a/projects/social_platform/src/app/ui/components/search/search.component.scss b/projects/social_platform/src/app/ui/components/search/search.component.scss index 5aea863ae..bc4f8d116 100644 --- a/projects/social_platform/src/app/ui/components/search/search.component.scss +++ b/projects/social_platform/src/app/ui/components/search/search.component.scss @@ -16,7 +16,7 @@ } &__hidden { - height: 22px; + height: 18px; } &__input { diff --git a/projects/social_platform/src/app/ui/components/select/select.component.scss b/projects/social_platform/src/app/ui/components/select/select.component.scss index c6819a050..2a0471e56 100644 --- a/projects/social_platform/src/app/ui/components/select/select.component.scss +++ b/projects/social_platform/src/app/ui/components/select/select.component.scss @@ -43,7 +43,7 @@ right: 0; bottom: -6px; left: 0; - z-index: 3; + z-index: 11; max-height: 200px; padding: 10px; overflow-y: auto; diff --git a/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.scss b/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.scss index 0b3c901db..c6bf45676 100644 --- a/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.scss +++ b/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.scss @@ -60,6 +60,7 @@ padding: 26px 10px 10px; background-color: var(--light-gray); border-radius: var(--rounded-lg); + width: 100%; } } diff --git a/projects/ui/src/lib/components/layout/profile-info/profile-info.component.html b/projects/ui/src/lib/components/layout/profile-info/profile-info.component.html index 7b09eb0c1..509f3be5f 100644 --- a/projects/ui/src/lib/components/layout/profile-info/profile-info.component.html +++ b/projects/ui/src/lib/components/layout/profile-info/profile-info.component.html @@ -7,13 +7,13 @@
{{ user.firstName }} {{ user.lastName }}
- @if (user.verificationDate; as verificationDate) { +
    @for (p of user.programs.slice(0, 3); track p.id) { -
  • +
  • } diff --git a/projects/social_platform/src/app/office/profile/edit/edit.component.html b/projects/social_platform/src/app/office/profile/edit/edit.component.html index 439e622f4..7496f7ce4 100644 --- a/projects/social_platform/src/app/office/profile/edit/edit.component.html +++ b/projects/social_platform/src/app/office/profile/edit/edit.component.html @@ -4,7 +4,7 @@

    Редактировать профиль

    - + Назад
    diff --git a/projects/social_platform/src/app/office/profile/edit/edit.component.ts b/projects/social_platform/src/app/office/profile/edit/edit.component.ts index 47b68ba01..70d804796 100644 --- a/projects/social_platform/src/app/office/profile/edit/edit.component.ts +++ b/projects/social_platform/src/app/office/profile/edit/edit.component.ts @@ -39,14 +39,13 @@ import { ModalComponent } from "@ui/components/modal/modal.component"; import { Skill } from "@office/models/skill"; import { SkillsService } from "@office/services/skills.service"; import { navItems } from "projects/core/src/consts/navProfileItems"; -import { yearList } from "projects/core/src/consts/list-years"; import { educationUserLevel, educationUserType } from "projects/core/src/consts/list-education"; import { languageLevelsList, languageNamesList } from "projects/core/src/consts/list-language"; import { transformYearStringToNumber } from "@utils/transformYear"; import { yearRangeValidators } from "@utils/yearRangeValidators"; -import { CheckboxComponent } from "../../../ui/components/checkbox/checkbox.component"; import { User } from "@auth/models/user.model"; import { SwitchComponent } from "@ui/components/switch/switch.component"; +import { generateYearList } from "@utils/generate-year-list"; dayjs.extend(cpf); @@ -372,7 +371,7 @@ export class ProfileEditComponent implements OnInit, OnDestroy, AfterViewInit { ctl?.setValue(!ctl.value); } - readonly yearListEducation = yearList; + readonly yearListEducation = generateYearList(55); readonly educationStatusList = educationUserType; @@ -975,8 +974,4 @@ export class ProfileEditComponent implements OnInit, OnDestroy, AfterViewInit { toggleSpecsGroupsModal(): void { this.specsGroupsModalOpen.update(open => !open); } - - onBack() { - this.location.back(); - } } diff --git a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html b/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html index b2be5a572..d7b88b035 100644 --- a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html +++ b/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html @@ -11,7 +11,7 @@ }
    - {{ newsItem.datetimeCreated | dayjs: "format":"DD MMMM, HH:mm" }} + {{ newsItem.datetimeCreated | dayjs: "format":"DD MMMM YYYY, HH:mm" }}
    diff --git a/projects/social_platform/src/app/office/shared/news-card/news-card.component.html b/projects/social_platform/src/app/office/shared/news-card/news-card.component.html index f8a5a7803..b5b6c8f19 100644 --- a/projects/social_platform/src/app/office/shared/news-card/news-card.component.html +++ b/projects/social_platform/src/app/office/shared/news-card/news-card.component.html @@ -10,7 +10,7 @@
    {{ feedItem.name }}
    - {{ feedItem.datetimeCreated | dayjs: "format":"DD MMMM, HH:mm" }} + {{ feedItem.datetimeCreated | dayjs: "format":"DD MMMM YYYY, HH:mm" }}
    diff --git a/projects/social_platform/src/app/utils/generate-year-list.ts b/projects/social_platform/src/app/utils/generate-year-list.ts new file mode 100644 index 000000000..597bbb1ca --- /dev/null +++ b/projects/social_platform/src/app/utils/generate-year-list.ts @@ -0,0 +1,48 @@ +/** @format */ + +interface yearListElement { + id: number; // порядковый номер в массиве + value: string; // строка, которую будем показывать в UI + label: string; // то же самое, что и value (можно использовать обе подписи) +} + +/** + * Генерирует массив годов. + * + * @param amount – сколько лет нужно вывести. + * Считаем, что список должен начинаться с «текущий‑year‑amount+1» и + * заканчиваться текущим календарным годом. + * @returns массив объектов вида { id, value, label } + * + * Пример: generateYearList(3) (при текущем 2025‑м году) → + * [ + * { id: 0, value: '2023 год', label: '2023 год' }, + * { id: 1, value: '2024 год', label: '2024 год' }, + * { id: 2, value: '2025 год', label: '2025 год' } + * ] + */ +export const generateYearList = (amount: number): yearListElement[] => { + if (amount <= 0) return []; + + const now = new Date().getFullYear(); + const firstYear = now - amount + 1; + const list: yearListElement[] = []; + + for (let i = 0; i < amount; i++) { + const year = firstYear + i; + list.push({ + id: i, + value: `${year} год`, + label: `${year} год`, + }); + } + + const currentId = amount - 1; + list.push({ + id: currentId, + value: `${now} год`, + label: "по наст. вр.", + }); + + return list; +}; From e851ad02474d734110b8caec827ccd33b26be3f6 Mon Sep 17 00:00:00 2001 From: Awakich Date: Tue, 19 Aug 2025 11:55:41 +0300 Subject: [PATCH 04/22] add deletion project in edit & remove achievements block --- .../detail/profile-detail.component.html | 4 +- .../office/projects/edit/edit.component.html | 3 + .../office/projects/edit/edit.component.ts | 15 +++++ .../project-card/project-card.component.html | 67 ++++++++++--------- .../project-card/project-card.component.scss | 15 ++++- 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/projects/social_platform/src/app/office/profile/detail/profile-detail.component.html b/projects/social_platform/src/app/office/profile/detail/profile-detail.component.html index 7ded85773..df1108b76 100644 --- a/projects/social_platform/src/app/office/profile/detail/profile-detail.component.html +++ b/projects/social_platform/src/app/office/profile/detail/profile-detail.component.html @@ -133,12 +133,12 @@

    {{ user.firstName }} {{ user.lastName }}

    } } -
    + diff --git a/projects/social_platform/src/app/office/projects/edit/edit.component.html b/projects/social_platform/src/app/office/projects/edit/edit.component.html index 168ea8126..8376a734a 100644 --- a/projects/social_platform/src/app/office/projects/edit/edit.component.html +++ b/projects/social_platform/src/app/office/projects/edit/edit.component.html @@ -53,6 +53,9 @@ }
    + + Удалить проект + { + this.router.navigateByUrl(`/office/projects/my`); + }, + }); + } + /** * Сохранение проекта как опубликованного с проверкой доп. полей */ diff --git a/projects/social_platform/src/app/office/shared/project-card/project-card.component.html b/projects/social_platform/src/app/office/shared/project-card/project-card.component.html index 05b37125d..6adf340c5 100644 --- a/projects/social_platform/src/app/office/shared/project-card/project-card.component.html +++ b/projects/social_platform/src/app/office/shared/project-card/project-card.component.html @@ -7,43 +7,46 @@
    }
    -
    -
    -

    {{ 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 }}

    -
    -
    - @if (isSubscribed) { -
    -
    - } @if (project.isCompany) { -
    -
    -

    Компания

    -
    - } +
    +

    {{ project.shortDescription }}

    +
    +
    + @if (isSubscribed) { +
    + +
    + } @if (project.isCompany) { +
    +
    +

    Компания

    +
    + } +
    - + +
    +
    @if (project.draft) {

    Черновик

    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..576d8d90a 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 @@ -11,6 +11,9 @@ padding: 24px 15px; background-color: var(--white); border-radius: 15px; + display: flex; + flex-direction: column; + justify-content: space-between; @include responsive.apply-desktop { border: 1px solid var(--medium-grey-for-outline); @@ -41,6 +44,12 @@ border-radius: 15px 15px 0 0; } + &__content { + flex-grow: 1; + display: flex; + flex-direction: column; + } + &__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; } } } From cf1f62fa12c870edd170255b0a70c9044a414c6a Mon Sep 17 00:00:00 2001 From: Awakich Date: Tue, 19 Aug 2025 11:56:10 +0300 Subject: [PATCH 05/22] add styles for project card --- .../shared/project-card/project-card.component.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 576d8d90a..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,13 +7,13 @@ flex-direction: column; &__body { + display: flex; + flex-direction: column; + justify-content: space-between; height: 200px; padding: 24px 15px; background-color: var(--white); border-radius: 15px; - display: flex; - flex-direction: column; - justify-content: space-between; @include responsive.apply-desktop { border: 1px solid var(--medium-grey-for-outline); @@ -45,9 +45,9 @@ } &__content { - flex-grow: 1; display: flex; flex-direction: column; + flex-grow: 1; } &__head { From d52b477cc4384cb2a25ec26eee8f7048f78f2650 Mon Sep 17 00:00:00 2001 From: Awakich Date: Tue, 19 Aug 2025 16:19:16 +0300 Subject: [PATCH 06/22] add team members block in edit project --- .../app/office/models/collaborator.model.ts | 10 +++- .../src/app/office/models/project.model.ts | 11 ++++- .../src/app/office/office.component.scss | 2 +- .../detail/profile-detail.component.scss | 2 +- .../detail/projects/projects.component.html | 18 ++++++-- .../office/projects/edit/edit.component.ts | 1 + .../edit/services/project-team.service.ts | 26 +++++++---- .../project-team-step.component.html | 23 +++++++++- .../project-team-step.component.ts | 11 ++++- .../collaborator-card.component.html | 22 +++++++++ .../collaborator-card.component.scss | 33 +++++++++++++ .../collaborator-card.component.spec.ts | 26 +++++++++++ .../collaborator-card.component.ts | 46 +++++++++++++++++++ .../invite-card/invite-card.component.ts | 3 +- .../app/office/shared/nav/nav.component.html | 2 +- 15 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html create mode 100644 projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.scss create mode 100644 projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.spec.ts create mode 100644 projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.ts diff --git a/projects/social_platform/src/app/office/models/collaborator.model.ts b/projects/social_platform/src/app/office/models/collaborator.model.ts index 40b8f4525..1ee591f65 100644 --- a/projects/social_platform/src/app/office/models/collaborator.model.ts +++ b/projects/social_platform/src/app/office/models/collaborator.model.ts @@ -15,7 +15,15 @@ export class Collaborator { /** Роль участника в проекте (например, "Разработчик", "Дизайнер") */ role!: string; /** Массив ключевых навыков участника */ - keySkills!: string[]; + skills!: { + id: number; + name: string; + category: { + id: number; + name: string; + }; + }[]; + /** URL аватара участника */ avatar!: string; } diff --git a/projects/social_platform/src/app/office/models/project.model.ts b/projects/social_platform/src/app/office/models/project.model.ts index 4da6aaa5f..c600b53b4 100644 --- a/projects/social_platform/src/app/office/models/project.model.ts +++ b/projects/social_platform/src/app/office/models/project.model.ts @@ -106,6 +106,15 @@ const collaborator = { lastName: "string", userId: 0, avatar: "string", - keySkills: ["angular"], + skills: [ + { + id: 309, + name: "Python", + category: { + id: 7, + name: "Back-end", + }, + }, + ], role: "Front-end", }; diff --git a/projects/social_platform/src/app/office/office.component.scss b/projects/social_platform/src/app/office/office.component.scss index be49149e3..86d90ca01 100644 --- a/projects/social_platform/src/app/office/office.component.scss +++ b/projects/social_platform/src/app/office/office.component.scss @@ -88,7 +88,7 @@ transition: color 0.2s; &:hover { - color: var(--black); + color: var(--accent); } &__name { diff --git a/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss b/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss index 7c78d34b0..ae0ff3ecd 100644 --- a/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss +++ b/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss @@ -180,7 +180,7 @@ $detail-bar-mb: 12px; transform: translateX(-50%); @include responsive.apply-desktop { - top: 50%; + top: 10%; bottom: auto; left: 21px; transform: translate(0, -50%); diff --git a/projects/social_platform/src/app/office/profile/detail/projects/projects.component.html b/projects/social_platform/src/app/office/profile/detail/projects/projects.component.html index 5de149c7d..baf44d358 100644 --- a/projects/social_platform/src/app/office/profile/detail/projects/projects.component.html +++ b/projects/social_platform/src/app/office/profile/detail/projects/projects.component.html @@ -1,9 +1,9 @@ - @if (user | async; as user) {
    @if (loggedUserId | async; as loggedUserId) {
    + @if (user.projects.length) {

    {{ @@ -16,12 +16,13 @@

    @for (p of user.projects; track p.id) {
  • - +
  • }

+ } @if (subs | async; as subs) { @if (subs.length) {

{{ @@ -30,18 +31,25 @@

: "Проекты, на которые подписан " + user.firstName }}

- @if (subs | async; as subs) {
    @for (p of subs; track p.id) {
  • - +
  • }
- }
+ } } @if (!user.projects.length) { @if (subs | async; as subs) { @if (!subs.length) { +

+ Вы пока не состоите ни в одном проекте и не подписаны ни на один. +

+ } } @else { +

+ Вы пока не состоите ни в одном проекте и не подписаны ни на один. +

+ } } } diff --git a/projects/social_platform/src/app/office/projects/edit/edit.component.ts b/projects/social_platform/src/app/office/projects/edit/edit.component.ts index 5d5c84764..2c188c1a6 100644 --- a/projects/social_platform/src/app/office/projects/edit/edit.component.ts +++ b/projects/social_platform/src/app/office/projects/edit/edit.component.ts @@ -580,6 +580,7 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy { // Используем сервис для инициализации данных проекта this.projectFormService.initializeProjectData(project); this.projectTeamService.setInvites(invites); + this.projectTeamService.setCollaborators(project.collaborators); // Инициализируем дополнительные поля через сервис if (project.partnerProgram) { diff --git a/projects/social_platform/src/app/office/projects/edit/services/project-team.service.ts b/projects/social_platform/src/app/office/projects/edit/services/project-team.service.ts index f3578d20a..becc9e491 100644 --- a/projects/social_platform/src/app/office/projects/edit/services/project-team.service.ts +++ b/projects/social_platform/src/app/office/projects/edit/services/project-team.service.ts @@ -3,6 +3,7 @@ import { computed, inject, Injectable, signal } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { ValidationService } from "@corelib"; +import { Collaborator } from "@office/models/collaborator.model"; import { Invite } from "@office/models/invite.model"; import { InviteService } from "@services/invite.service"; @@ -19,6 +20,7 @@ export class ProjectTeamService { private readonly validationService = inject(ValidationService); public readonly invites = signal([]); + public readonly collaborators = signal([]); public readonly isInviteModalOpen = signal(false); public readonly inviteNotExistingError = signal(null); @@ -63,6 +65,22 @@ export class ProjectTeamService { this.invites.set(invites); } + /** + * Устанавливает список команды + * @param collaborators массив Collaborator + */ + public setCollaborators(collaborators: Collaborator[]): void { + this.collaborators.set(collaborators); + } + + /** + * Возвращает текущий список команды. + * @returns Collaborator[] массив команды + */ + public getCollaborators(): Collaborator[] { + return this.collaborators(); + } + /** * Возвращает текущий список приглашений. * @returns Invite[] массив приглашений @@ -84,14 +102,6 @@ export class ProjectTeamService { return this.inviteForm.get("specialization"); } - /** - * Проверяет, заполнены ли все приглашения (accepted === null). - * @returns boolean true если все приглашения приняты или отклонены - */ - public readonly invitesFill = computed( - () => this.invites().length > 0 && this.invites().every(inv => inv.isAccepted === null) - ); - /** * Открывает модальное окно для отправки приглашения. */ diff --git a/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.html b/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.html index 6ee82d631..a61b685be 100644 --- a/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.html +++ b/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.html @@ -5,8 +5,27 @@
    + @for (collaborator of collaborators; track collaborator.userId) { +
  • + +
  • + } +
+ + +
    @for (user of invites; track user.id) { @if (user.isAccepted === null) {
  • diff --git a/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.ts b/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.ts index 82c44d127..9e0ccbe42 100644 --- a/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.ts +++ b/projects/social_platform/src/app/office/projects/edit/shared/project-team-step/project-team-step.component.ts @@ -12,6 +12,7 @@ import { ProjectTeamService } from "../../services/project-team.service"; import { rolesMembersList } from "projects/core/src/consts/list-roles-members"; import { ActivatedRoute } from "@angular/router"; import { IconComponent } from "@uilib"; +import { CollaboratorCardComponent } from "@office/shared/collaborator-card/collaborator-card.component"; @Component({ selector: "app-project-team-step", @@ -28,6 +29,7 @@ import { IconComponent } from "@uilib"; ControlErrorPipe, InviteCardComponent, ModalComponent, + CollaboratorCardComponent, ], }) export class ProjectTeamStepComponent implements OnInit { @@ -41,6 +43,7 @@ export class ProjectTeamStepComponent implements OnInit { ngOnInit(): void { this.projectTeamService.setInvites(this.invites); + this.projectTeamService.setCollaborators(this.collaborators); // Настраиваем динамическую валидацию this.projectTeamService.setupDynamicValidation(); @@ -68,8 +71,12 @@ export class ProjectTeamStepComponent implements OnInit { return this.projectTeamService.getInvites(); } - get invitesFill() { - return this.projectTeamService.invitesFill; + get collaborators() { + return this.projectTeamService.getCollaborators(); + } + + get invitesFill(): boolean { + return this.invites.some(inv => inv.isAccepted === null); } get isInviteModalOpen() { diff --git a/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html new file mode 100644 index 000000000..8b4fb548e --- /dev/null +++ b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html @@ -0,0 +1,22 @@ + + +@if (collaborator) { +
    +
    +

    + {{ collaborator.firstName }} {{ collaborator.lastName }} +

    + +

    + @for (skill of collaborator.skills; track $index) { +

    +

    Категория - {{ skill.category.name }}

    +

    Навык - {{ skill.name }}

    +
    + } +

    + +

    Роль - {{ collaborator.role }}

    +
    +
    +} diff --git a/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.scss b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.scss new file mode 100644 index 000000000..95d0eb155 --- /dev/null +++ b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.scss @@ -0,0 +1,33 @@ +/** @format */ + +@use "styles/typography"; + +.collaborator { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 15px 10px; + background-color: var(--light-gray); + border-radius: var(--rounded-md); + + &__role { + color: var(--black); + } + + &__requirements { + color: var(--dark-grey); + } + + &__info { + display: flex; + flex-direction: column; + gap: 5px; + } + + &__skills { + display: flex; + flex-direction: column; + gap: 10px; + margin: 10px 0; + } +} diff --git a/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.spec.ts b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.spec.ts new file mode 100644 index 000000000..a14a0ba25 --- /dev/null +++ b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.spec.ts @@ -0,0 +1,26 @@ +/** @format */ + +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { InviteCardComponent } from "./collaborator-card.component"; + +describe("VacancyCardComponent", () => { + let component: InviteCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InviteCardComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InviteCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.ts b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.ts new file mode 100644 index 000000000..87d715b6c --- /dev/null +++ b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.ts @@ -0,0 +1,46 @@ +/** @format */ + +import { CommonModule } from "@angular/common"; +import { Component, Input, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms"; +import { ErrorMessage } from "@error/models/error-message"; +import { Collaborator } from "@office/models/collaborator.model"; + +/** + * Компонент карточки участника команды или проект + * + * Функциональность: + * - Отображает информацию о участнике (роль, специализация) + * + * Входные параметры: + * @Input invite - объект участника (обязательный) + */ +@Component({ + selector: "app-collaborator-card", + templateUrl: "./collaborator-card.component.html", + styleUrl: "./collaborator-card.component.scss", + standalone: true, + imports: [CommonModule, ReactiveFormsModule], +}) +export class CollaboratorCardComponent implements OnInit { + constructor(private readonly fb: FormBuilder) { + this.inviteForm = this.fb.group({ + role: [""], + specializations: this.fb.array([]), + }); + } + + inviteForm: FormGroup; + errorMessage = ErrorMessage; + + @Input({ required: true }) collaborator!: Collaborator; + + ngOnInit(): void { + if (this.collaborator) { + this.inviteForm.patchValue({ + role: this.collaborator.role, + specialization: this.collaborator.skills, + }); + } + } +} diff --git a/projects/social_platform/src/app/office/shared/invite-card/invite-card.component.ts b/projects/social_platform/src/app/office/shared/invite-card/invite-card.component.ts index 383c54f6f..da0c96cc4 100644 --- a/projects/social_platform/src/app/office/shared/invite-card/invite-card.component.ts +++ b/projects/social_platform/src/app/office/shared/invite-card/invite-card.component.ts @@ -20,7 +20,7 @@ import { rolesMembersList } from "projects/core/src/consts/list-roles-members"; * * Входные параметры: * @Input invite - объект приглашения (обязательный) - * @Input type - тип приглашения: "team" или "members" (по умолчанию "members") + * @Input type - тип приглашения: "team" или "invite" (по умолчанию "invite") * * Выходные события: * @Output remove - событие удаления приглашения, передает ID приглашения @@ -55,7 +55,6 @@ export class InviteCardComponent implements OnInit { errorMessage = ErrorMessage; @Input({ required: true }) invite!: Invite; - @Input() type: "team" | "members" = "members"; @Output() remove = new EventEmitter(); @Output() edit = new EventEmitter<{ inviteId: number; role: string; specialization: string }>(); diff --git a/projects/social_platform/src/app/office/shared/nav/nav.component.html b/projects/social_platform/src/app/office/shared/nav/nav.component.html index d795304a6..877993e27 100644 --- a/projects/social_platform/src/app/office/shared/nav/nav.component.html +++ b/projects/social_platform/src/app/office/shared/nav/nav.component.html @@ -83,7 +83,7 @@

    {{ title }}

    - Навыки + Траектории
    PRO
    From 11d4b0f41e8fb9067d84f4db178de6622a1b67f6 Mon Sep 17 00:00:00 2001 From: Awakich Date: Thu, 21 Aug 2025 14:16:37 +0300 Subject: [PATCH 07/22] fix approve skills in profile & add validators for password --- .../src/lib/services/validation.service.ts | 124 +++++++++++++++++- .../app/auth/models/password-errors.model.ts | 14 ++ .../app/auth/register/register.component.html | 32 ++++- .../app/auth/register/register.component.ts | 2 +- .../profile/detail/main/main.component.html | 17 ++- .../profile/detail/main/main.component.scss | 1 + .../profile/detail/main/main.component.ts | 33 ++++- .../detail/profile-detail.component.scss | 2 +- .../app/office/profile/edit/edit.component.ts | 3 +- .../collaborator-card.component.html | 8 +- 10 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 projects/social_platform/src/app/auth/models/password-errors.model.ts diff --git a/projects/core/src/lib/services/validation.service.ts b/projects/core/src/lib/services/validation.service.ts index bc42943d9..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 }; } diff --git a/projects/social_platform/src/app/auth/models/password-errors.model.ts b/projects/social_platform/src/app/auth/models/password-errors.model.ts new file mode 100644 index 000000000..686338022 --- /dev/null +++ b/projects/social_platform/src/app/auth/models/password-errors.model.ts @@ -0,0 +1,14 @@ +/** @format */ + +import { ValidationErrors } from "@angular/forms"; + +export interface PasswordValidationErrors extends ValidationErrors { + passwordTooShort?: { requiredLength: number; actualLength: number }; + passwordNoUppercase?: { message: string }; + passwordNoLowercase?: { message: string }; + passwordNoNumber?: { message: string }; + passwordNoSpecialChar?: { message: string }; + passwordHasSpaces?: { message: string }; + passwordHasSequence?: { message: string }; + passwordHasRepeating?: { message: string }; +} diff --git a/projects/social_platform/src/app/auth/register/register.component.html b/projects/social_platform/src/app/auth/register/register.component.html index 2c7071e5e..764eebca0 100644 --- a/projects/social_platform/src/app/auth/register/register.component.html +++ b/projects/social_platform/src/app/auth/register/register.component.html @@ -124,12 +124,34 @@

    {{ errorMessage.VALIDATION_REQUIRED }}
    - } @if (password | controlError: "minlength") { + } @if (password | controlError: "passwordTooShort") {
    - @if (password.errors) { - {{ errorMessage.VALIDATION_TOO_SHORT }} - {{ password.errors["minlength"]["requiredLength"] }} - } + @if (password.errors) { Пароль должен содержать минимум + {{ password.errors["passwordTooShort"]["requiredLength"] }} символов } +
    + } @if (password | controlError: "passwordNoUppercase") { +
    + Пароль должен содержать минимум одну заглавную букву (A-Z) +
    + } @if (password | controlError: "passwordNoLowercase") { +
    + Пароль должен содержать минимум одну строчную букву (a-z) +
    + } @if (password | controlError: "passwordNoNumber") { +
    Пароль должен содержать минимум одну цифру (0-9)
    + } @if (password | controlError: "passwordNoSpecialChar") { +
    + Пароль должен содержать минимум один специальный символ +
    + } @if (password | controlError: "passwordHasSpaces") { +
    Пароль не должен содержать пробелы
    + } @if (password | controlError: "passwordHasSequence") { +
    + Пароль не должен содержать последовательности символов (123456, abcdef и т.д.) +
    + } @if (password | controlError: "passwordHasRepeating") { +
    + Пароль не должен содержать более 2 одинаковых символов подряд
    } @if (password | controlError: "unMatch") {
    diff --git a/projects/social_platform/src/app/auth/register/register.component.ts b/projects/social_platform/src/app/auth/register/register.component.ts index 7e426b1b6..ac8e3f88b 100644 --- a/projects/social_platform/src/app/auth/register/register.component.ts +++ b/projects/social_platform/src/app/auth/register/register.component.ts @@ -73,7 +73,7 @@ export class RegisterComponent implements OnInit { "", [Validators.required, Validators.email, this.validationService.useEmailValidator()], ], - password: ["", [Validators.required, Validators.minLength(6)]], + password: ["", [Validators.required, this.validationService.usePasswordValidator(6)]], repeatedPassword: ["", [Validators.required]], }, { validators: [this.validationService.useMatchValidator("password", "repeatedPassword")] } diff --git a/projects/social_platform/src/app/office/profile/detail/main/main.component.html b/projects/social_platform/src/app/office/profile/detail/main/main.component.html index 1778d044c..98298f5c5 100644 --- a/projects/social_platform/src/app/office/profile/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/profile/detail/main/main.component.html @@ -85,12 +85,12 @@

    Обо мне

    - + {{ skill.name }} @@ -451,4 +451,15 @@

    Контакты

    } + + + + } diff --git a/projects/social_platform/src/app/office/profile/detail/main/main.component.scss b/projects/social_platform/src/app/office/profile/detail/main/main.component.scss index a7b00c771..242a3463f 100644 --- a/projects/social_platform/src/app/office/profile/detail/main/main.component.scss +++ b/projects/social_platform/src/app/office/profile/detail/main/main.component.scss @@ -518,6 +518,7 @@ $section-padding: 24px; gap: 80px; align-items: center; justify-content: space-between; + width: 100%; } &__left { diff --git a/projects/social_platform/src/app/office/profile/detail/main/main.component.ts b/projects/social_platform/src/app/office/profile/detail/main/main.component.ts index 757cea152..6a794bc42 100644 --- a/projects/social_platform/src/app/office/profile/detail/main/main.component.ts +++ b/projects/social_platform/src/app/office/profile/detail/main/main.component.ts @@ -29,6 +29,7 @@ import { ProfileService } from "@auth/services/profile.service"; import { ModalComponent } from "@ui/components/modal/modal.component"; import { AvatarComponent } from "../../../../ui/components/avatar/avatar.component"; import { Skill } from "@office/models/skill"; +import { HttpErrorResponse } from "@angular/common/http"; /** * Главный компонент страницы профиля пользователя @@ -148,6 +149,8 @@ export class ProfileMainComponent implements OnInit, AfterViewInit, OnDestroy { readAllWorkExperience = false; readAllModal = false; + approveOwnSkillModal = false; + @ViewChild(NewsFormComponent) newsFormComponent?: NewsFormComponent; @ViewChild(NewsCardComponent) newsCardComponent?: NewsCardComponent; @@ -236,13 +239,17 @@ export class ProfileMainComponent implements OnInit, AfterViewInit, OnDestroy { * @param event - событие клика для предотвращения всплытия * @param skill - объект навыка для обновления */ - onToggleApprove(skillId: number, event: Event, skill: Skill) { + onToggleApprove(skillId: number, event: Event, skill: Skill, profileId: number) { event.stopPropagation(); const userId = this.route.snapshot.params["id"]; - if (skill.approves.length > 0) { + const isApprovedByCurrentUser = skill.approves.some(approve => { + return approve.confirmedBy.id === profileId; + }); + + if (isApprovedByCurrentUser) { this.profileApproveSkillService.unApproveSkill(userId, skillId).subscribe(() => { - skill.approves = skill.approves.slice(0, -1); + skill.approves = skill.approves.filter(approve => approve.confirmedBy.id !== profileId); this.cdRef.markForCheck(); }); } else { @@ -260,13 +267,27 @@ export class ProfileMainComponent implements OnInit, AfterViewInit, OnDestroy { ) ) ) - .subscribe(updatedApprove => { - skill.approves = [...skill.approves, updatedApprove]; - this.cdRef.markForCheck(); + .subscribe({ + next: updatedApprove => { + skill.approves = [...skill.approves, updatedApprove]; + this.cdRef.markForCheck(); + }, + error: err => { + if (err instanceof HttpErrorResponse) { + if (err.status === 400) { + this.approveOwnSkillModal = true; + this.cdRef.markForCheck(); + } + } + }, }); } } + isUserApproveSkill(skill: Skill, profileId: number): boolean { + return skill.approves.some(approve => approve.confirmedBy.id === profileId); + } + openSkills: any = {}; /** diff --git a/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss b/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss index ae0ff3ecd..bcd773f6a 100644 --- a/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss +++ b/projects/social_platform/src/app/office/profile/detail/profile-detail.component.scss @@ -31,7 +31,7 @@ $detail-bar-mb: 12px; display: flex; flex-direction: column; flex-grow: 1; - gap: 20px; + gap: 10px; max-height: calc(100% - #{$detail-bar-height} - #{$detail-bar-mb}); padding-bottom: 12px; } diff --git a/projects/social_platform/src/app/office/profile/edit/edit.component.ts b/projects/social_platform/src/app/office/profile/edit/edit.component.ts index 70d804796..ee76d2ec3 100644 --- a/projects/social_platform/src/app/office/profile/edit/edit.component.ts +++ b/projects/social_platform/src/app/office/profile/edit/edit.component.ts @@ -107,8 +107,7 @@ export class ProfileEditComponent implements OnInit, OnDestroy, AfterViewInit { private readonly skillsService: SkillsService, private readonly router: Router, private readonly route: ActivatedRoute, - private readonly navService: NavService, - private readonly location: Location + private readonly navService: NavService ) { this.profileForm = this.fb.group({ firstName: ["", [Validators.required]], diff --git a/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html index 8b4fb548e..e3f6c1dd2 100644 --- a/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html +++ b/projects/social_platform/src/app/office/shared/collaborator-card/collaborator-card.component.html @@ -7,14 +7,14 @@

    {{ collaborator.firstName }} {{ collaborator.lastName }}

    -

    +

    @for (skill of collaborator.skills; track $index) { -
    +

    Категория - {{ skill.category.name }}

    Навык - {{ skill.name }}

    -
    +
    } -

    +

    Роль - {{ collaborator.role }}

    From cb17c291e5436302a479166023dd7e567a9d5bd0 Mon Sep 17 00:00:00 2001 From: Awakich Date: Fri, 22 Aug 2025 13:15:37 +0300 Subject: [PATCH 08/22] remove linking block for duplicated project --- .../office/projects/edit/services/project-form.service.ts | 4 +++- .../project-main-step/project-main-step.component.html | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts b/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts index 2f776ea91..5b8247711 100644 --- a/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts +++ b/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts @@ -109,9 +109,11 @@ export class ProjectFormService { problem: project.problem ?? "", presentationAddress: project.presentationAddress, coverImageAddress: project.coverImageAddress, - partnerProgramId: project.partnerProgramId ?? null, + partnerProgramId: project.partnerProgram?.programId ?? null, }); + console.log(project.partnerProgram?.programId); + if (project.partnerProgram) { this.relationId.set(project.partnerProgram?.programLinkId); } diff --git a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html index 882e391d9..2d768662d 100644 --- a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html +++ b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html @@ -182,8 +182,8 @@
    - @if (authService.profile | async; as profile) { @if (profile.id == leaderId) { @if - (programTagsOptions.length) { + @if (!partnerProgramId?.value) { @if (authService.profile | async; as profile) { @if (profile.id + == leaderId) { @if (programTagsOptions.length) {
    Привязать проект к программе - } } } @if (presentationAddress; as presentationAddress) { + } } } } @if (presentationAddress; as presentationAddress) {
    Date: Fri, 22 Aug 2025 19:29:19 +0300 Subject: [PATCH 09/22] fix loading of projects with search --- .../profile/detail/main/main.component.html | 19 +++---- .../profile/detail/main/main.component.scss | 22 ++++++++ .../office/projects/edit/edit.component.html | 1 - .../office/projects/edit/edit.component.ts | 42 ++++++--------- .../project-main-step.component.ts | 1 - .../office/projects/list/list.component.html | 4 +- .../office/projects/list/list.component.ts | 53 ++++++++----------- .../projects-filter.component.ts | 3 +- .../app/office/projects/projects.component.ts | 2 +- .../office/vacancies/list/list.component.ts | 6 +-- 10 files changed, 77 insertions(+), 76 deletions(-) diff --git a/projects/social_platform/src/app/office/profile/detail/main/main.component.html b/projects/social_platform/src/app/office/profile/detail/main/main.component.html index 98298f5c5..abadc1600 100644 --- a/projects/social_platform/src/app/office/profile/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/profile/detail/main/main.component.html @@ -257,9 +257,9 @@

    Образование

    - +

    {{ education.organizationName }} - +

    {{ education.description }}

    @@ -315,9 +315,9 @@

    Работа

    - +

    {{ workExperience.organizationName }} - +

    {{ workExperience.description }}

    @@ -340,7 +340,7 @@

    Языки

      - @for (p of user.userLanguages.slice(0, 3); track $index) { + @for (p of user.userLanguages.slice(0, 2); track $index) {
    • Языки

  • }
-
- @if (user.userLanguages) { +
+ @if (user.userLanguages.length > 2) {
    - @for (userLanguagesItem of user.userLanguages.slice(3); track $index) { + @for (userLanguagesItem of user.userLanguages.slice(2); track $index) {
  • Языки

{{ userLanguages.language }} {{ userLanguages.languageLevel }}

- @if (userLanguagesLength > 3) { + @if (userLanguagesLength > 2) {
{{ readAllLanguages ? "Скрыть" : "Читать полностью" }}
@@ -457,6 +457,7 @@

Контакты

diff --git a/projects/social_platform/src/app/office/profile/detail/main/main.component.scss b/projects/social_platform/src/app/office/profile/detail/main/main.component.scss index 242a3463f..6d73578e4 100644 --- a/projects/social_platform/src/app/office/profile/detail/main/main.component.scss +++ b/projects/social_platform/src/app/office/profile/detail/main/main.component.scss @@ -30,6 +30,28 @@ $section-padding: 24px; } } +.languages { + &__title { + margin-bottom: 12px; + color: var(--black); + } + + ul { + overflow: hidden; + + span { + overflow: hidden; + text-overflow: ellipsis; + } + } + + li:not(:last-child) { + margin-bottom: 12px; + } + + @include expandable-list; +} + .main { display: flex; flex-direction: column; diff --git a/projects/social_platform/src/app/office/projects/edit/edit.component.html b/projects/social_platform/src/app/office/projects/edit/edit.component.html index 8376a734a..668b76a78 100644 --- a/projects/social_platform/src/app/office/projects/edit/edit.component.html +++ b/projects/social_platform/src/app/office/projects/edit/edit.component.html @@ -23,7 +23,6 @@ [leaderId]="leaderId" [projSubmitInitiated]="projSubmitInitiated" (assignToProgram)="assignProjectToProgram()" - (saveProject)="onSaveProject($event)" > } @else if (editingStep === "contacts") { diff --git a/projects/social_platform/src/app/office/projects/edit/edit.component.ts b/projects/social_platform/src/app/office/projects/edit/edit.component.ts index 2c188c1a6..8c8d764b7 100644 --- a/projects/social_platform/src/app/office/projects/edit/edit.component.ts +++ b/projects/social_platform/src/app/office/projects/edit/edit.component.ts @@ -298,20 +298,6 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy { setProjFormIsSubmitting!: (status: boolean) => void; - /** - * Выполнение сохранения проекта - * Из дочернего компонента project-main-step через emit - * - * @param event тип проекта для публикации или для черновика - */ - onSaveProject(event: { type: "draft" | "published" }): void { - if (event.type === "draft") { - this.saveProjectAsDraft(); - } else { - this.saveProjectAsPublished(); - } - } - /** * Очистка всех ошибок валидации */ @@ -391,26 +377,30 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy { }); const payload = this.projectFormService.getFormValue(); + const projectId = Number(this.route.snapshot.paramMap.get("projectId")); + + if (this.vacancyForm.dirty) { + this.projectVacancyService.submitVacancy(projectId); + } if ( !this.validationService.getFormValidation(this.projectForm) || - !this.validationService.getFormValidation(this.additionalForm) + !this.validationService.getFormValidation(this.additionalForm) || + !this.validationService.getFormValidation(this.vacancyForm) ) { return; } this.setProjFormIsSubmitting(true); - this.projectService - .updateProject(Number(this.route.snapshot.paramMap.get("projectId")), payload) - .subscribe({ - next: () => { - this.setProjFormIsSubmitting(false); - this.router.navigateByUrl(`/office/projects/my`); - }, - error: () => { - this.setProjFormIsSubmitting(false); - }, - }); + this.projectService.updateProject(projectId, payload).subscribe({ + next: () => { + this.setProjFormIsSubmitting(false); + this.router.navigateByUrl(`/office/projects/my`); + }, + error: () => { + this.setProjFormIsSubmitting(false); + }, + }); } // Методы для работы с модальными окнами diff --git a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts index 764c3da95..00d92e12a 100644 --- a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts +++ b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts @@ -44,7 +44,6 @@ export class ProjectMainStepComponent implements OnInit, OnDestroy { @Input() projectId!: number; @Output() assignToProgram = new EventEmitter(); - @Output() saveProject = new EventEmitter<{ type: "draft" | "published" }>(); private subscription = new Subscription(); diff --git a/projects/social_platform/src/app/office/projects/list/list.component.html b/projects/social_platform/src/app/office/projects/list/list.component.html index b58e06bef..ee0f61b1e 100644 --- a/projects/social_platform/src/app/office/projects/list/list.component.html +++ b/projects/social_platform/src/app/office/projects/list/list.component.html @@ -8,7 +8,7 @@ } -
-
- Заработная плата -
- -
- -
+
+ Заработная плата +
+ + + +
+ + +
- +
diff --git a/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.ts b/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.ts index 571a447d4..d02211cbe 100644 --- a/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.ts +++ b/projects/social_platform/src/app/office/vacancies/shared/filter/vacancy-filter.component.ts @@ -16,21 +16,15 @@ import { ActivatedRoute, Router } from "@angular/router"; import { ButtonComponent, CheckboxComponent, IconComponent, InputComponent } from "@ui/components"; import { ClickOutsideModule } from "ng-click-outside"; import { FeedService } from "@office/feed/services/feed.service"; -import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms"; import { VacancyService } from "@office/services/vacancy.service"; -import { debounceTime, map, Subscription, tap } from "rxjs"; +import { debounceTime, map, Subject, Subscription, tap } from "rxjs"; import { filterExperience } from "projects/core/src/consts/filter-experience"; import { filterWorkFormat } from "projects/core/src/consts/filter-work-format"; import { filterWorkSchedule } from "projects/core/src/consts/filter-work-schedule"; /** - * Компонент фильтра вакансий - * Предоставляет интерфейс для фильтрации вакансий по различным критериям: - * - Опыт работы - * - Формат работы (удаленно/офис/гибрид) - * - График работы - * - Диапазон зарплаты - * Поддерживает как десктопную, так и мобильную версии интерфейса + * Компонент фильтра вакансий без использования реактивных форм + * Использует сигналы для управления состоянием полей зарплаты */ @Component({ selector: "app-vacancy-filter", @@ -42,7 +36,6 @@ import { filterWorkSchedule } from "projects/core/src/consts/filter-work-schedul ClickOutsideModule, IconComponent, InputComponent, - ReactiveFormsModule, ], templateUrl: "./vacancy-filter.component.html", styleUrl: "./vacancy-filter.component.scss", @@ -67,17 +60,7 @@ export class VacancyFilterComponent implements OnInit { /** Сервис для работы с вакансиями */ vacancyService = inject(VacancyService); - /** - * Конструктор компонента - * @param fb - FormBuilder для создания формы зарплатного диапазона - */ - constructor(private readonly fb: FormBuilder) { - // Создание формы для фильтрации по зарплате - this.salaryForm = this.fb.group({ - salaryMin: [""], // Минимальная зарплата - salaryMax: [""], // Максимальная зарплата - }); - } + constructor() {} /** Приватное поле для хранения значения поиска */ private _searchValue: string | undefined; @@ -101,33 +84,12 @@ export class VacancyFilterComponent implements OnInit { /** Событие изменения значения поиска */ @Output() searchValueChange = new EventEmitter(); - /** - * Инициализация компонента - * Подписывается на изменения параметров запроса для синхронизации фильтров - */ - ngOnInit() { - this.salaryForm.valueChanges.pipe(debounceTime(300)).subscribe(vacancyFormValues => { - console.log(vacancyFormValues.salary_max); - }); - - // Подписка на изменения параметров запроса - this.queries$ = this.route.queryParams.subscribe(queries => { - // Синхронизация текущих значений фильтров с URL - this.currentExperience.set(queries["required_experience"]); - this.currentWorkFormat.set(queries["work_format"]); - this.currentWorkSchedule.set(queries["work_schedule"]); - this.currentSalaryMin.set(queries["salary_min"]); - this.currentSalaryMax.set(queries["salary_max"]); - this.searchValue = queries["role_contains"]; - }); - } - /** Подписка на параметры запроса */ queries$?: Subscription; + /** Состояние открытия фильтра (для мобильной версии) */ filterOpen = signal(false); - /** Форма для фильтрации по зарплате */ - salaryForm: FormGroup; + /** Общее количество элементов */ totalItemsCount = signal(0); @@ -143,6 +105,15 @@ export class VacancyFilterComponent implements OnInit { /** Текущая максимальная зарплата */ currentSalaryMax = signal(undefined); + // Сигналы для значений полей зарплаты + /** Значение поля минимальной зарплаты */ + salaryMinValue = signal(""); + /** Значение поля максимальной зарплаты */ + salaryMaxValue = signal(""); + + // Subject для debounce изменений зарплаты + private salaryChanges$ = new Subject<{ min: string; max: string }>(); + /** Опции фильтра по опыту работы */ readonly filterExperienceOptions = filterExperience; @@ -152,6 +123,61 @@ export class VacancyFilterComponent implements OnInit { /** Опции фильтра по графику работы */ filterWorkScheduleOptions = filterWorkSchedule; + /** + * Инициализация компонента + */ + ngOnInit() { + // Подписка на изменения зарплаты с debounce + this.salaryChanges$.pipe(debounceTime(300)).subscribe(({ min, max }) => { + this.router.navigate([], { + queryParams: { + role_contains: this.searchValue || null, + salary_min: min || null, + salary_max: max || null, + }, + queryParamsHandling: "merge", + relativeTo: this.route, + }); + }); + + // Подписка на изменения параметров запроса + this.queries$ = this.route.queryParams.subscribe(queries => { + // Синхронизация текущих значений фильтров с URL + this.currentExperience.set(queries["required_experience"]); + this.currentWorkFormat.set(queries["work_format"]); + this.currentWorkSchedule.set(queries["work_schedule"]); + this.currentSalaryMin.set(queries["salary_min"]); + this.currentSalaryMax.set(queries["salary_max"]); + this.searchValue = queries["role_contains"]; + + // Синхронизация полей зарплаты + this.salaryMinValue.set(queries["salary_min"] || ""); + this.salaryMaxValue.set(queries["salary_max"] || ""); + }); + } + + /** + * Обработчик изменения минимальной зарплаты + */ + onSalaryMinChange(value: string): void { + this.salaryMinValue.set(value); + this.salaryChanges$.next({ + min: value, + max: this.salaryMaxValue(), + }); + } + + /** + * Обработчик изменения максимальной зарплаты + */ + onSalaryMaxChange(value: string): void { + this.salaryMaxValue.set(value); + this.salaryChanges$.next({ + min: this.salaryMinValue(), + max: value, + }); + } + /** * Установка фильтра по опыту работы * @param event - событие клика @@ -214,25 +240,6 @@ export class VacancyFilterComponent implements OnInit { .then(() => console.debug("Query change from ProjectsComponent")); } - /** - * Применение фильтров - * Обновляет URL с текущими значениями всех фильтров - */ - applyFilter() { - const salaryMin = this.salaryForm.get("salaryMin")?.value || ""; - const salaryMax = this.salaryForm.get("salaryMax")?.value || ""; - - this.router.navigate([], { - queryParams: { - role_contains: this.searchValue || null, - salary_min: salaryMin, - salary_max: salaryMax, - }, - queryParamsHandling: "merge", - relativeTo: this.route, - }); - } - /** * Сброс всех фильтров * Очищает все параметры фильтрации и обновляет URL @@ -241,8 +248,14 @@ export class VacancyFilterComponent implements OnInit { this.currentExperience.set(undefined); this.currentWorkFormat.set(undefined); this.currentWorkSchedule.set(undefined); + this.currentSalaryMax.set(undefined); + this.currentSalaryMin.set(undefined); + + // Сбрасываем значения полей + this.salaryMinValue.set(""); + this.salaryMaxValue.set(""); + this.onSearchValueChanged(""); - this.salaryForm.reset(); this.router .navigate([], { @@ -303,4 +316,9 @@ export class VacancyFilterComponent implements OnInit { map(res => res) ); } + + ngOnDestroy() { + this.queries$?.unsubscribe(); + this.salaryChanges$.complete(); + } } From 9723abd151c373b11b8a23a57af5bd1a52ed45b9 Mon Sep 17 00:00:00 2001 From: Awakich Date: Wed, 27 Aug 2025 10:16:50 +0300 Subject: [PATCH 12/22] change naming of modal to approve skill --- .../src/app/office/profile/detail/main/main.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/social_platform/src/app/office/profile/detail/main/main.component.html b/projects/social_platform/src/app/office/profile/detail/main/main.component.html index abadc1600..f5a9dd5ff 100644 --- a/projects/social_platform/src/app/office/profile/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/profile/detail/main/main.component.html @@ -26,7 +26,7 @@

Обо мне

+ + +
+
+ +

Редактирование изображения перед отправкой!

+
+ + @if (showCropperModalErrorMessage) { +

+ {{ showCropperModalErrorMessage }} +

+ } + +
+ +
+ +
+
Предварительный просмотр:
+ @if (croppedImage) { + Preview + } +
+ +
+ Отменить + Сохранить +
+
+
diff --git a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss index 00cb2eb6f..f857ce42f 100644 --- a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss +++ b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss @@ -1,3 +1,5 @@ +@use "styles/responsive"; + .control { border-radius: 50%; @@ -69,3 +71,142 @@ .placeholder { background-color: var(--gray); } + +.cancel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 80%; + max-height: calc(100vh - 40px); + padding: 40px 0 80px; + overflow-y: auto; + + @include responsive.apply-desktop { + width: 50%; + } + + &__cross { + position: absolute; + top: 0; + right: 0; + width: 32px; + height: 32px; + cursor: pointer; + + @include responsive.apply-desktop { + top: 8px; + right: 8px; + } + } + + &__cropper-container { + width: 100%; + max-width: 500px; + margin: 20px 0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + background: var(--background-secondary, #f8f9fa); + + ::ng-deep { + .ngx-ic-main { + border-radius: 8px; + background: white; + } + + .ngx-ic-overlay { + border-radius: 8px; + } + + .ngx-ic-crop { + border-radius: 50%; + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); + } + + .ngx-ic-move, .ngx-ic-resize { + background: var(--accent, #007bff); + border: 2px solid white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } + + .ngx-ic-crop::before { + border-color: rgba(255, 255, 255, 0.5); + } + } + } + + &__preview { + margin: 20px 0; + text-align: center; + padding: 20px; + background: var(--background-tertiary, #f0f0f0); + border-radius: 12px; + width: 100%; + max-width: 300px; + + &-label { + font-size: 14px; + font-weight: 500; + color: var(--text-secondary, #666); + margin-bottom: 15px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &-img { + border: 3px solid var(--border-color, #ddd); + object-fit: cover; + transition: transform 0.2s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + + &:hover { + transform: scale(1.05); + } + + &--round { + border-radius: 50%; + } + } + } + + &__top { + display: flex; + flex-direction: column; + margin-bottom: 10px; + } + + &__title { + text-align: center; + } + + &__text { + text-align: center; + } + + &__buttons { + display: flex; + gap: 10px; + align-items: center; + margin-top: 20px; + + ::ng-deep { + app-button { + .button { + padding-right: 52px; + padding-left: 52px; + } + } + } + } + + &__button { + margin-top: 20px; + } + + &__preview-img { + animation: fadeInScale 0.3s ease-out; + } + + +} diff --git a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.ts b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.ts index 474ad3716..b2816f185 100644 --- a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.ts +++ b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.ts @@ -5,14 +5,17 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { nanoid } from "nanoid"; import { FileService } from "@core/services/file.service"; import { catchError, concatMap, map, of } from "rxjs"; -import { IconComponent } from "@ui/components"; +import { IconComponent, ButtonComponent } from "@ui/components"; import { LoaderComponent } from "../loader/loader.component"; import { CommonModule } from "@angular/common"; +import { ImageCroppedEvent, ImageCropperComponent } from "ngx-image-cropper"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { ModalComponent } from "../modal/modal.component"; /** * Компонент для управления аватаром пользователя. * Реализует ControlValueAccessor для интеграции с Angular Forms. - * Позволяет загружать, обновлять и удалять изображение аватара. + * Позволяет загружать, обрезать, обновлять и удалять изображение аватара. * * Входящие параметры: * - size: размер аватара в пикселях (по умолчанию 140) @@ -34,12 +37,19 @@ import { CommonModule } from "@angular/common"; }, ], standalone: true, - imports: [LoaderComponent, IconComponent, CommonModule], + imports: [ + LoaderComponent, + IconComponent, + CommonModule, + ImageCropperComponent, + ModalComponent, + ButtonComponent, + ], }) export class AvatarControlComponent implements OnInit, ControlValueAccessor { - constructor(private fileService: FileService) {} + constructor(private fileService: FileService, private sanitizer: DomSanitizer) {} - /** Размер авата��а в пикселях */ + /** Размер аватара в пикселях */ @Input() size = 140; /** Состояние ошибки */ @@ -56,6 +66,21 @@ export class AvatarControlComponent implements OnInit, ControlValueAccessor { /** Текущее значение URL изображения */ value = ""; + /** Показывать ли модальное окно кроппера */ + showCropperModal = false; + + /** Текст ошибки при обрезки фотографии */ + showCropperModalErrorMessage = ""; + + /** Исходное изображение для обрезки */ + imageChangedEvent: Event | null = null; + + /** Обрезанное изображение */ + croppedImage: SafeUrl = ""; + + /** Blob обрезанного изображения для загрузки */ + croppedBlob: Blob | null = null; + /** Записывает значение URL изображения */ writeValue(address: string) { this.value = address; @@ -77,17 +102,63 @@ export class AvatarControlComponent implements OnInit, ControlValueAccessor { loading = false; /** - * Обработчик обновления изображения - * Загружает новый файл, при наличии старого - сначала удаляет его + * Обработчик выбора файла - открывает кроппер */ - onUpdate(event: Event) { + onFileSelected(event: Event) { const files = (event.currentTarget as HTMLInputElement).files; if (!files?.length) { return; } + this.imageChangedEvent = event; + this.showCropperModal = true; + } + + /** + * Обработчик обрезки изображения + */ + imageCropped(event: ImageCroppedEvent) { + if (event.objectUrl) { + this.croppedImage = this.sanitizer.bypassSecurityTrustUrl(event.objectUrl); + } + this.croppedBlob = event.blob || null; + } + + /** + * Обработчик загружено фото или нет + */ + imageLoaded() {} + + /** + * Обработчик готовности обрезки фотографии + */ + cropperReady() {} + + /** + * Обработчик ошибки загрузки + */ + loadImageFailed() { + console.error("Не удалось загрузить изображение"); + this.showCropperModalErrorMessage = "Не удалось загрузить изображение. Попробуйте ещё раз!"; + } + + /** + * Сохранить обрезанное изображение + */ + saveCroppedImage() { + if (!this.croppedBlob) { + return; + } + this.loading = true; + this.showCropperModal = false; + + // Создаем файл из blob + const file = new File([this.croppedBlob], "cropped-avatar.jpg", { + type: "image/jpeg", + lastModified: Date.now(), + }); const source = this.value ? this.fileService.deleteFile(this.value).pipe( @@ -95,14 +166,30 @@ export class AvatarControlComponent implements OnInit, ControlValueAccessor { console.error(err); return of({}); }), - concatMap(() => this.fileService.uploadFile(files[0])), + concatMap(() => this.fileService.uploadFile(file)), map(r => r["url"]) ) - : this.fileService.uploadFile(files[0]).pipe(map(r => r.url)); + : this.fileService.uploadFile(file).pipe(map(r => r.url)); source.subscribe(this.updateValue.bind(this)); } + /** + * Закрыть кроппер без сохранения + */ + closeCropper() { + this.showCropperModal = false; + this.imageChangedEvent = null; + this.croppedImage = ""; + this.croppedBlob = null; + + // Сбрасываем значение input + const input = document.getElementById(this.controlId) as HTMLInputElement; + if (input) { + input.value = ""; + } + } + /** * Обновляет значение URL и уведомляет о изменении * @param url - новый URL изображения From 66c34936fda4b6c8af3baf89b76f342ff73e7543 Mon Sep 17 00:00:00 2001 From: Awakich Date: Thu, 28 Aug 2025 12:48:37 +0300 Subject: [PATCH 14/22] add styles to cropper --- .../avatar-control.component.scss | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss index f857ce42f..0ce44e16e 100644 --- a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss +++ b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.scss @@ -104,15 +104,15 @@ width: 100%; max-width: 500px; margin: 20px 0; - border-radius: 8px; overflow: hidden; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); background: var(--background-secondary, #f8f9fa); + border-radius: 8px; + box-shadow: 0 4px 12px rgb(0 0 0 / 10%); ::ng-deep { .ngx-ic-main { - border-radius: 8px; background: white; + border-radius: 8px; } .ngx-ic-overlay { @@ -121,44 +121,46 @@ .ngx-ic-crop { border-radius: 50%; - box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); + box-shadow: 0 0 0 9999px rgb(0 0 0 / 50%); } - .ngx-ic-move, .ngx-ic-resize { + .ngx-ic-move, + .ngx-ic-resize { background: var(--accent, #007bff); border: 2px solid white; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); } .ngx-ic-crop::before { - border-color: rgba(255, 255, 255, 0.5); + border-color: rgb(255 255 255 / 50%); } } } &__preview { + width: 100%; + max-width: 300px; + padding: 20px; margin: 20px 0; text-align: center; - padding: 20px; background: var(--background-tertiary, #f0f0f0); border-radius: 12px; - width: 100%; - max-width: 300px; &-label { + margin-bottom: 15px; font-size: 14px; font-weight: 500; color: var(--text-secondary, #666); - margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px; } &-img { border: 3px solid var(--border-color, #ddd); - object-fit: cover; + box-shadow: 0 4px 12px rgb(0 0 0 / 10%); transition: transform 0.2s ease; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + object-fit: cover; + animation: fadeInScale 0.3s ease-out; &:hover { transform: scale(1.05); @@ -203,10 +205,4 @@ &__button { margin-top: 20px; } - - &__preview-img { - animation: fadeInScale 0.3s ease-out; - } - - } From fecb23d6de9de906c7d8c37bf15eebac7691f120 Mon Sep 17 00:00:00 2001 From: Awakich Date: Fri, 29 Aug 2025 11:50:27 +0300 Subject: [PATCH 15/22] fix link to register --- .../src/app/office/program/detail/main/main.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.html b/projects/social_platform/src/app/office/program/detail/main/main.component.html index 33f9bc140..fce1ddde4 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.html @@ -49,7 +49,11 @@

{{ program.name }}

@if (!program.isUserMember && !registerDateExpired) { - + + + Зарегистрироваться } From d9485c0628d563738611e8183b5dc0217d9faf82 Mon Sep 17 00:00:00 2001 From: Awakich Date: Fri, 29 Aug 2025 11:51:24 +0300 Subject: [PATCH 16/22] add checking to name of program --- .../office/program/detail/main/main.component.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.html b/projects/social_platform/src/app/office/program/detail/main/main.component.html index fce1ddde4..1a7e36d1b 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.html @@ -48,15 +48,16 @@

{{ program.name }}

- @if (!program.isUserMember && !registerDateExpired) { - - + @if (!program.isUserMember && !registerDateExpired) { @if + (program.name.includes("кейс-чемпионат по предпринимательству MIR")) { Зарегистрироваться - } + } @else { + + Зарегистрироваться + + } } From 729e9c8b2dd8bd59931973e71fefc17fc76849d0 Mon Sep 17 00:00:00 2001 From: Awakich Date: Mon, 1 Sep 2025 09:19:00 +0300 Subject: [PATCH 17/22] add check for another program --- .../src/app/office/program/detail/main/main.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.html b/projects/social_platform/src/app/office/program/detail/main/main.component.html index 1a7e36d1b..df66c9a50 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.html @@ -49,7 +49,8 @@

{{ program.name }}

@if (!program.isUserMember && !registerDateExpired) { @if - (program.name.includes("кейс-чемпионат по предпринимательству MIR")) { + (program.name.includes("кейс-чемпионат по предпринимательству MIR") || + program.name.includes("Кейс-чемпионат MIR")) { Зарегистрироваться From 6a998dd1b4602caac8840329894bb2478f9a39ee Mon Sep 17 00:00:00 2001 From: Awakich Date: Mon, 1 Sep 2025 09:45:31 +0300 Subject: [PATCH 18/22] fix includes check --- .../src/app/office/program/detail/main/main.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.html b/projects/social_platform/src/app/office/program/detail/main/main.component.html index df66c9a50..534016de3 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.html @@ -49,8 +49,7 @@

{{ program.name }}

@if (!program.isUserMember && !registerDateExpired) { @if - (program.name.includes("кейс-чемпионат по предпринимательству MIR") || - program.name.includes("Кейс-чемпионат MIR")) { + (program.name.includes("Кейс-чемпионат MIR")) { Зарегистрироваться From 62661a3adbe8753768db53691db00ad75291131b Mon Sep 17 00:00:00 2001 From: Awakich Date: Mon, 1 Sep 2025 14:06:37 +0300 Subject: [PATCH 19/22] fix cropper & add modal to permission to projects in program --- .../app/auth/register/register.component.ts | 4 +- .../set-password/set-password.component.html | 99 +++++++++++++++---- .../set-password/set-password.component.ts | 4 +- .../program/detail/main/main.component.html | 19 ++++ .../program/detail/main/main.component.scss | 49 +++++++++ .../program/detail/main/main.component.ts | 27 ++++- .../detail/projects/projects.component.scss | 1 + .../detail/projects/projects.component.ts | 18 ++-- .../detail/projects/projects.resolver.ts | 31 ++++-- .../program/services/program.service.ts | 4 +- .../autocomplete-input.component.scss | 2 +- .../avatar-control.component.html | 14 --- .../avatar-control.component.scss | 36 ------- 13 files changed, 219 insertions(+), 89 deletions(-) diff --git a/projects/social_platform/src/app/auth/register/register.component.ts b/projects/social_platform/src/app/auth/register/register.component.ts index ac8e3f88b..1aff07cc9 100644 --- a/projects/social_platform/src/app/auth/register/register.component.ts +++ b/projects/social_platform/src/app/auth/register/register.component.ts @@ -73,10 +73,10 @@ export class RegisterComponent implements OnInit { "", [Validators.required, Validators.email, this.validationService.useEmailValidator()], ], - password: ["", [Validators.required, this.validationService.usePasswordValidator(6)]], + password: ["", [Validators.required, this.validationService.usePasswordValidator(8)]], repeatedPassword: ["", [Validators.required]], }, - { validators: [this.validationService.useMatchValidator("password", "repeatedPassword")] } + { validators: [validationService.useMatchValidator("password", "repeatedPassword")] } ); } diff --git a/projects/social_platform/src/app/auth/set-password/set-password.component.html b/projects/social_platform/src/app/auth/set-password/set-password.component.html index c6bd2b0e1..f75e4599d 100644 --- a/projects/social_platform/src/app/auth/set-password/set-password.component.html +++ b/projects/social_platform/src/app/auth/set-password/set-password.component.html @@ -38,17 +38,47 @@

Новый пароль

> } - @if (password | controlError: "required") { -
- {{ errorMessage.VALIDATION_REQUIRED }} -
- } @if (password | controlError: "minlength") { -
- @if (password.errors) { - {{ errorMessage.VALIDATION_TOO_SHORT }} - {{ password.errors["minlength"]["requiredLength"] }} + @if (credsSubmitInitiated) { + + @if (password | controlError: "required") { +
+ {{ errorMessage.VALIDATION_REQUIRED }} +
+ } @if (password | controlError: "passwordTooShort") { +
+ @if (password.errors) { Пароль должен содержать минимум + {{ password.errors["passwordTooShort"]["requiredLength"] }} символов } +
+ } @if (password | controlError: "passwordNoUppercase") { +
+ Пароль должен содержать минимум одну заглавную букву (A-Z) +
+ } @if (password | controlError: "passwordNoLowercase") { +
+ Пароль должен содержать минимум одну строчную букву (a-z) +
+ } @if (password | controlError: "passwordNoNumber") { +
Пароль должен содержать минимум одну цифру (0-9)
+ } @if (password | controlError: "passwordNoSpecialChar") { +
+ Пароль должен содержать минимум один специальный символ +
+ } @if (password | controlError: "passwordHasSpaces") { +
Пароль не должен содержать пробелы
+ } @if (password | controlError: "passwordHasSequence") { +
+ Пароль не должен содержать последовательности символов (123456, abcdef и т.д.) +
+ } @if (password | controlError: "passwordHasRepeating") { +
+ Пароль не должен содержать более 2 одинаковых символов подряд +
+ } @if (password | controlError: "unMatch") { +
+ {{ errorMessage.VALIDATION_PASSWORD_UNMATCH }} +
} -
+ } } @if (passwordForm.get("passwordRepeated"); as passwordRepeated) { @@ -61,14 +91,47 @@

Новый пароль

formControlName="passwordRepeated" placeholder="Пароль" > - @if (passwordRepeated | controlError: "required") { -
- {{ errorMessage.VALIDATION_REQUIRED }} -
- } @if (passwordForm | controlError: "unMatch") { -
- {{ errorMessage.VALIDATION_PASSWORD_UNMATCH }} -
+ @if (credsSubmitInitiated) { + + @if (passwordRepeated | controlError: "required") { +
+ {{ errorMessage.VALIDATION_REQUIRED }} +
+ } @if (passwordRepeated | controlError: "passwordTooShort") { +
+ @if (passwordRepeated.errors) { Пароль должен содержать минимум + {{ passwordRepeated.errors["passwordTooShort"]["requiredLength"] }} символов } +
+ } @if (passwordRepeated | controlError: "passwordNoUppercase") { +
+ Пароль должен содержать минимум одну заглавную букву (A-Z) +
+ } @if (passwordRepeated | controlError: "passwordNoLowercase") { +
+ Пароль должен содержать минимум одну строчную букву (a-z) +
+ } @if (passwordRepeated | controlError: "passwordNoNumber") { +
Пароль должен содержать минимум одну цифру (0-9)
+ } @if (passwordRepeated | controlError: "passwordNoSpecialChar") { +
+ Пароль должен содержать минимум один специальный символ +
+ } @if (passwordRepeated | controlError: "passwordHasSpaces") { +
Пароль не должен содержать пробелы
+ } @if (passwordRepeated | controlError: "passwordHasSequence") { +
+ Пароль не должен содержать последовательности символов (123456, abcdef и т.д.) +
+ } @if (passwordRepeated | controlError: "passwordHasRepeating") { +
+ Пароль не должен содержать более 2 одинаковых символов подряд +
+ } @if (passwordRepeated | controlError: "unMatch") { +
+ {{ errorMessage.VALIDATION_PASSWORD_UNMATCH }} +
+ } +
} } @if (errorRequest) { diff --git a/projects/social_platform/src/app/auth/set-password/set-password.component.ts b/projects/social_platform/src/app/auth/set-password/set-password.component.ts index fa9399438..315f86eb1 100644 --- a/projects/social_platform/src/app/auth/set-password/set-password.component.ts +++ b/projects/social_platform/src/app/auth/set-password/set-password.component.ts @@ -41,7 +41,7 @@ export class SetPasswordComponent implements OnInit { ) { this.passwordForm = this.fb.group( { - password: ["", [Validators.required, Validators.minLength(8)]], + password: ["", [Validators.required, this.validationService.usePasswordValidator(8)]], passwordRepeated: ["", [Validators.required]], }, { validators: [validationService.useMatchValidator("password", "passwordRepeated")] } @@ -52,6 +52,7 @@ export class SetPasswordComponent implements OnInit { isSubmitting = false; errorMessage = ErrorMessage; errorRequest = false; + credsSubmitInitiated = false; showPassword = false; @@ -68,6 +69,7 @@ export class SetPasswordComponent implements OnInit { } onSubmit() { + this.credsSubmitInitiated = true; const token = this.route.snapshot.queryParamMap.get("token"); if (!token || !this.validationService.getFormValidation(this.passwordForm)) return; diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.html b/projects/social_platform/src/app/office/program/detail/main/main.component.html index 534016de3..02571c376 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.html @@ -161,4 +161,23 @@ + + +
+
+ +

Ошибка в доступе к программе!

+
+ + @if (showProgramModalErrorMessage()) { +

+ {{ showProgramModalErrorMessage() }} +

+ } + + Хорошо +
+
} diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.scss b/projects/social_platform/src/app/office/program/detail/main/main.component.scss index 5ae07bd19..75996d394 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.scss +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.scss @@ -382,3 +382,52 @@ margin-top: 20px; } } + + +.cancel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 80%; + max-height: calc(100vh - 40px); + padding: 40px 0 80px; + overflow-y: auto; + + @include responsive.apply-desktop { + width: 50%; + } + + &__cross { + position: absolute; + top: 0; + right: 0; + width: 32px; + height: 32px; + cursor: pointer; + + @include responsive.apply-desktop { + top: 8px; + right: 8px; + } + } + + &__top { + display: flex; + flex-direction: column; + margin-bottom: 10px; + } + + &__title { + text-align: center; + } + + &__text { + text-align: center; + } + + &__button { + margin-top: 20px; + } + +} diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.ts b/projects/social_platform/src/app/office/program/detail/main/main.component.ts index 26326672b..871c60e1c 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.ts +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.ts @@ -10,7 +10,7 @@ import { ViewChild, } from "@angular/core"; import { ProgramService } from "@office/program/services/program.service"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { concatMap, fromEvent, map, noop, of, Subscription, tap, throttleTime } from "rxjs"; import { Program } from "@office/program/models/program.model"; import { ProgramNewsService } from "@office/program/services/program-news.service"; @@ -24,6 +24,7 @@ import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { ApiPagination } from "@models/api-pagination.model"; import { TagComponent } from "@ui/components/tag/tag.component"; import { NewsFormComponent } from "@office/shared/news-form/news-form.component"; +import { ModalComponent } from "@ui/components/modal/modal.component"; /** * Главный компонент детальной страницы программы @@ -85,6 +86,7 @@ import { NewsFormComponent } from "@office/shared/news-form/news-form.component" ParseBreaksPipe, ParseLinksPipe, NewsFormComponent, + ModalComponent, ], }) export class ProgramDetailMainComponent implements OnInit, OnDestroy { @@ -92,6 +94,7 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { private readonly programService: ProgramService, private readonly programNewsService: ProgramNewsService, private readonly route: ActivatedRoute, + private readonly router: Router, private readonly cdRef: ChangeDetectorRef ) {} @@ -100,6 +103,9 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { fetchLimit = signal(10); fetchPage = signal(0); + showProgramModal = signal(false); + showProgramModalErrorMessage = signal(null); + programId?: number; subscriptions$ = signal([]); @@ -115,6 +121,20 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { ) .subscribe(); + const routeModalSub$ = this.route.queryParams.subscribe(param => { + if (param["access"] === "accessDenied") { + this.showProgramModal.set(true); + this.showProgramModalErrorMessage.set("У вас не доступа к этой вкладке!"); + + this.router.navigate([], { + relativeTo: this.route, + queryParams: { access: null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } + }); + const program$ = this.route.data .pipe( map(r => r["data"]), @@ -139,6 +159,7 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { this.subscriptions$().push(program$); this.subscriptions$().push(programIdSubscription$); + this.subscriptions$().push(routeModalSub$); } ngAfterViewInit() { @@ -252,6 +273,10 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { this.readFullDescription = !isExpanded; } + closeModal(): void { + this.showProgramModal.set(false); + } + program?: Program; registerDateExpired!: boolean; descriptionExpandable!: boolean; diff --git a/projects/social_platform/src/app/office/program/detail/projects/projects.component.scss b/projects/social_platform/src/app/office/program/detail/projects/projects.component.scss index fef6abb23..97ad6ef38 100644 --- a/projects/social_platform/src/app/office/program/detail/projects/projects.component.scss +++ b/projects/social_platform/src/app/office/program/detail/projects/projects.component.scss @@ -37,6 +37,7 @@ grid-template-columns: repeat(2, 1fr); gap: 20px 40px; width: 70%; + height: 100%; } } diff --git a/projects/social_platform/src/app/office/program/detail/projects/projects.component.ts b/projects/social_platform/src/app/office/program/detail/projects/projects.component.ts index 98a04bedd..03a812d96 100644 --- a/projects/social_platform/src/app/office/program/detail/projects/projects.component.ts +++ b/projects/social_platform/src/app/office/program/detail/projects/projects.component.ts @@ -132,9 +132,11 @@ export class ProgramProjectsComponent implements OnInit, AfterViewInit, OnDestro tap(r => (this.projectsTotalCount = r["count"])), map(r => r["results"]) ) - .subscribe(projects => { - this.projects = projects; - this.searchedProjects = projects; + .subscribe({ + next: projects => { + this.projects = projects; + this.searchedProjects = projects; + }, }); const searchFormSearch$ = this.searchForm.get("search")?.valueChanges.subscribe(search => { @@ -171,21 +173,21 @@ export class ProgramProjectsComponent implements OnInit, AfterViewInit, OnDestro const hasFilters = reqQuery && reqQuery["filters"] && Object.keys(reqQuery["filters"]).length > 0; - const params = new HttpParams({ fromObject: { partner_program: programId } }); + const params = new HttpParams({ fromObject: { offset: 0, limit: 21 } }); if (hasFilters) { return this.programService.createProgramFilters(programId, reqQuery["filters"]).pipe( catchError(err => { console.error("createFilters failed, fallback to getAllProjects()", err); - return this.programService.getAllProjects(params); + return this.programService.getAllProjects(programId, params); }) ); } - return this.programService.getAllProjects(params).pipe( + return this.programService.getAllProjects(programId, params).pipe( catchError(err => { console.error("getAllProjects failed", err); - return this.programService.getAllProjects(params); + return this.programService.getAllProjects(programId, params); }) ); } @@ -298,7 +300,7 @@ export class ProgramProjectsComponent implements OnInit, AfterViewInit, OnDestro const limit = this.perPage; return this.programService - .getAllProjects(new HttpParams({ fromObject: { partner_program: programId, offset, limit } })) + .getAllProjects(programId, new HttpParams({ fromObject: { offset, limit } })) .pipe( tap(projects => { this.projectsTotalCount = projects.count; diff --git a/projects/social_platform/src/app/office/program/detail/projects/projects.resolver.ts b/projects/social_platform/src/app/office/program/detail/projects/projects.resolver.ts index 8724b79bb..c474a63ce 100644 --- a/projects/social_platform/src/app/office/program/detail/projects/projects.resolver.ts +++ b/projects/social_platform/src/app/office/program/detail/projects/projects.resolver.ts @@ -1,11 +1,12 @@ /** @format */ import { inject } from "@angular/core"; -import { ActivatedRouteSnapshot, ResolveFn } from "@angular/router"; +import { ActivatedRoute, ActivatedRouteSnapshot, ResolveFn, Router } from "@angular/router"; import { ProgramService } from "@office/program/services/program.service"; import { Project } from "@models/project.model"; import { ApiPagination } from "@models/api-pagination.model"; import { HttpParams } from "@angular/common/http"; +import { catchError, EMPTY } from "rxjs"; /** * Резолвер для предзагрузки проектов программы @@ -33,9 +34,27 @@ export const ProgramProjectsResolver: ResolveFn> = ( route: ActivatedRouteSnapshot ) => { const programService = inject(ProgramService); - return programService.getAllProjects( - new HttpParams({ - fromObject: { partner_program: route.parent?.params["programId"], offset: 0, limit: 21 }, - }) - ); + const programId = route.parent?.params["programId"]; + const router = inject(Router); + + return programService + .getAllProjects( + programId, + new HttpParams({ + fromObject: { offset: 0, limit: 21 }, + }) + ) + .pipe( + catchError(error => { + if (error.status === 403) { + router.navigate([], { + queryParams: { access: "accessDenied" }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } + + return EMPTY; + }) + ); }; diff --git a/projects/social_platform/src/app/office/program/services/program.service.ts b/projects/social_platform/src/app/office/program/services/program.service.ts index f372baa8f..397fcd89b 100644 --- a/projects/social_platform/src/app/office/program/services/program.service.ts +++ b/projects/social_platform/src/app/office/program/services/program.service.ts @@ -79,8 +79,8 @@ export class ProgramService { return this.apiService.post(`${this.PROGRAMS_URL}/${programId}/register/`, additionalData); } - getAllProjects(params?: HttpParams): Observable> { - return this.apiService.get(`${this.PROJECTS_URL}/`, params); + getAllProjects(programId: number, params?: HttpParams): Observable> { + return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/projects`, params); } getAllMembers(programId: number, skip: number, take: number): Observable> { diff --git a/projects/social_platform/src/app/ui/components/autocomplete-input/autocomplete-input.component.scss b/projects/social_platform/src/app/ui/components/autocomplete-input/autocomplete-input.component.scss index 470b7f64a..3d533c241 100644 --- a/projects/social_platform/src/app/ui/components/autocomplete-input/autocomplete-input.component.scss +++ b/projects/social_platform/src/app/ui/components/autocomplete-input/autocomplete-input.component.scss @@ -134,7 +134,7 @@ position: absolute; top: 50%; right: 10px; - z-index: 4; + z-index: 2; display: flex; align-items: center; transform: translateY(-50%); diff --git a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.html b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.html index 033d4f4e1..14bd2607e 100644 --- a/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.html +++ b/projects/social_platform/src/app/ui/components/avatar-control/avatar-control.component.html @@ -73,20 +73,6 @@ > -
-
Предварительный просмотр:
- @if (croppedImage) { - Preview - } -
-
Date: Mon, 1 Sep 2025 14:07:04 +0300 Subject: [PATCH 20/22] add modal styles --- .../src/app/office/program/detail/main/main.component.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.scss b/projects/social_platform/src/app/office/program/detail/main/main.component.scss index 75996d394..a71058748 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.scss +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.scss @@ -383,7 +383,6 @@ } } - .cancel { display: flex; flex-direction: column; @@ -429,5 +428,4 @@ &__button { margin-top: 20px; } - } From 44004b0045eb900fdcd4e5c165a7878714d45a8d Mon Sep 17 00:00:00 2001 From: Awakich Date: Mon, 1 Sep 2025 14:23:41 +0300 Subject: [PATCH 21/22] run format --- .../program/detail/main/main.component.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.ts b/projects/social_platform/src/app/office/program/detail/main/main.component.ts index b1df3a7bd..7512896ec 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.ts +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.ts @@ -1,5 +1,4 @@ /** @format */ - import { ChangeDetectorRef, Component, @@ -97,7 +96,6 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { private readonly projectService: ProjectService, private readonly router: Router, private readonly route: ActivatedRoute, - private readonly router: Router, private readonly cdRef: ChangeDetectorRef ) {} @@ -112,7 +110,6 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { programId?: number; subscriptions$ = signal([]); - ngOnInit(): void { const programIdSubscription$ = this.route.params .pipe( @@ -168,9 +165,7 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { ngAfterViewInit() { const descElement = this.descEl?.nativeElement; this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight; - this.cdRef.detectChanges(); - const target = document.querySelector(".office__body"); if (target) { const scrollEvents$ = fromEvent(target, "scroll") @@ -179,7 +174,6 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { throttleTime(2000) ) .subscribe(); - this.subscriptions$().push(scrollEvents$); } } @@ -204,19 +198,14 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { const target = document.querySelector(".office__body"); if (!target) return of({}); - const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight; - if (scrollBottom > 0) return of({}); - this.fetchPage.update(p => p + 1); - return this.fetchNews(this.fetchPage() * this.fetchLimit(), this.fetchLimit()); } fetchNews(offset: number, limit: number) { const programId = this.route.snapshot.params["programId"]; - return this.programNewsService.fetchNews(limit, offset, programId).pipe( tap(({ count, results }) => { this.totalNewsCount.set(count); @@ -227,14 +216,12 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { @ViewChild(NewsFormComponent) newsFormComponent?: NewsFormComponent; @ViewChild("descEl") descEl?: ElementRef; - onNewsInVew(entries: IntersectionObserverEntry[]): void { const ids = entries.map(e => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return e.target.dataset.id; }); - this.programNewsService.readNews(this.route.snapshot.params["programId"], ids).subscribe(noop); } @@ -250,7 +237,6 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { onDelete(newsId: number) { const item = this.news().find((n: any) => n.id === newsId); if (!item) return; - this.programNewsService.deleteNews(this.route.snapshot.params["programId"], newsId).subscribe({ next: () => { const index = this.news().findIndex(news => news.id === newsId); @@ -262,7 +248,6 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { onLike(newsId: number) { const item = this.news().find((n: any) => n.id === newsId); if (!item) return; - this.programNewsService .toggleLike(this.route.snapshot.params["programId"], newsId, !item.isUserLiked) .subscribe(() => { @@ -276,7 +261,6 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { this.readFullDescription = !isExpanded; } - closeModal(): void { this.showProgramModal.set(false); } @@ -287,7 +271,6 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { ...this.projectService.projectsCount.getValue(), my: this.projectService.projectsCount.getValue().my + 1, }); - this.router .navigateByUrl(`/office/projects/${project.id}/edit?editingStep=main`) .then(() => console.debug("Route change from ProjectsComponent")); From de6e32a14f3ff0bc3a91f3ada13fe8f4da9bc24e Mon Sep 17 00:00:00 2001 From: Awakich Date: Tue, 2 Sep 2025 09:54:46 +0300 Subject: [PATCH 22/22] add loading service to project & fix program loading error --- .../social_platform/src/app/app.component.ts | 24 ++-- .../program/detail/main/main.component.ts | 119 +++++++++--------- .../app/office/services/loading.service.ts | 27 ++++ 3 files changed, 104 insertions(+), 66 deletions(-) create mode 100644 projects/social_platform/src/app/office/services/loading.service.ts diff --git a/projects/social_platform/src/app/app.component.ts b/projects/social_platform/src/app/app.component.ts index be9cf814e..861f9e768 100644 --- a/projects/social_platform/src/app/app.component.ts +++ b/projects/social_platform/src/app/app.component.ts @@ -18,6 +18,7 @@ import { import { MatProgressBarModule } from "@angular/material/progress-bar"; import { AsyncPipe, NgIf } from "@angular/common"; import { TokenService } from "@corelib"; +import { LoadingService } from "@office/services/loading.service"; /** * Корневой компонент приложения @@ -36,7 +37,8 @@ export class AppComponent implements OnInit, OnDestroy { constructor( private authService: AuthService, private tokenService: TokenService, - private router: Router + private router: Router, + private loadingService: LoadingService ) {} ngOnInit(): void { @@ -45,17 +47,26 @@ export class AppComponent implements OnInit, OnDestroy { this.authService.getChangeableRoles(), ]).subscribe(noop); - this.showLoaderEvents = this.router.events.pipe( + const showLoaderEvents = this.router.events.pipe( filter(evt => evt instanceof ResolveStart), map(() => true) ); - this.hideLoaderEvents = this.router.events.pipe( + + const hideLoaderEvents = this.router.events.pipe( filter(evt => evt instanceof ResolveEnd), debounceTime(200), map(() => false) ); - this.isLoading$ = merge(this.hideLoaderEvents, this.showLoaderEvents); + this.routerLoadingSub$ = merge(hideLoaderEvents, showLoaderEvents).subscribe(isLoading => { + if (isLoading) { + this.loadingService.show(); + } else { + this.loadingService.hide(); + } + }); + + this.isLoading$ = this.loadingService.isLoading$; if (location.pathname === "/") { if (this.tokenService.getTokens() === null) { @@ -80,10 +91,12 @@ export class AppComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.rolesSub$?.unsubscribe(); + this.routerLoadingSub$?.unsubscribe(); this.appHeight$?.unsubscribe(); } rolesSub$?: Subscription; + routerLoadingSub$?: Subscription; private loadEvent?: Observable; private resizeEvent?: Observable; @@ -91,7 +104,4 @@ export class AppComponent implements OnInit, OnDestroy { private appHeight$?: Subscription; isLoading$?: Observable; - private showLoaderEvents?: Observable; - - private hideLoaderEvents?: Observable; } diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.ts b/projects/social_platform/src/app/office/program/detail/main/main.component.ts index 7512896ec..55bd31691 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.ts +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.ts @@ -10,7 +10,17 @@ import { } from "@angular/core"; import { ProgramService } from "@office/program/services/program.service"; import { ActivatedRoute, Router, RouterLink } from "@angular/router"; -import { concatMap, fromEvent, map, noop, of, Subscription, tap, throttleTime } from "rxjs"; +import { + concatMap, + fromEvent, + map, + noop, + Observable, + of, + Subscription, + tap, + throttleTime, +} from "rxjs"; import { Program } from "@office/program/models/program.model"; import { ProgramNewsService } from "@office/program/services/program-news.service"; import { FeedNews } from "@office/projects/models/project-news.model"; @@ -25,51 +35,10 @@ import { TagComponent } from "@ui/components/tag/tag.component"; import { NewsFormComponent } from "@office/shared/news-form/news-form.component"; import { ModalComponent } from "@ui/components/modal/modal.component"; import { ProjectService } from "@office/services/project.service"; +import { MatProgressBarModule } from "@angular/material/progress-bar"; +import { AsyncPipe } from "@angular/common"; +import { LoadingService } from "@office/services/loading.service"; -/** - * Главный компонент детальной страницы программы - * - * Отображает основную информацию о программе и новостную ленту: - * - Детальное описание программы с возможностью развернуть/свернуть - * - Информацию о датах и регистрации - * - Новостную ленту для участников программы - * - Форму добавления новостей - * - Взаимодействие с новостями (лайки, просмотры) - * - * Принимает: - * @param {ProgramService} programService - Сервис программ - * @param {ProgramNewsService} programNewsService - Сервис новостей программы - * @param {ActivatedRoute} route - Для получения данных программы - * @param {ChangeDetectorRef} cdRef - Для ручного обновления представления - * - * Состояние (signals): - * @property {Signal} news - Массив новостей программы - * @property {Signal} totalNewsCount - Общее количество новостей - * @property {Signal} fetchLimit - Лимит загрузки новостей (10) - * @property {Signal} fetchPage - Текущая страница новостей - * @property {Signal} subscriptions$ - Подписки для очистки - * - * Данные программы: - * @property {Program} program - Объект программы - * @property {boolean} registerDateExpired - Истек ли срок регистрации - * @property {boolean} descriptionExpandable - Можно ли развернуть описание - * @property {boolean} readFullDescription - Развернуто ли описание - * - * ViewChild: - * @ViewChild NewsFormComponent - Ссылка на компонент формы новостей - * @ViewChild descEl - Ссылка на элемент описания - * - * Методы: - * @method fetchNews(offset, limit) - Загружает новости с пагинацией - * @method onScroll() - Обработчик прокрутки для подгрузки новостей - * @method onNewsInVew(entries) - Отмечает новости как просмотренные - * @method onAddNews(news) - Добавляет новую новость - * @method onLike(newsId) - Переключает лайк новости - * @method onExpandDescription() - Разворачивает/сворачивает описание - * - * Возвращает: - * HTML шаблон с информацией о программе и новостной лентой - */ @Component({ selector: "app-main", templateUrl: "./main.component.html", @@ -83,10 +52,12 @@ import { ProjectService } from "@office/services/project.service"; ProgramNewsCardComponent, TagComponent, UserLinksPipe, + AsyncPipe, ParseBreaksPipe, ParseLinksPipe, NewsFormComponent, ModalComponent, + MatProgressBarModule, ], }) export class ProgramDetailMainComponent implements OnInit, OnDestroy { @@ -96,7 +67,8 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { private readonly projectService: ProjectService, private readonly router: Router, private readonly route: ActivatedRoute, - private readonly cdRef: ChangeDetectorRef + private readonly cdRef: ChangeDetectorRef, + private readonly loadingService: LoadingService ) {} news = signal([]); @@ -110,6 +82,7 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { programId?: number; subscriptions$ = signal([]); + ngOnInit(): void { const programIdSubscription$ = this.route.params .pipe( @@ -123,6 +96,8 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { const routeModalSub$ = this.route.queryParams.subscribe(param => { if (param["access"] === "accessDenied") { + this.loadingService.hide(); + this.showProgramModal.set(true); this.showProgramModalErrorMessage.set("У вас не доступа к этой вкладке!"); @@ -150,13 +125,25 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { } }) ) - .subscribe(news => { - if (news.results?.length) { - this.news.set(news.results); - this.totalNewsCount.set(news.count); - } + .subscribe({ + next: news => { + if (news.results?.length) { + this.news.set(news.results); + this.totalNewsCount.set(news.count); + } + + this.loadingService.hide(); + }, + error: () => { + this.loadingService.hide(); + + this.showProgramModal.set(true); + this.showProgramModalErrorMessage.set("Произошла ошибка при загрузке программы"); + }, }); + this.loadEvent = fromEvent(window, "load"); + this.subscriptions$().push(program$); this.subscriptions$().push(programIdSubscription$); this.subscriptions$().push(routeModalSub$); @@ -216,6 +203,7 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { @ViewChild(NewsFormComponent) newsFormComponent?: NewsFormComponent; @ViewChild("descEl") descEl?: ElementRef; + onNewsInVew(entries: IntersectionObserverEntry[]): void { const ids = entries.map(e => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -263,20 +251,33 @@ export class ProgramDetailMainComponent implements OnInit, OnDestroy { closeModal(): void { this.showProgramModal.set(false); + this.loadingService.hide(); } addProject(): void { - this.projectService.create().subscribe(project => { - this.projectService.projectsCount.next({ - ...this.projectService.projectsCount.getValue(), - my: this.projectService.projectsCount.getValue().my + 1, - }); - this.router - .navigateByUrl(`/office/projects/${project.id}/edit?editingStep=main`) - .then(() => console.debug("Route change from ProjectsComponent")); + this.loadingService.show(); + + this.projectService.create().subscribe({ + next: project => { + this.projectService.projectsCount.next({ + ...this.projectService.projectsCount.getValue(), + my: this.projectService.projectsCount.getValue().my + 1, + }); + this.router + .navigateByUrl(`/office/projects/${project.id}/edit?editingStep=main`) + .then(() => { + console.debug("Route change from ProjectsComponent"); + }); + }, + error: error => { + this.loadingService.hide(); + console.error("Project creation error:", error); + }, }); } + private loadEvent?: Observable; + program?: Program; registerDateExpired!: boolean; descriptionExpandable!: boolean; diff --git a/projects/social_platform/src/app/office/services/loading.service.ts b/projects/social_platform/src/app/office/services/loading.service.ts new file mode 100644 index 000000000..efafda4d5 --- /dev/null +++ b/projects/social_platform/src/app/office/services/loading.service.ts @@ -0,0 +1,27 @@ +/** @format */ + +import { Injectable } from "@angular/core"; +import { BehaviorSubject, Observable } from "rxjs"; + +@Injectable({ + providedIn: "root", +}) +export class LoadingService { + private _isLoading$ = new BehaviorSubject(false); + + get isLoading$(): Observable { + return this._isLoading$.asObservable(); + } + + show(): void { + this._isLoading$.next(true); + } + + hide(): void { + this._isLoading$.next(false); + } + + toggle(): void { + this._isLoading$.next(!this._isLoading$.value); + } +}