diff --git a/projects/social_platform/src/app/office/courses/courses.service.ts b/projects/social_platform/src/app/office/courses/courses.service.ts index 629fa14ea..4667747e8 100644 --- a/projects/social_platform/src/app/office/courses/courses.service.ts +++ b/projects/social_platform/src/app/office/courses/courses.service.ts @@ -1,6 +1,6 @@ /** @format */ -import { Injectable } from "@angular/core"; +import { Injectable, signal } from "@angular/core"; import { ApiService } from "@corelib"; import { CourseCard, @@ -23,6 +23,9 @@ import { Observable } from "rxjs"; export class CoursesService { private readonly COURSE_URL = "/courses"; + readonly currentLesson = signal(null); + readonly courseStructure = signal(null); + constructor(private readonly apiService: ApiService) {} /** diff --git a/projects/social_platform/src/app/office/courses/detail/course-detail.component.html b/projects/social_platform/src/app/office/courses/detail/course-detail.component.html index e874a8146..4305f7b0c 100644 --- a/projects/social_platform/src/app/office/courses/detail/course-detail.component.html +++ b/projects/social_platform/src/app/office/courses/detail/course-detail.component.html @@ -2,46 +2,96 @@
- @if(course()) { + @if (course()) {
- cover - -
- - @if (!isTaskDetail()) { -

{{ course()!.title }}

- } + @if (showCover) { +
+ cover + +
+ + + @if (!isTaskDetail() && !isMobile) { +

+ {{ course()!.title }} +

+ } +
-
+ } + +
+
+ @if (showBackOnly) { + + назад + -
-
- {{ isTaskDetail() ? "назад к модулю" : "назад" }} - - вернуться в программу +
+ @if (currentLesson()) { +

модуль {{ currentLesson()!.moduleOrder }}

+

урок {{ lessonOrder }}

+ } +
+ + + о курсе + + } @else { @if (showAnalyticsButton) { + аналитика + } @if (showAboutButton) { + + о курсе + + } + + + назад + + + @if (showProgramButton) { + + вернуться в программу + + } } +
+ + + + }
diff --git a/projects/social_platform/src/app/office/courses/detail/course-detail.component.scss b/projects/social_platform/src/app/office/courses/detail/course-detail.component.scss index 5383399fb..bfed9bc0c 100644 --- a/projects/social_platform/src/app/office/courses/detail/course-detail.component.scss +++ b/projects/social_platform/src/app/office/courses/detail/course-detail.component.scss @@ -7,9 +7,16 @@ $detail-bar-mb: 12px; .detail { display: flex; flex-direction: column; - height: 100%; max-height: 100%; padding-top: 20px; + padding-bottom: 20px; + margin: 0 20px; + + @include responsive.apply-desktop { + height: 100%; + padding-bottom: 0; + margin: 0; + } &__body { flex-grow: 1; @@ -23,14 +30,19 @@ $detail-bar-mb: 12px; position: relative; padding: 0; + margin-bottom: 20px; border: none; border-radius: $body-slide; &__cover { position: relative; - width: 100%; height: 136px; - border-radius: 15px 15px 0 0; + margin-bottom: 24px; + border-radius: 15px; + + @include responsive.apply-desktop { + border-radius: 15px 15px 0 0; + } img { position: absolute; @@ -53,13 +65,14 @@ $detail-bar-mb: 12px; position: absolute; bottom: -10px; left: 50%; - z-index: 100; + z-index: 10; display: block; display: flex; flex-direction: column; align-items: center; cursor: pointer; border-radius: 50%; + transform: translate(-50%, 30%); &--program { bottom: 15px; @@ -102,13 +115,47 @@ $detail-bar-mb: 12px; &__actions { display: grid; grid-template-columns: 1fr 1fr; - gap: 180px; + gap: 12px; align-items: center; - padding: 24px 0 30px; + + @include responsive.apply-desktop { + gap: 180px; + padding: 0 0 30px; + } + + &--task { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: space-between; + } + + &--end { + justify-self: end; + } + + &--full { + grid-column: 1 / -1; + + @include responsive.apply-desktop { + grid-column: auto; + } + } &--disabled { cursor: not-allowed; - opacity: 0.5; } } } + +.task { + &__lesson { + color: var(--dark-grey); + } + + &__meta { + display: flex; + flex-direction: column; + align-items: center; + } +} 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 b66e3a03b..3d3b5beee 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 @@ -1,13 +1,17 @@ /** @format */ import { CommonModule } from "@angular/common"; -import { Component, DestroyRef, inject, signal, OnInit } from "@angular/core"; +import { Component, DestroyRef, HostListener, inject, signal, OnInit } from "@angular/core"; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { filter, map, tap } from "rxjs"; import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { ButtonComponent } from "@ui/components"; +import { IconComponent } from "@uilib"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { CourseDetail, CourseStructure } from "@office/models/courses.model"; +import { CourseDetail, CourseLesson, CourseStructure } from "@office/models/courses.model"; +import { ModalComponent } from "@ui/components/modal/modal.component"; +import { CourseAboutComponent } from "@office/courses/shared/course-about/course-about.component"; +import { CoursesService } from "@office/courses/courses.service"; /** * Компонент детального просмотра траектории @@ -17,7 +21,15 @@ import { CourseDetail, CourseStructure } from "@office/models/courses.model"; @Component({ selector: "app-course-detail", standalone: true, - imports: [CommonModule, RouterOutlet, AvatarComponent, ButtonComponent], + imports: [ + CommonModule, + RouterOutlet, + AvatarComponent, + ButtonComponent, + IconComponent, + ModalComponent, + CourseAboutComponent, + ], templateUrl: "./course-detail.component.html", styleUrl: "./course-detail.component.scss", }) @@ -25,12 +37,59 @@ export class CourseDetailComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); + private readonly coursesService = inject(CoursesService); + + appWidth = window.innerWidth; + + @HostListener("window:resize") + onResize() { + this.appWidth = window.innerWidth; + } protected readonly isTaskDetail = signal(false); protected readonly isDisabled = signal(false); + isAboutModalOpen = false; protected readonly courseModules = signal([]); protected readonly course = signal(undefined); + protected readonly courseStructure = signal(undefined); + protected readonly currentLesson = this.coursesService.currentLesson; + + get lessonOrder(): number | null { + const lesson = this.currentLesson(); + const structure = this.courseStructure(); + if (!lesson || !structure) return null; + + for (const mod of structure.modules) { + const found = mod.lessons.find(l => l.id === lesson.id); + if (found) return found.order; + } + return null; + } + + get isMobile(): boolean { + return this.appWidth < 920; + } + + get showCover(): boolean { + return !this.isTaskDetail() || !this.isMobile; + } + + get showAboutButton(): boolean { + return this.isMobile && !this.isTaskDetail(); + } + + get showBackOnly(): boolean { + return this.isTaskDetail() && this.isMobile; + } + + get showAnalyticsButton(): boolean { + return this.isMobile && !this.isTaskDetail(); + } + + get showProgramButton(): boolean { + return !this.showBackOnly; + } /** * Инициализация компонента @@ -44,8 +103,10 @@ export class CourseDetailComponent implements OnInit { takeUntilDestroyed(this.destroyRef) ) .subscribe({ - next: ([course, _]: [CourseDetail, CourseStructure]) => { + next: ([course, structure]: [CourseDetail, CourseStructure]) => { this.course.set(course); + this.courseStructure.set(structure); + this.coursesService.courseStructure.set(structure); if (!course.partnerProgramId) { this.isDisabled.set(true); diff --git a/projects/social_platform/src/app/office/courses/detail/course-detail.routes.ts b/projects/social_platform/src/app/office/courses/detail/course-detail.routes.ts index 8b168483c..5550d2854 100644 --- a/projects/social_platform/src/app/office/courses/detail/course-detail.routes.ts +++ b/projects/social_platform/src/app/office/courses/detail/course-detail.routes.ts @@ -1,9 +1,9 @@ /** @format */ import type { Routes } from "@angular/router"; -import { TrajectoryInfoComponent } from "./info/info.component"; import { CourseDetailComponent } from "./course-detail.component"; import { CoursesDetailResolver } from "./course-detail.resolver"; +import { CourseInfoComponent } from "./info/info.component"; export const COURSE_DETAIL_ROUTES: Routes = [ { @@ -16,7 +16,7 @@ export const COURSE_DETAIL_ROUTES: Routes = [ children: [ { path: "", - component: TrajectoryInfoComponent, + component: CourseInfoComponent, }, { path: "lesson", diff --git a/projects/social_platform/src/app/office/courses/detail/info/info.component.html b/projects/social_platform/src/app/office/courses/detail/info/info.component.html index 1902dec97..2b6776ff7 100644 --- a/projects/social_platform/src/app/office/courses/detail/info/info.component.html +++ b/projects/social_platform/src/app/office/courses/detail/info/info.component.html @@ -4,6 +4,7 @@
+ @if (appWidth > 920) {
+ }
+ @if (appWidth > 920) {
-
-
-

о курсе

- -
- @if (courseDetail()!.description) { -
-

- @if (descriptionExpandable) { -
- {{ readFullDescription ? "cкрыть" : "подробнее" }} -
- } -
- } -
+
+ }
diff --git a/projects/social_platform/src/app/office/courses/detail/info/info.component.scss b/projects/social_platform/src/app/office/courses/detail/info/info.component.scss index fa96d9440..2bf5aac8b 100644 --- a/projects/social_platform/src/app/office/courses/detail/info/info.component.scss +++ b/projects/social_platform/src/app/office/courses/detail/info/info.component.scss @@ -40,8 +40,12 @@ &__details { display: grid; - grid-template-columns: 2fr 5fr 3fr; + grid-template-columns: 1fr; grid-gap: 20px; + + @include responsive.apply-desktop { + grid-template-columns: 2fr 5fr 3fr; + } } &__progress { @@ -92,76 +96,6 @@ } } -.about { - padding: 24px; - background-color: var(--light-white); - border-radius: var(--rounded-lg); - - &__head { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; - border-bottom: 0.5px solid var(--accent); - - &--icon { - color: var(--accent); - } - } - - &__title { - margin-bottom: 8px; - color: var(--accent); - } - - /* stylelint-disable value-no-vendor-prefix */ - &__text { - p { - display: -webkit-box; - overflow: hidden; - color: var(--black); - text-overflow: ellipsis; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - transition: all 0.7s ease-in-out; - - &.expanded { - -webkit-line-clamp: unset; - } - } - - ::ng-deep a { - color: var(--accent); - text-decoration: underline; - text-decoration-color: transparent; - text-underline-offset: 3px; - transition: text-decoration-color 0.2s; - - &:hover { - text-decoration-color: var(--accent); - } - } - } - /* stylelint-enable value-no-vendor-prefix */ - - &__read-full { - margin-top: 8px; - color: var(--accent); - cursor: pointer; - } -} - -.read-more { - margin-top: 8px; - color: var(--accent); - cursor: pointer; - transition: background-color 0.2s; - - &:hover { - color: var(--accent-dark); - } -} - .cancel { display: flex; flex-direction: column; diff --git a/projects/social_platform/src/app/office/courses/detail/info/info.component.ts b/projects/social_platform/src/app/office/courses/detail/info/info.component.ts index 5e15b937d..40da88b16 100644 --- a/projects/social_platform/src/app/office/courses/detail/info/info.component.ts +++ b/projects/social_platform/src/app/office/courses/detail/info/info.component.ts @@ -1,20 +1,9 @@ /** @format */ -import { - AfterViewInit, - ChangeDetectorRef, - Component, - DestroyRef, - ElementRef, - inject, - OnInit, - signal, - ViewChild, -} from "@angular/core"; +import { Component, DestroyRef, HostListener, inject, OnInit, signal } from "@angular/core"; import { ActivatedRoute, RouterModule } from "@angular/router"; -import { ParseBreaksPipe, ParseLinksPipe } from "@corelib"; import { IconComponent } from "@uilib"; -import { expandElement } from "@utils/expand-element"; +import { CourseAboutComponent } from "@office/courses/shared/course-about/course-about.component"; import { map } from "rxjs"; import { CommonModule } from "@angular/common"; import { SoonCardComponent } from "@office/shared/soon-card/soon-card.component"; @@ -40,25 +29,27 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; imports: [ IconComponent, RouterModule, - ParseBreaksPipe, - ParseLinksPipe, CommonModule, SoonCardComponent, ModalComponent, ButtonComponent, CourseModuleCardComponent, + CourseAboutComponent, ], templateUrl: "./info.component.html", styleUrl: "./info.component.scss", }) -export class TrajectoryInfoComponent implements OnInit, AfterViewInit { - @ViewChild("descEl") descEl?: ElementRef; +export class CourseInfoComponent implements OnInit { + appWidth = window.innerWidth; + + @HostListener("window:resize") + onResize() { + this.appWidth = window.innerWidth; + } private readonly route = inject(ActivatedRoute); private readonly destroyRef = inject(DestroyRef); - private readonly cdRef = inject(ChangeDetectorRef); - protected readonly courseStructure = signal(undefined); protected readonly courseDetail = signal(undefined); protected readonly isCompleteModule = signal(false); @@ -100,28 +91,4 @@ export class TrajectoryInfoComponent implements OnInit, AfterViewInit { } }); } - - protected descriptionExpandable?: boolean; - protected readFullDescription!: boolean; - - /** - * Проверка возможности расширения описания после инициализации представления - */ - ngAfterViewInit(): void { - const descElement = this.descEl?.nativeElement; - this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight; - - this.cdRef.detectChanges(); - } - - /** - * Переключение развернутого/свернутого состояния описания - * @param elem - HTML элемент описания - * @param expandedClass - CSS класс для развернутого состояния - * @param isExpanded - текущее состояние (развернуто/свернуто) - */ - onExpandDescription(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void { - expandElement(elem, expandedClass, isExpanded); - this.readFullDescription = !isExpanded; - } } diff --git a/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.scss b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.scss index 0e2a04514..ee674d96f 100644 --- a/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.scss +++ b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.scss @@ -2,12 +2,16 @@ @use "styles/typography"; .complete { - min-height: 358px; + min-height: 543px; padding: 24px; background-color: var(--light-white); border: 0.5px solid var(--medium-grey-for-outline); border-radius: var(--rounded-lg); + @include responsive.apply-desktop { + min-height: 358px; + } + &__block { display: flex; flex-direction: column; diff --git a/projects/social_platform/src/app/office/courses/lesson/lesson.component.html b/projects/social_platform/src/app/office/courses/lesson/lesson.component.html index 53cca9543..fdc3ea4a9 100644 --- a/projects/social_platform/src/app/office/courses/lesson/lesson.component.html +++ b/projects/social_platform/src/app/office/courses/lesson/lesson.component.html @@ -2,10 +2,13 @@ @if (lessonInfo()) {
-
+
+ @if (appWidth >= 920) {

модуль {{ lessonInfo()?.moduleOrder }}

+

урок {{ lessonOrder() }}

+ }
@for (task of tasks(); track task.id) { @@ -15,7 +18,7 @@ [ngClass]="{ 'progress__task--done': isDone(task), 'progress__task--current': isCurrent(task.id), - 'progress__task--clickable': isDone(task) + 'progress__task--clickable': isClickable(task) }" >

{{ task.order }}

diff --git a/projects/social_platform/src/app/office/courses/lesson/lesson.component.scss b/projects/social_platform/src/app/office/courses/lesson/lesson.component.scss index fa3302c71..acc498df6 100644 --- a/projects/social_platform/src/app/office/courses/lesson/lesson.component.scss +++ b/projects/social_platform/src/app/office/courses/lesson/lesson.component.scss @@ -10,12 +10,21 @@ .task { display: grid; - grid-template-columns: 68px 1fr; + grid-template-columns: 1fr; gap: 20px; + @include responsive.apply-desktop { + grid-template-columns: 68px 1fr; + } + &__wrapper { display: flex; flex-direction: column; + order: -1; + + @include responsive.apply-desktop { + order: 0; + } } &__tasks { @@ -27,14 +36,21 @@ color: var(--black); } - &__name { - color: var(--dark-grey); + &__lesson { + color: var(--grey-for-text); } &__progress { display: flex; - flex-direction: column; + flex-direction: row; gap: 12px; + padding-bottom: 0; + overflow-x: auto; + + @include responsive.apply-desktop { + flex-direction: column; + overflow-x: visible; + } } &__main { @@ -43,6 +59,10 @@ justify-content: center; } + &__content { + width: 100%; + } + &__complete { display: block; } @@ -54,13 +74,19 @@ margin-top: 20px; :last-child { - width: 333px; + width: 90%; + + @include responsive.apply-desktop { + width: 333px; + } } } } .progress { &__task { + flex-shrink: 0; + min-width: 42px; height: 23px; padding: 4px 0; text-align: center; @@ -82,3 +108,15 @@ } } } + +.action { + &__button { + position: fixed; + bottom: 2%; + left: 5%; + + @include responsive.apply-desktop { + position: static; + } + } +} diff --git a/projects/social_platform/src/app/office/courses/lesson/lesson.component.ts b/projects/social_platform/src/app/office/courses/lesson/lesson.component.ts index ff9bed681..3ed1c5c1d 100644 --- a/projects/social_platform/src/app/office/courses/lesson/lesson.component.ts +++ b/projects/social_platform/src/app/office/courses/lesson/lesson.component.ts @@ -1,6 +1,14 @@ /** @format */ -import { Component, computed, DestroyRef, inject, OnInit, signal } from "@angular/core"; +import { + Component, + computed, + DestroyRef, + HostListener, + inject, + OnInit, + signal, +} from "@angular/core"; import { CommonModule } from "@angular/common"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from "@angular/router"; @@ -40,9 +48,17 @@ export class LessonComponent implements OnInit { private readonly coursesService = inject(CoursesService); private readonly snackbarService = inject(SnackbarService); + appWidth = window.innerWidth; + + @HostListener("window:resize") + onResize() { + this.appWidth = window.innerWidth; + } + protected readonly lessonInfo = signal(undefined); protected readonly isComplete = signal(false); protected readonly currentTaskId = signal(null); + protected readonly activeTaskId = signal(null); protected readonly loader = signal(false); protected readonly loading = signal(false); @@ -54,6 +70,18 @@ export class LessonComponent implements OnInit { protected readonly tasks = computed(() => this.lessonInfo()?.tasks ?? []); + protected readonly lessonOrder = computed(() => { + const lesson = this.lessonInfo(); + const structure = this.coursesService.courseStructure(); + if (!lesson || !structure) return null; + + for (const mod of structure.modules) { + const found = mod.lessons.find(l => l.id === lesson.id); + if (found) return found.order; + } + return null; + }); + protected readonly currentTask = computed(() => { const id = this.currentTaskId(); return this.tasks().find(t => t.id === id) ?? null; @@ -96,6 +124,8 @@ export class LessonComponent implements OnInit { .subscribe({ next: lessonInfo => { this.lessonInfo.set(lessonInfo); + console.log(lessonInfo); + this.coursesService.currentLesson.set(lessonInfo); // Если курс уже завершен, редирект на results if (lessonInfo.progressStatus === "completed") { @@ -117,6 +147,7 @@ export class LessonComponent implements OnInit { if (onResultsPage && !allCompleted) { // Находимся на results, но не все задания выполнены — редирект обратно this.currentTaskId.set(nextTaskId); + this.activeTaskId.set(nextTaskId); setTimeout(() => { this.loading.set(false); this.router.navigate(["./"], { relativeTo: this.route }); @@ -129,6 +160,7 @@ export class LessonComponent implements OnInit { }, 500); } else { this.currentTaskId.set(nextTaskId); + this.activeTaskId.set(nextTaskId); setTimeout(() => this.loading.set(false), 500); } }, @@ -162,8 +194,12 @@ export class LessonComponent implements OnInit { return task ? this.isDone(task) : false; }); + isClickable(task: Task): boolean { + return this.isDone(task) || task.id === this.activeTaskId(); + } + onSelectTask(task: Task) { - if (!this.isDone(task)) return; + if (!this.isClickable(task)) return; this.currentTaskId.set(task.id); this.answerBody.set(null); @@ -217,6 +253,7 @@ export class LessonComponent implements OnInit { if (nextId) { this.currentTaskId.set(nextId); + this.activeTaskId.set(nextId); this.success.set(false); this.answerBody.set(null); } else { 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 d393effe1..52b006396 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 @@ -3,8 +3,13 @@ .exclude { display: flex; - flex-direction: row; - gap: 20px; + flex-direction: column; + gap: 12px; + + @include responsive.apply-desktop { + flex-direction: row; + gap: 20px; + } // &--hasContent { // display: flex; @@ -30,10 +35,14 @@ align-self: center; width: 100%; max-width: 395px; - height: 223px; + height: 185px; aspect-ratio: 16 / 9; border: 0; border-radius: var(--rounded-lg); + + @include responsive.apply-desktop { + height: 223px; + } } } } @@ -89,11 +98,15 @@ .read-more { align-items: start; align-self: self-start; - margin-top: 12px; + margin-top: 0; color: var(--accent); cursor: pointer; transition: color 0.2s; + @include responsive.apply-desktop { + margin-top: 12px; + } + &:hover { color: var(--accent-dark); } @@ -127,14 +140,26 @@ } &__content { - max-width: 350px; + max-width: 100%; + + @include responsive.apply-desktop { + max-width: 350px; + } &--hasContent { - min-width: 333px; + max-width: 100%; + + @include responsive.apply-desktop { + min-width: 333px; + } } &:not(&--hasContent) { - max-width: 333px; + max-width: 100%; + + @include responsive.apply-desktop { + max-width: 333px; + } } } 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 4b6a23d3b..251dea3b3 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 @@ -3,10 +3,13 @@ .file-task { display: flex; - flex-direction: row; - gap: 20px; + flex-direction: column; + gap: 12px; @include responsive.apply-desktop { + flex-direction: row; + gap: 20px; + &--hasVideo { flex-direction: row; } @@ -14,11 +17,16 @@ &__header, &__content { - min-height: 358px; - padding: 24px; + min-height: 0; + padding: 12px; background: var(--light-white); border: 0.5px solid var(--medium-grey-for-outline); border-radius: var(--rounded-lg); + + @include responsive.apply-desktop { + min-height: 358px; + padding: 24px; + } } &__header { @@ -27,18 +35,30 @@ &__content { min-width: 333px; - max-width: 350px; - padding: 12px 24px; + max-width: 100%; + padding: 12px; + + @include responsive.apply-desktop { + max-width: 350px; + } ::ng-deep { app-upload-file { .control { - max-height: 83px; + max-height: 61px; border-radius: var(--rounded-lg); + + @include responsive.apply-desktop { + max-height: 83px; + } } } } + @include responsive.apply-desktop { + padding: 12px 24px; + } + &--success { border-color: var(--green); } @@ -106,10 +126,14 @@ align-self: center; width: 100%; max-width: 395px; - height: 223px; + height: 185px; aspect-ratio: 16 / 9; border: 0; border-radius: var(--rounded-lg); + + @include responsive.apply-desktop { + height: 223px; + } } } @@ -157,11 +181,15 @@ .read-more { align-items: start; align-self: self-start; - margin-top: 12px; + margin-top: 0; color: var(--accent); cursor: pointer; transition: color 0.2s; + @include responsive.apply-desktop { + margin-top: 12px; + } + &:hover { color: var(--accent-dark); } 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 a0a4f8dac..c750a6209 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 @@ -2,8 +2,11 @@ @use "styles/responsive"; .radio { + display: flex; + flex-direction: column; + gap: 12px; + @include responsive.apply-desktop { - display: flex; flex-direction: row; gap: 20px; @@ -35,11 +38,15 @@ .read-more { align-items: start; align-self: self-start; - margin-top: 12px; + margin-top: 0; color: var(--accent); cursor: pointer; transition: color 0.2s; + @include responsive.apply-desktop { + margin-top: 12px; + } + &:hover { color: var(--accent-dark); } @@ -140,23 +147,39 @@ align-self: center; width: 100%; max-width: 395px; - height: 223px; + height: 185px; aspect-ratio: 16 / 9; border: 0; border-radius: var(--rounded-lg); + + @include responsive.apply-desktop { + height: 223px; + } } } } &__content { - max-width: 350px; + max-width: 100%; + + @include responsive.apply-desktop { + max-width: 350px; + } &--hasContent { - min-width: 333px; + max-width: 100%; + + @include responsive.apply-desktop { + min-width: 333px; + } } &:not(&--hasContent) { - max-width: 333px; + max-width: 100%; + + @include responsive.apply-desktop { + max-width: 333px; + } } } diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.html b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.html deleted file mode 100644 index f812a3c8f..000000000 --- a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.html +++ /dev/null @@ -1,57 +0,0 @@ - - -
-

{{ data.text }}

-
-
- - -
- @for (_ of Array(data.connectLeft.length); track $index) { -
- @if (data.connectLeft[$index].text) { - {{ data.connectLeft[$index].text }} - } @else if (data.connectLeft[$index].file) { - - } -
- } -
- -
- @for (_ of Array(data.connectRight.length); track $index) { -
- @if (data.connectRight[$index].text) { - {{ data.connectRight[$index].text }} - } @else if (data.connectRight[$index].file) { - - } -
- } -
-
- @if (hint.length) { -
- } -
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.scss b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.scss deleted file mode 100644 index 16477a974..000000000 --- a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.scss +++ /dev/null @@ -1,92 +0,0 @@ -@use "styles/typography"; -@use "styles/responsive"; - -.relations { - padding: 26px; - background-color: var(--white); - border: 1px solid var(--grey-button); - border-radius: 15px; - - &__title { - margin-bottom: 20px; - color: var(--black); - - @include typography.heading-4; - - @include responsive.apply-desktop { - margin-bottom: 26px; - - @include typography.heading-3; - } - } - - &__description { - margin-bottom: 20px; - white-space: pre-line; - - @include typography.body-14; - } - - &__wrapper { - display: flex; - flex-direction: column; - gap: 50px; - justify-content: space-between; - - @include responsive.apply-desktop { - flex-direction: row; - } - } - - &__column { - display: flex; - flex-direction: column; - flex-grow: 1; - gap: 10px; - min-width: 220px; - - &--grid { - display: grid; - - @include responsive.apply-desktop { - grid-template-columns: repeat(2, 1fr); - } - } - } - - &__item { - max-height: 500px; - padding: 10px; - overflow: hidden; - cursor: pointer; - border: 1px solid var(--grey-button); - border-radius: 10px; - - @include typography.body-14; - - @include responsive.apply-desktop { - @include typography.body-12; - } - - &--active { - border-color: var(--accent); - } - - &--success { - color: var(--green); - background-color: var(--light-green); - border-color: var(--green); - } - - &--disabled { - pointer-events: none; - opacity: 0.3; - } - } - - :host ::ng-deep &__hint p { - margin-top: 20px; - - @include typography.body-14; - } -} diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.spec.ts b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.spec.ts deleted file mode 100644 index e2cfaff43..000000000 --- a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** @format */ - -import { ComponentFixture, TestBed } from "@angular/core/testing"; - -import { RelationsTaskComponent } from "./relations-task.component"; - -describe("RelationsTaskComponent", () => { - let component: RelationsTaskComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [RelationsTaskComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(RelationsTaskComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.ts b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.ts deleted file mode 100644 index de13975f9..000000000 --- a/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** @format */ - -import { - Component, - type OnInit, - type AfterViewInit, - type OnDestroy, - ElementRef, - ViewChild, - ViewChildren, - type QueryList, - Input, - Output, - EventEmitter, - signal, - computed, - inject, -} from "@angular/core"; -import { CommonModule } from "@angular/common"; -import { DomSanitizer } from "@angular/platform-browser"; -import { fromEvent, type Subscription } from "rxjs"; -import { debounceTime } from "rxjs/operators"; -import { ParseBreaksPipe, ParseLinksPipe } from "@corelib"; -import { - ConnectQuestion, - ConnectQuestionRequest, - ConnectQuestionResponse, -} from "projects/skills/src/models/step.model"; - -/** - * Компонент задачи на установление связей - * Позволяет пользователю соединять элементы из левого столбца с элементами правого столбца - * - * Входные параметры: - * @Input data - данные вопроса типа ConnectQuestion - * @Input hint - текст подсказки - * @Input success - флаг успешного выполнения - * @Input error - объект ошибки для сброса состояния - * - * Выходные события: - * @Output update - событие обновления с массивом связей - * - * Функциональность: - * - Отображает два столбца элементов для соединения - * - Рисует SVG линии между связанными элементами - * - Поддерживает текстовые и графические элементы - * - Автоматически перерисовывает линии при изменении размера окна - */ -@Component({ - selector: "app-relations-task", - standalone: true, - imports: [CommonModule, ParseBreaksPipe, ParseLinksPipe], - templateUrl: "./relations-task.component.html", - styleUrls: ["./relations-task.component.scss"], -}) -export class RelationsTaskComponent implements OnInit, AfterViewInit, OnDestroy { - @Input({ required: true }) data!: ConnectQuestion; // Данные вопроса - @Input() hint!: string; // Текст подсказки - @Input() success = false; // Флаг успешного выполнения - - // Сеттер для обработки ошибок и сброса состояния - @Input() - set error(error: ConnectQuestionResponse | null) { - this._error.set(error); - - if (error !== null) { - this.result.set([]); // Сбрасываем результат при ошибке - this.selectedLeftId.set(null); // Сбрасываем выбранный элемент - } - } - - get error() { - return this._error(); - } - - protected readonly Array = Array; - - _error = signal(null); - - @Output() update = new EventEmitter(); // Событие обновления связей - - // Ссылки на DOM элементы - @ViewChild("svgOverlay", { static: true }) svgOverlay!: ElementRef; - @ViewChildren("leftItem", { read: ElementRef }) leftItems!: QueryList>; - @ViewChildren("rightItem", { read: ElementRef }) rightItems!: QueryList>; - - private resizeSub!: Subscription; // Подписка на изменение размера окна - - // Состояние компонента - result = signal([]); // Массив установленных связей - resultLeft = computed(() => this.result().map(r => r.leftId)); // ID связанных левых элементов - resultRight = computed(() => this.result().map(r => r.rightId)); // ID связанных правых элементов - selectedLeftId = signal(null); // ID выбранного левого элемента - - description!: any; // Обработанное описание - sanitizer = inject(DomSanitizer); - - // Проверяет, является ли правый столбец сеткой изображений - get isImageGrid() { - return this.data.connectRight.every(itm => !!itm.file); - } - - ngOnInit() { - // Безопасно обрабатываем HTML в описании - this.description = this.sanitizer.bypassSecurityTrustHtml(this.data.description); - } - - ngAfterViewInit() { - // Подписываемся на изменение размера окна для перерисовки линий - this.resizeSub = fromEvent(window, "resize") - .pipe(debounceTime(100)) - .subscribe(() => this.drawLines()); - - // Рисуем линии после инициализации представления - setTimeout(() => this.drawLines()); - } - - ngOnDestroy() { - this.resizeSub.unsubscribe(); - } - - /** - * Обработчик выбора элемента из левого столбца - * @param id - ID выбранного элемента - */ - onSelectLeft(id: number) { - const current = this.selectedLeftId(); - - // Если элемент уже выбран, снимаем выбор - if (current === id) { - this.selectedLeftId.set(null); - return; - } - - // Если элемент уже связан, удаляем связь - const existingIndex = this.result().findIndex(r => r.leftId === id); - if (existingIndex !== -1) { - this.result.update(r => r.filter((_, i) => i !== existingIndex)); - this.drawLines(); - this.update.emit(this.result()); - } - - this.selectedLeftId.set(id); - } - - /** - * Обработчик выбора элемента из правого столбца - * Создает связь между выбранным левым и правым элементами - * @param id - ID выбранного правого элемента - */ - onSelectRight(id: number) { - const leftId = this.selectedLeftId(); - if (leftId === null) return; - - // Удаляем существующие связи для этих элементов - let newResult = this.result().filter(r => r.leftId !== leftId && r.rightId !== id); - - // Добавляем новую связь - newResult = [...newResult, { leftId, rightId: id }]; - - this.result.set(newResult); - this.selectedLeftId.set(null); - - this.drawLines(); - this.update.emit(this.result()); - } - - /** - * Удаляет все SVG линии - */ - removeLines() { - const svgEl = this.svgOverlay.nativeElement; - while (svgEl.firstChild) { - svgEl.removeChild(svgEl.firstChild); - } - } - - /** - * Рисует SVG линии между связанными элементами - * Вычисляет позиции элементов и создает линии между ними - */ - private drawLines() { - this.removeLines(); - - const svgEl = this.svgOverlay.nativeElement; - const svgRect = svgEl.getBoundingClientRect(); - - // Получаем позиции левых элементов - const leftPositions = new Map(); - this.leftItems.forEach(el => { - const id = Number(el.nativeElement.dataset["id"]); - leftPositions.set(id, el.nativeElement.getBoundingClientRect()); - }); - - // Получаем позиции правых элементов - const rightPositions = new Map(); - this.rightItems.forEach(el => { - const id = Number(el.nativeElement.dataset["id"]); - rightPositions.set(id, el.nativeElement.getBoundingClientRect()); - }); - - // Рисуем линии для каждой связи - this.result().forEach(pair => { - const leftRect = leftPositions.get(pair.leftId); - const rightRect = rightPositions.get(pair.rightId); - - if (!leftRect || !rightRect) return; - - // Вычисляем координаты линии - const x1 = leftRect.right - svgRect.left; - const y1 = leftRect.top + leftRect.height / 2 - svgRect.top; - const x2 = rightRect.left - svgRect.left; - const y2 = rightRect.top + rightRect.height / 2 - svgRect.top; - - // Создаем SVG линию - const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); - line.setAttribute("x1", x1.toString()); - line.setAttribute("y1", y1.toString()); - line.setAttribute("x2", x2.toString()); - line.setAttribute("y2", y2.toString()); - line.setAttribute("stroke", "#6B46C1"); - line.setAttribute("stroke-width", "4"); - line.setAttribute("stroke-linecap", "round"); - line.setAttribute("stroke-linejoin", "round"); - line.setAttribute("class", "connection-line"); - - svgEl.appendChild(line); - }); - } -} 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 8db8a6463..3941bc493 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 @@ -6,26 +6,39 @@ &__body { display: flex; flex-direction: row; - gap: 24px; + gap: 12px; min-height: 358px; - padding: 24px; + padding: 12px; background-color: var(--light-white); border: 0.5px solid var(--medium-grey-for-outline); border-radius: var(--rounded-lg); + @include responsive.apply-desktop { + gap: 24px; + padding: 24px; + } + iframe { display: block; align-self: center; width: 100%; max-width: 395px; - height: 223px; + height: 185px; aspect-ratio: 16 / 9; border: 0; border-radius: var(--rounded-lg); + + @include responsive.apply-desktop { + height: 223px; + } } &--hasImage { - flex-direction: row-reverse; + flex-direction: column; + + @include responsive.apply-desktop { + flex-direction: row-reverse; + } } &--hasVideo { @@ -39,13 +52,21 @@ width: 100%; &--hasVideo { - flex-direction: row; + flex-direction: column; flex-grow: 1; + + @include responsive.apply-desktop { + flex-direction: row; + } } &--hasImage { - flex-direction: row-reverse; + flex-direction: column-reverse; flex-grow: 1; + + @include responsive.apply-desktop { + flex-direction: row-reverse; + } } } @@ -77,11 +98,15 @@ .read-more { align-items: start; align-self: self-start; - margin-top: 12px; + margin-top: -14px; color: var(--accent); cursor: pointer; transition: color 0.2s; + @include responsive.apply-desktop { + margin-top: 12px; + } + &:hover { color: var(--accent-dark); } diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.html b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.html index d89c6bdb6..b30d261f2 100644 --- a/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.html +++ b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.html @@ -52,7 +52,7 @@

ответ