diff --git a/projects/core/src/consts/lists/education-info-list.const.ts b/projects/core/src/consts/lists/education-info-list.const.ts index d9dc0806c..b4975ab36 100644 --- a/projects/core/src/consts/lists/education-info-list.const.ts +++ b/projects/core/src/consts/lists/education-info-list.const.ts @@ -44,4 +44,9 @@ export const educationUserLevel = [ value: "Высшее образование – аспирантура", label: "высшее образование – аспирантура", }, + // { + // id: 5, + // value: "Дополнительное профессиональное образования", + // label: "дополнительное профессиональное образования" + // }, ]; diff --git a/projects/social_platform/src/app/office/courses/detail/course-detail.component.ts b/projects/social_platform/src/app/office/courses/detail/course-detail.component.ts index 8bd12453a..b66e3a03b 100644 --- a/projects/social_platform/src/app/office/courses/detail/course-detail.component.ts +++ b/projects/social_platform/src/app/office/courses/detail/course-detail.component.ts @@ -77,8 +77,6 @@ export class CourseDetailComponent implements OnInit { } redirectToProgram(): void { - this.router.navigate([`/office/program/${this.course()?.partnerProgramId}`], { - queryParams: { courseId: this.course()?.id }, - }); + this.router.navigateByUrl(`/office/program/${this.course()?.partnerProgramId}`); } } diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.scss b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.scss index 8366c8d14..002046d0e 100644 --- a/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.scss +++ b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.scss @@ -29,7 +29,7 @@ display: block; align-self: center; width: 100%; - max-width: 295px; + max-width: 395px; height: 223px; aspect-ratio: 16 / 9; border: 0; @@ -82,6 +82,8 @@ } .read-more { + align-items: start; + align-self: self-start; margin-top: 12px; color: var(--accent); cursor: pointer; diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.scss b/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.scss index 3ef36bfd3..4b6a23d3b 100644 --- a/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.scss +++ b/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.scss @@ -105,7 +105,7 @@ display: block; align-self: center; width: 100%; - max-width: 295px; + max-width: 395px; height: 223px; aspect-ratio: 16 / 9; border: 0; @@ -155,6 +155,8 @@ } .read-more { + align-items: start; + align-self: self-start; margin-top: 12px; color: var(--accent); cursor: pointer; diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.scss b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.scss index a2b1ddda0..f0f8ef5ac 100644 --- a/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.scss +++ b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.scss @@ -33,6 +33,8 @@ } .read-more { + align-items: start; + align-self: self-start; margin-top: 12px; color: var(--accent); cursor: pointer; @@ -132,7 +134,7 @@ display: block; align-self: center; width: 100%; - max-width: 295px; + max-width: 395px; height: 223px; aspect-ratio: 16 / 9; border: 0; diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.scss b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.scss index a0a57bad1..8db8a6463 100644 --- a/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.scss +++ b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.scss @@ -17,7 +17,7 @@ display: block; align-self: center; width: 100%; - max-width: 295px; + max-width: 395px; height: 223px; aspect-ratio: 16 / 9; border: 0; @@ -75,6 +75,8 @@ } .read-more { + align-items: start; + align-self: self-start; margin-top: 12px; color: var(--accent); cursor: pointer; diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.scss b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.scss index 22c824532..c5f57bfa1 100644 --- a/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.scss +++ b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.scss @@ -49,7 +49,7 @@ display: block; align-self: center; width: 100%; - max-width: 295px; + max-width: 395px; height: 223px; aspect-ratio: 16 / 9; border: 0; @@ -63,6 +63,8 @@ } .read-more { + align-items: start; + align-self: self-start; margin-top: 12px; color: var(--accent); cursor: pointer; diff --git a/projects/social_platform/src/app/office/features/detail/detail.component.html b/projects/social_platform/src/app/office/features/detail/detail.component.html index d36dfd81a..b4be4f2ed 100644 --- a/projects/social_platform/src/app/office/features/detail/detail.component.html +++ b/projects/social_platform/src/app/office/features/detail/detail.component.html @@ -181,11 +181,8 @@ участники - } @else { @if (info().courseId || queryCourseId()) { - + } @else { @if (info().courses?.length) { + перейти в курс diff --git a/projects/social_platform/src/app/office/features/detail/detail.component.ts b/projects/social_platform/src/app/office/features/detail/detail.component.ts index 357ad0e22..22f9daa5e 100644 --- a/projects/social_platform/src/app/office/features/detail/detail.component.ts +++ b/projects/social_platform/src/app/office/features/detail/detail.component.ts @@ -80,7 +80,6 @@ export class DeatilComponent implements OnInit, OnDestroy { profile?: User; profileProjects = signal([]); listType: "project" | "program" | "profile" = "project"; - queryCourseId = signal(null); // Переменная для подсказок isTooltipVisible = false; @@ -159,12 +158,6 @@ export class DeatilComponent implements OnInit, OnDestroy { this.listType = data["listType"]; }); - const queryParamsSub$ = this.route.queryParams.subscribe(params => { - const courseId = params["courseId"]; - this.queryCourseId.set(courseId ? +courseId : null); - }); - this.subscriptions.push(queryParamsSub$); - this.initializeBackPath(); this.updatePageStates(); diff --git a/projects/social_platform/src/app/office/features/news-card/news-card.component.ts b/projects/social_platform/src/app/office/features/news-card/news-card.component.ts index 29cc4b868..0801f2cb5 100644 --- a/projects/social_platform/src/app/office/features/news-card/news-card.component.ts +++ b/projects/social_platform/src/app/office/features/news-card/news-card.component.ts @@ -25,7 +25,7 @@ import { FileService } from "@core/services/file.service"; import { nanoid } from "nanoid"; import { expandElement } from "@utils/expand-element"; import { FileModel } from "@models/file.model"; -import { forkJoin, noop, Observable, tap } from "rxjs"; +import { catchError, forkJoin, noop, Observable, of, tap } from "rxjs"; import { ButtonComponent, IconComponent } from "@ui/components"; import { FileItemComponent } from "@ui/components/file-item/file-item.component"; import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-upload-item.component"; @@ -279,6 +279,11 @@ export class NewsCardComponent implements OnInit { fileObj.src = file.url; fileObj.loading = false; fileObj.tempFile = null; + }), + catchError(() => { + fileObj.loading = false; + fileObj.error = true; + return of(null); }) ) ); @@ -301,6 +306,11 @@ export class NewsCardComponent implements OnInit { fileObj.loading = false; fileObj.src = file.url; fileObj.tempFile = null; + }), + catchError(() => { + fileObj.loading = false; + fileObj.error = "Ошибка загрузки"; + return of(null); }) ) ); diff --git a/projects/social_platform/src/app/office/features/news-form/news-form.component.ts b/projects/social_platform/src/app/office/features/news-form/news-form.component.ts index 31ba1cdc8..6cb0546d6 100644 --- a/projects/social_platform/src/app/office/features/news-form/news-form.component.ts +++ b/projects/social_platform/src/app/office/features/news-form/news-form.component.ts @@ -5,7 +5,7 @@ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angula import { ValidationService } from "projects/core"; import { nanoid } from "nanoid"; import { FileService } from "@core/services/file.service"; -import { forkJoin, noop, Observable, tap } from "rxjs"; +import { catchError, forkJoin, noop, Observable, of, tap } from "rxjs"; import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-upload-item.component"; import { IconComponent, InputComponent } from "@ui/components"; import { AutosizeModule } from "ngx-autosize"; @@ -76,6 +76,8 @@ export class NewsFormComponent implements OnInit { ...this.messageForm.value, files: [...this.imagesList.map(f => f.src), ...this.filesList.map(f => f.src)], }); + + this.onResetForm(); } /** @@ -83,6 +85,7 @@ export class NewsFormComponent implements OnInit { */ onResetForm() { this.imagesList = []; + this.filesList = []; this.messageForm.reset(); } @@ -128,6 +131,11 @@ export class NewsFormComponent implements OnInit { fileObj.src = file.url; fileObj.loading = false; fileObj.tempFile = null; + }), + catchError(() => { + fileObj.loading = false; + fileObj.error = true; + return of(null); }) ) ); @@ -145,6 +153,11 @@ export class NewsFormComponent implements OnInit { tap(file => { fileObj.loading = false; fileObj.src = file.url; + }), + catchError(() => { + fileObj.loading = false; + fileObj.error = "Ошибка загрузки"; + return of(null); }) ) ); diff --git a/projects/social_platform/src/app/office/feed/feed.component.ts b/projects/social_platform/src/app/office/feed/feed.component.ts index b0cd630d4..7b2609c01 100644 --- a/projects/social_platform/src/app/office/feed/feed.component.ts +++ b/projects/social_platform/src/app/office/feed/feed.component.ts @@ -15,7 +15,18 @@ import { CommonModule } from "@angular/common"; import { NewProjectComponent } from "@office/feed/shared/new-project/new-project.component"; import { ActivatedRoute } from "@angular/router"; import { FeedItem, FeedItemType } from "@office/feed/models/feed-item.model"; -import { concatMap, fromEvent, map, noop, of, skip, Subscription, tap, throttleTime } from "rxjs"; +import { + catchError, + concatMap, + fromEvent, + map, + noop, + of, + skip, + Subscription, + tap, + throttleTime, +} from "rxjs"; import { ApiPagination } from "@models/api-pagination.model"; import { FeedService } from "@office/feed/services/feed.service"; import { ProjectNewsService } from "@office/projects/detail/services/project-news.service"; @@ -99,7 +110,7 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy { if (target) { const scrollEvents$ = fromEvent(target, "scroll") .pipe( - concatMap(() => this.onScroll()), + concatMap(() => this.onScroll().pipe(catchError(() => of({})))), throttleTime(500) ) .subscribe(noop); diff --git a/projects/social_platform/src/app/office/members/members.component.ts b/projects/social_platform/src/app/office/members/members.component.ts index dff7d51e5..d3f0f3409 100644 --- a/projects/social_platform/src/app/office/members/members.component.ts +++ b/projects/social_platform/src/app/office/members/members.component.ts @@ -14,6 +14,7 @@ import { import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { BehaviorSubject, + catchError, concatMap, debounceTime, distinctUntilChanged, @@ -204,8 +205,8 @@ export class MembersComponent implements OnInit, OnDestroy, AfterViewInit { if (target) { const scrollEvents$ = fromEvent(target, "scroll") .pipe( - concatMap(() => this.onScroll()), - throttleTime(500) // Ограничиваем частоту обработки прокрутки + concatMap(() => this.onScroll().pipe(catchError(() => of({})))), + throttleTime(500) ) .subscribe(noop); diff --git a/projects/social_platform/src/app/office/mentors/mentors.component.ts b/projects/social_platform/src/app/office/mentors/mentors.component.ts index 1a1345d68..b67a692e5 100644 --- a/projects/social_platform/src/app/office/mentors/mentors.component.ts +++ b/projects/social_platform/src/app/office/mentors/mentors.component.ts @@ -11,14 +11,23 @@ import { ViewChild, } from "@angular/core"; import { ActivatedRoute, RouterLink } from "@angular/router"; -import { concatMap, fromEvent, map, noop, of, Subscription, tap, throttleTime } from "rxjs"; +import { + catchError, + concatMap, + fromEvent, + map, + noop, + of, + Subscription, + tap, + throttleTime, +} from "rxjs"; import { AuthService } from "@auth/services"; import { User } from "@auth/models/user.model"; import { NavService } from "@services/nav.service"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { containerSm } from "@utils/responsive"; import { MemberService } from "@services/member.service"; -import { MemberCardComponent } from "../shared/member-card/member-card.component"; import { ApiPagination } from "@models/api-pagination.model"; /** @@ -99,7 +108,7 @@ export class MentorsComponent implements OnInit, OnDestroy, AfterViewInit { if (target) fromEvent(target, "scroll") .pipe( - concatMap(() => this.onScroll()), + concatMap(() => this.onScroll().pipe(catchError(() => of({})))), throttleTime(500) ) .subscribe(noop); 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 801afc42d..5ed010191 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 @@ -82,10 +82,17 @@

редактирование профиля

@if(editingStep === 'main'){
+
+ + + +
+
@if (profileForm.get("avatar"); as avatar) {
-
редактирование профиля
} @if (profileForm.get("coverImageAddress"); as coverImageAddress) {
- { + fileObj.loading = false; + fileObj.error = true; + return of(null); }) ) ); @@ -288,6 +293,11 @@ export class ProgramNewsCardComponent implements OnInit, AfterViewInit { fileObj.loading = false; fileObj.src = file.url; fileObj.tempFile = null; + }), + catchError(() => { + fileObj.loading = false; + fileObj.error = "Ошибка загрузки"; + return of(null); }) ) ); diff --git a/projects/social_platform/src/app/office/program/models/program.model.ts b/projects/social_platform/src/app/office/program/models/program.model.ts index 6c231e449..25c58f1e4 100644 --- a/projects/social_platform/src/app/office/program/models/program.model.ts +++ b/projects/social_platform/src/app/office/program/models/program.model.ts @@ -59,6 +59,7 @@ export class Program { isUserMember!: boolean; publishProjectsAfterFinish!: boolean; courseId!: number | null; + courses!: { id: number; title: string; isAvailable: boolean }[]; static default(): Program { return { @@ -88,6 +89,7 @@ export class Program { isUserManager: false, publishProjectsAfterFinish: false, courseId: null, + courses: [], }; } } 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 f9e261853..d583f62a0 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 @@ -128,7 +128,7 @@ export class ProgramService { url += `?${params.toString()}`; } - return this.apiService.post(url, { filters: filters }); + return this.apiService.post(url, { filters }); } submitCompettetiveProject(relationId: number): Observable { diff --git a/projects/social_platform/src/app/office/program/services/project-rating.service.ts b/projects/social_platform/src/app/office/program/services/project-rating.service.ts index e30cb490e..5887937fc 100644 --- a/projects/social_platform/src/app/office/program/services/project-rating.service.ts +++ b/projects/social_platform/src/app/office/program/services/project-rating.service.ts @@ -58,7 +58,7 @@ export class ProjectRatingService { url += `?${params.toString()}`; } - return this.apiService.post(url, { filters: filters }); + return this.apiService.post(url, { filters }); } rate(projectId: number, scores: ProjectRatingCriterionOutput[]): Observable { diff --git a/projects/social_platform/src/app/office/projects/list/list.component.ts b/projects/social_platform/src/app/office/projects/list/list.component.ts index 3cba85c12..0eac12ccc 100644 --- a/projects/social_platform/src/app/office/projects/list/list.component.ts +++ b/projects/social_platform/src/app/office/projects/list/list.component.ts @@ -13,6 +13,7 @@ import { } from "@angular/core"; import { ActivatedRoute, NavigationEnd, Params, Router, RouterLink } from "@angular/router"; import { + catchError, concatMap, distinctUntilChanged, fromEvent, @@ -192,7 +193,7 @@ export class ProjectsListComponent implements OnInit, AfterViewInit, OnDestroy { if (target) { const scrollEvent$ = fromEvent(target, "scroll") .pipe( - concatMap(() => this.onScroll()), + concatMap(() => this.onScroll().pipe(catchError(() => of({})))), throttleTime(500) ) .subscribe(noop); diff --git a/projects/social_platform/src/app/office/vacancies/list/list.component.ts b/projects/social_platform/src/app/office/vacancies/list/list.component.ts index cce757e48..e387a330d 100644 --- a/projects/social_platform/src/app/office/vacancies/list/list.component.ts +++ b/projects/social_platform/src/app/office/vacancies/list/list.component.ts @@ -6,6 +6,7 @@ import { Component, inject, signal } from "@angular/core"; import { CommonModule } from "@angular/common"; import { + catchError, concatMap, debounceTime, fromEvent, @@ -124,7 +125,7 @@ export class VacanciesListComponent { if (target) { const scrollEvents$ = fromEvent(target, "scroll") .pipe( - concatMap(() => this.onScroll()), + concatMap(() => this.onScroll().pipe(catchError(() => of({})))), throttleTime(500) ) .subscribe(noop); 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 1e3cf0453..02f0ef396 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 @@ -68,6 +68,7 @@
{ + // Используем исходное событие, но imageChangedEvent уже содержит исправленное изображение + this.imageChangedEvent = event; + this.showCropperModal = true; + }); + } + + /** + * Исправляет EXIF ориентацию изображения + * Решает проблему с повернутыми фотографиями со смартфонов + */ + private fixImageOrientation(file: File, onComplete: () => void) { + const reader = new FileReader(); + + reader.onload = e => { + const img = new Image(); + img.onload = () => { + // Читаем EXIF данные для определения ориентации + this.getImageOrientation(file, orientation => { + // Если ориентация нормальная (1), просто используем исходное изображение + if (orientation === 1) { + this.correctedImageBase64 = ""; + onComplete(); + return; + } + + // Ротируем изображение на Canvas + const canvas = this.rotateImage(img, orientation); + this.correctedImageBase64 = canvas.toDataURL(file.type); + onComplete(); + }); + }; + img.src = e.target?.result as string; + }; + + reader.readAsDataURL(file); + } + + /** + * Определяет EXIF ориентацию изображения + */ + private getImageOrientation(file: File, onOrientationDetected: (orientation: number) => void) { + const reader = new FileReader(); + + reader.onload = event => { + const view = new DataView(event.target?.result as ArrayBuffer); + // Проверяем JPEG маркер + if (view.byteLength < 2 || view.getUint16(0) !== 0xffd8) { + onOrientationDetected(1); // Не JPEG, используем нормальную ориентацию + return; + } + + let offset = 2; + // Ищем EXIF данные + while (offset < view.byteLength - 9) { + if (view.getUint16(offset) === 0xffe1) { + const length = view.getUint16(offset + 2) + 2; + // Проверяем EXIF идентификатор + if (view.getUint32(offset + 4) === 0x45786966 && view.getUint16(offset + 8) === 0x0000) { + const orientation = this.getExifOrientation(view, offset + 10); + onOrientationDetected(orientation); + return; + } + offset += length; + } else { + offset += 2; + } + } + onOrientationDetected(1); // EXIF не найден, используем нормальную ориентацию + }; + + reader.readAsArrayBuffer(file); + } + + /** + * Извлекает значение ориентации из EXIF данных + */ + private getExifOrientation(view: DataView, offset: number): number { + try { + const littleEndian = view.getUint16(offset) === 0x4949; + const ifdOffset = view.getUint32(offset + 4, littleEndian); + const entries = view.getUint16(offset + ifdOffset, littleEndian); + + for (let i = 0; i < entries; i++) { + const entryOffset = offset + ifdOffset + 2 + i * 12; + const tag = view.getUint16(entryOffset, littleEndian); + // 0x0112 это тег для ориентации (Orientation tag) + if (tag === 0x0112) { + const value = view.getUint32(entryOffset + 8, littleEndian); + return value > 1 && value <= 8 ? value : 1; + } + } + } catch (e) { + console.warn("Ошибка при чтении EXIF ориентации:", e); + } + return 1; + } + + /** + * Ротирует изображение на Canvas в зависимости от EXIF ориентации + */ + private rotateImage(img: HTMLImageElement, orientation: number): HTMLCanvasElement { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) { + return canvas; + } + + const [newWidth, newHeight] = [img.width, img.height]; + + switch (orientation) { + case 2: + canvas.width = newWidth; + canvas.height = newHeight; + ctx.scale(-1, 1); + ctx.drawImage(img, -newWidth, 0); + break; + case 3: + canvas.width = newWidth; + canvas.height = newHeight; + ctx.rotate(Math.PI); + ctx.drawImage(img, -newWidth, -newHeight); + break; + case 4: + canvas.width = newWidth; + canvas.height = newHeight; + ctx.scale(1, -1); + ctx.drawImage(img, 0, -newHeight); + break; + case 5: + canvas.width = newHeight; + canvas.height = newWidth; + ctx.rotate(Math.PI / 2); + ctx.scale(-1, 1); + ctx.drawImage(img, -newHeight, 0); + break; + case 6: + canvas.width = newHeight; + canvas.height = newWidth; + ctx.rotate(Math.PI / 2); + ctx.drawImage(img, 0, -newWidth); + break; + case 7: + canvas.width = newHeight; + canvas.height = newWidth; + ctx.rotate(-Math.PI / 2); + ctx.scale(-1, 1); + ctx.drawImage(img, -newHeight, -newWidth); + break; + case 8: + canvas.width = newHeight; + canvas.height = newWidth; + ctx.rotate(-Math.PI / 2); + ctx.drawImage(img, -newHeight, 0); + break; + default: + canvas.width = newWidth; + canvas.height = newHeight; + ctx.drawImage(img, 0, 0); + } + + return canvas; } /**