- @if (userTrajectory()?.mentorFirstName && userTrajectory()?.mentorLastName) {
-
-
-
-
- {{ userTrajectory()?.mentorFirstName }} {{ userTrajectory()?.mentorLastName }}
-
-
- 2 года опыта преподавания
-
-
+ @if (userTrajectory()?.availableSkills; as availableSkills) {
+
+ @for (skill of availableSkills; track skill.id) {
+
+ }
-
- Написать наставнику
-
- } @else {
-
- Скоро тут будет наставник!
-
}
-
+
-
- @if (trajectory.description) { @if (desktopMode$ | async; as desktopMode) {
-
- } @else {
-
-
- @if (descriptionExpandable) {
-
- {{ readFullDescription ? "Скрыть" : "Читать полностью" }}
+
+
+
+
о курсе
+
+
+ @if (trajectory.description) {
+
+
+ @if (descriptionExpandable) {
+
+ {{ readFullDescription ? "cкрыть" : "подробнее" }}
+
+ }
}
- } }
-
-
-
-
-
- Стартовая встреча с наставником
-
-
-
-
-
-
- Финальная встреча с наставником
-
-
-
-
-
-
-
-
-
-
Персональные навыки
-
- @if (userTrajectory()?.individualSkills) { @for(skill of userTrajectory()?.individualSkills;
- track $index) {
-
- } }
+
-
-
Навыки текущего месяца
-
- @if (userTrajectory()?.availableSkills) { @for(skill of availableSkills(); track $index) {
-
- } } @if (completeAllMainSkills()) {
-
- }
-
-
+
+
+
ты прошел модуль!
-
-
Грядущие навыки
-
- @if (userTrajectory()?.unavailableSkills) { @for(skill of
- userTrajectory()?.unavailableSkills; track $index) {
-
- } }
-
-
+
-
-
Пройденные навыки
-
- @if (userTrajectory()?.completedSkills) { @for (skill of completedSkills(); track $index) {
-
- } }
-
+
отлично
-
-
+
+
+}
diff --git a/projects/skills/src/app/trajectories/track-career/detail/info/info.component.scss b/projects/skills/src/app/trajectories/track-career/detail/info/info.component.scss
index b175f6830..14cf4cbf8 100644
--- a/projects/skills/src/app/trajectories/track-career/detail/info/info.component.scss
+++ b/projects/skills/src/app/trajectories/track-career/detail/info/info.component.scss
@@ -1,55 +1,117 @@
@use "styles/responsive";
@use "styles/typography";
-@mixin expandable-list {
- &__remaining {
+.trajectory {
+ padding-top: 12px;
+ padding-bottom: 100px;
+
+ @include responsive.apply-desktop {
+ padding-bottom: 0;
+ }
+
+ &__main {
display: grid;
- grid-template-rows: 0fr;
- overflow: hidden;
- transition: all 0.5s ease-in-out;
+ grid-template-columns: 1fr;
+ }
- &--show {
- grid-template-rows: 1fr;
- margin-top: 12px;
- }
+ &__right {
+ display: flex;
+ flex-direction: column;
+ max-height: 226px;
+ }
- ul {
- min-height: 0;
+ &__left {
+ max-width: 157px;
+ }
+
+ &__section {
+ padding: 24px;
+ margin-bottom: 14px;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ }
+
+ &__info {
+ @include responsive.apply-desktop {
+ grid-column: span 3;
}
+ }
- li {
- &:first-child {
- margin-top: 12px;
- }
+ &__details {
+ display: grid;
+ grid-template-columns: 2fr 5fr 3fr;
+ grid-gap: 20px;
+ }
- &:not(:last-child) {
- margin-bottom: 12px;
+ &__progress {
+ position: relative;
+ height: 48px;
+ padding: 10px;
+ text-align: center;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+
+ &--cover {
+ position: absolute;
+ top: 0%;
+ left: 0%;
+ height: 48px;
+ background-color: var(--accent);
+ border-radius: var(--rounded-lg);
+ opacity: 0.15;
+
+ &--complete {
+ background-color: var(--green-dark);
}
}
- }
-}
-.read-more {
- margin-top: 12px;
- color: var(--accent);
- cursor: pointer;
- transition: color 0.2s;
+ &--percent {
+ color: var(--green-dark);
+ }
- &:hover {
- color: var(--accent-dark);
+ &--complete {
+ color: var(--black);
+ }
}
- @include typography.body-14;
+ &__skills {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-top: 18px;
+ }
}
.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(--dark-grey);
+ color: var(--black);
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
@@ -72,273 +134,40 @@
}
}
}
-
/* stylelint-enable value-no-vendor-prefix */
&__read-full {
- margin-top: 2px;
+ margin-top: 8px;
color: var(--accent);
cursor: pointer;
}
}
-.trajectory__infoItem {
- @include responsive.apply-desktop {
- margin-left: 50px;
- }
-
- .trajectory {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
-
- @include responsive.apply-desktop {
- flex-direction: row;
- }
-
- &__cover {
- display: flex;
- flex-basis: 200%;
- flex-direction: column;
- gap: 30px;
- align-items: center;
- width: 100%;
-
- img {
- width: 150px;
- height: 130px;
- }
-
- @include responsive.apply-desktop {
- align-items: flex-start;
-
- img {
- width: 275px;
- height: 275px;
- }
- }
- }
-
- &__image {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- object-fit: cover;
- }
-
- &__title {
- @include typography.heading-4;
-
- width: 100%;
-
- @include responsive.apply-desktop {
- @include typography.heading-2;
- }
- }
-
- &__info {
- display: flex;
- flex-basis: 270%;
- flex-direction: column;
- gap: 10px;
- }
-
- &__info--additional {
- display: none;
- flex-direction: column;
- gap: 12px;
- align-items: center;
- justify-content: space-between;
- padding: 10px 25px;
- border: 1px solid var(--grey-button);
- border-radius: 15px;
-
- @include responsive.apply-desktop {
- flex-direction: row;
- }
- }
-
- &__timeline {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
- padding: 10px 25px;
- background-color: var(--white);
- border: 1px solid var(--grey-button);
- border-radius: 15px;
-
- ::ng-deep app-month-block {
- display: block;
- width: 100%;
-
- .month {
- display: flex;
- justify-content: center;
- }
-
- .item {
- max-width: 65px;
- height: 20px;
-
- &__name {
- top: -40%;
- left: 40%;
- font-size: 10px;
- font-weight: 600;
-
- @include responsive.apply-desktop {
- top: -50%;
- left: 44%;
-
- @include typography.body-14;
- }
- }
-
- @include responsive.apply-desktop {
- max-width: 120px;
- }
- }
- }
-
- @include responsive.apply-desktop {
- flex-direction: row;
- }
- }
-
- &__mentor {
- display: none;
- flex-direction: column;
- gap: 20px;
- align-items: flex-start;
- justify-content: space-between;
- padding: 10px 25px;
- border: 1px solid var(--grey-button);
- border-radius: 15px;
-
- @include responsive.apply-desktop {
- flex-direction: row;
- gap: 12px;
- align-items: center;
- }
- }
-
- &__description {
- height: auto;
- padding: 18px 25px;
- overflow-y: auto;
- border: 1px solid var(--grey-button);
- border-radius: 15px;
+.read-more {
+ margin-top: 8px;
+ color: var(--accent);
+ cursor: pointer;
+ transition: background-color 0.2s;
- @include responsive.apply-desktop {
- height: 235px;
- overflow-y: auto;
- }
- }
+ &:hover {
+ color: var(--accent-dark);
}
+}
- .timeline {
- &__images {
- display: flex;
- flex-direction: row;
- gap: 20px;
- align-items: center;
- }
-
- &__date {
- color: var(--dark-grey);
-
- @include responsive.apply-desktop {
- align-self: center !important;
- }
- }
-
- &__start {
- align-self: baseline;
- margin-top: 10px;
- color: var(--dark-grey);
-
- @include responsive.apply-desktop {
- margin: 0;
- }
- }
-
- &__end {
- align-self: end;
- margin-bottom: 10px;
- color: var(--dark-grey);
-
- @include responsive.apply-desktop {
- margin: 0;
- }
- }
- }
+.cancel {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ max-height: calc(100vh - 40px);
+ padding: 0 200px;
- .info__additional {
- display: flex;
- gap: 10px;
- align-items: center;
- padding: 5px 24px;
+ &__text {
+ color: var(--dark-grey);
text-align: center;
- border-radius: 15px;
-
- p {
- width: 100%;
- }
}
- .mentor {
- &__years {
- color: var(--dark-grey);
- }
-
- &__info {
- display: flex;
- gap: 12px;
- align-items: center;
- }
- }
-
- .skills {
- display: flex;
- flex-direction: column;
- gap: 20px;
- margin-top: 10px;
-
- &__future-container,
- &__personal-container,
- &__past-container {
- display: none;
- }
-
- &__now-container {
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-top: 20px;
-
- h4 {
- display: none;
- }
- }
-
- &__now,
- &__past,
- &__personal,
- &__future {
- display: grid;
- grid-template-columns: 1fr;
- gap: 18px;
-
- @include responsive.apply-desktop {
- grid-template-columns: repeat(3, 1fr);
- }
- }
-
- &__complete {
- width: 100%;
- height: 100%;
- }
+ &__img {
+ margin: 30px 0;
}
}
diff --git a/projects/skills/src/app/trajectories/track-career/detail/info/info.component.ts b/projects/skills/src/app/trajectories/track-career/detail/info/info.component.ts
index 8adbd1558..ff0ae407b 100644
--- a/projects/skills/src/app/trajectories/track-career/detail/info/info.component.ts
+++ b/projects/skills/src/app/trajectories/track-career/detail/info/info.component.ts
@@ -4,7 +4,6 @@ import {
type AfterViewInit,
ChangeDetectorRef,
Component,
- computed,
type ElementRef,
inject,
type OnInit,
@@ -13,19 +12,17 @@ import {
} from "@angular/core";
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { ParseBreaksPipe, ParseLinksPipe } from "@corelib";
-import { ButtonComponent } from "@ui/components";
-import { AvatarComponent, IconComponent } from "@uilib";
+import { IconComponent } from "@uilib";
import { expandElement } from "@utils/expand-element";
import { map, type Observable, type Subscription } from "rxjs";
-import { SkillCardComponent } from "../../../../skills/shared/skill-card/skill-card.component";
import { CommonModule } from "@angular/common";
-import { MonthBlockComponent } from "projects/skills/src/app/profile/shared/month-block/month-block.component";
import type { Trajectory, UserTrajectory } from "projects/skills/src/models/trajectory.model";
import { TrajectoriesService } from "../../../trajectories.service";
-import type { Month, UserData } from "projects/skills/src/models/profile.model";
-import { ProfileService } from "projects/skills/src/app/profile/services/profile.service";
-import { SkillService } from "projects/skills/src/app/skills/services/skill.service";
import { BreakpointObserver } from "@angular/cdk/layout";
+import { SoonCardComponent } from "@office/shared/soon-card/soon-card.component";
+import { SkillCardComponent } from "projects/skills/src/app/shared/skill-card/skill-card.component";
+import { ModalComponent } from "@ui/components/modal/modal.component";
+import { ButtonComponent } from "@ui/components";
/**
* Компонент детальной информации о траектории
@@ -42,13 +39,14 @@ import { BreakpointObserver } from "@angular/cdk/layout";
standalone: true,
imports: [
IconComponent,
- ButtonComponent,
RouterModule,
ParseBreaksPipe,
ParseLinksPipe,
- SkillCardComponent,
- AvatarComponent,
CommonModule,
+ SoonCardComponent,
+ SkillCardComponent,
+ ModalComponent,
+ ButtonComponent,
],
templateUrl: "./info.component.html",
styleUrl: "./info.component.scss",
@@ -60,29 +58,14 @@ export class TrajectoryInfoComponent implements OnInit, AfterViewInit {
cdRef = inject(ChangeDetectorRef);
trajectoryService = inject(TrajectoriesService);
- profileService = inject(ProfileService);
- skillService = inject(SkillService);
breakpointObserver = inject(BreakpointObserver);
subscriptions$: Subscription[] = [];
trajectory!: Trajectory;
userTrajectory = signal
(null);
- profileId!: number;
-
- // Вычисляемые свойства для состояния навыков
- completeAllMainSkills = computed(
- () => this.userTrajectory()?.availableSkills.every(skill => skill.isDone) ?? false
- );
-
- availableSkills = computed(
- () => this.userTrajectory()?.availableSkills.filter(s => !s.isDone) ?? []
- );
- completedSkills = computed(() => [
- ...(this.userTrajectory()?.completedSkills ?? []),
- ...(this.userTrajectory()?.availableSkills.filter(s => s.isDone) ?? []),
- ]);
+ isCompleteModule = signal(false);
@ViewChild("descEl") descEl?: ElementRef;
@@ -95,44 +78,17 @@ export class TrajectoryInfoComponent implements OnInit, AfterViewInit {
* Загружает данные траектории, пользовательскую информацию и настраивает навыки
*/
ngOnInit(): void {
- this.desktopMode$.subscribe(r => console.log(r));
+ this.desktopMode$.subscribe(_ => {});
- this.route.data.pipe(map(r => r["data"])).subscribe(r => {
+ this.route.parent?.data.pipe(map(r => r["data"])).subscribe(r => {
this.trajectory = r[0];
this.userTrajectory.set({ ...r[1], individualSkills: r[2] });
-
- // Настройка доступности навыков
- this.userTrajectory()?.availableSkills.forEach(i => (i.freeAccess = true));
- this.userTrajectory()?.completedSkills.forEach(i => {
- i.freeAccess = true;
- i.isDone = true;
- });
- this.userTrajectory()?.unavailableSkills.forEach(i => (i.freeAccess = true));
- this.userTrajectory()?.individualSkills.forEach(i => (i.freeAccess = true));
- });
-
- this.profileService.getUserData().subscribe((r: UserData) => {
- this.profileId = r.id;
- });
-
- // Создание макета месяцев для временной шкалы
- this.mockMonts = Array.from({ length: this.userTrajectory()!.durationMonths }, (_, index) => {
- const monthNumber = index + 1;
-
- return {
- month: `${monthNumber} месяц`,
- successfullyDone: monthNumber <= this.userTrajectory()!.activeMonth,
- };
+ this.isCompleteModule.set(this.userTrajectory()!.completedSkills.some(skill => skill.isDone));
});
}
- mockMonts: Month[] = [];
-
- placeholderUrl =
- "https://uch-ibadan.org.ng/wp-content/uploads/2021/10/Profile_avatar_placeholder_large.png";
-
- readFullDescription = false;
descriptionExpandable?: boolean;
+ readFullDescription!: boolean;
/**
* Проверка возможности расширения описания после инициализации представления
@@ -168,7 +124,6 @@ export class TrajectoryInfoComponent implements OnInit, AfterViewInit {
* @param skillId - ID выбранного навыка
*/
onSkillClick(skillId: number) {
- this.skillService.setSkillId(skillId);
this.router.navigate(["skills", skillId]);
}
}
diff --git a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.html b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.html
index ce09c1587..8c3bec611 100644
--- a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.html
+++ b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.html
@@ -1,14 +1,46 @@
+
+ @if(trajectory()) {
+
+
+
+
+
+ @if (!isTaskDetail()) {
+
{{ trajectory()?.name }}
+ }
+
+
+
+
+
+
{{ isTaskDetail() ? "назад к модулю?" : "назад" }}
-
+
вернуться в программу
+
+
+
+
+ }
+
+
diff --git a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.scss b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.scss
index e69de29bb..ebab1169d 100644
--- a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.scss
+++ b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.scss
@@ -0,0 +1,102 @@
+@use "styles/responsive";
+@use "styles/typography";
+
+$detail-bar-height: 63px;
+$detail-bar-mb: 12px;
+
+.detail {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ max-height: 100%;
+ padding-top: 20px;
+
+ &__body {
+ flex-grow: 1;
+ max-height: calc(100% - #{$detail-bar-height} - #{$detail-bar-mb});
+ padding-bottom: 12px;
+ }
+}
+
+.info {
+ $body-slide: 15px;
+
+ position: relative;
+ padding: 0;
+ background-color: transparent;
+ border: none;
+ border-radius: $body-slide;
+
+ &__cover {
+ position: relative;
+ height: 136px;
+ background: linear-gradient(var(--accent), var(--lime));
+ border-radius: 15px 15px 0 0;
+ }
+
+ &__body {
+ position: relative;
+ z-index: 2;
+ }
+
+ &__avatar {
+ position: absolute;
+ bottom: -10px;
+ left: 50%;
+ z-index: 100;
+ display: block;
+ cursor: pointer;
+ background-color: var(--white);
+ border-radius: 50%;
+
+ &--program {
+ bottom: 15px;
+ }
+
+ @include responsive.apply-desktop {
+ transform: translate(-50%, 50%);
+ }
+ }
+
+ &__row {
+ display: flex;
+ gap: 20px;
+ align-items: center;
+ justify-content: center;
+ margin-top: 2px;
+
+ @include responsive.apply-desktop {
+ justify-content: unset;
+ margin-top: 0;
+ }
+ }
+
+ &__title {
+ margin-top: 10px;
+ overflow: hidden;
+ color: var(--black);
+ text-align: center;
+ text-overflow: ellipsis;
+
+ &--project {
+ transform: translateX(-31%);
+ }
+ }
+
+ &__text {
+ color: var(--dark-grey);
+ }
+
+ &__actions {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 180px;
+ align-items: center;
+ padding: 24px 0 30px;
+
+ &--disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+ }
+}
diff --git a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.ts b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.ts
index 2b33ff185..264bd1cf8 100644
--- a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.ts
+++ b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.component.ts
@@ -1,11 +1,14 @@
/** @format */
import { CommonModule } from "@angular/common";
-import { Component, inject, type OnDestroy, type OnInit } from "@angular/core";
-import { ActivatedRoute, RouterOutlet } from "@angular/router";
-import { BarComponent } from "@ui/components";
+import { Component, DestroyRef, inject, signal, type OnDestroy, type OnInit } from "@angular/core";
+import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from "@angular/router";
import type { Trajectory } from "projects/skills/src/models/trajectory.model";
-import { map, type Subscription } from "rxjs";
+import { filter, map, type Subscription } from "rxjs";
+import { AvatarComponent } from "@ui/components/avatar/avatar.component";
+import { ButtonComponent } from "@ui/components";
+import { ModalComponent } from "@ui/components/modal/modal.component";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
/**
* Компонент детального просмотра траектории
@@ -15,33 +18,45 @@ import { map, type Subscription } from "rxjs";
@Component({
selector: "app-trajectory-detail",
standalone: true,
- imports: [CommonModule, BarComponent, RouterOutlet],
+ imports: [CommonModule, RouterOutlet, AvatarComponent, ButtonComponent, ModalComponent],
templateUrl: "./trajectory-detail.component.html",
styleUrl: "./trajectory-detail.component.scss",
})
export class TrajectoryDetailComponent implements OnInit, OnDestroy {
route = inject(ActivatedRoute);
+ router = inject(Router);
+ destroyRef = inject(DestroyRef);
subscriptions$: Subscription[] = [];
- trajectory?: Trajectory;
- trackId?: string;
+ trajectory = signal
(undefined);
+ isDisabled = signal(false);
+ isTaskDetail = signal(false);
/**
* Инициализация компонента
* Подписывается на параметры маршрута и данные траектории
*/
ngOnInit(): void {
- const trackIdSub = this.route.params.subscribe(params => {
- this.trackId = params["trackId"];
- });
+ this.route.data
+ .pipe(
+ map(data => data["data"]),
+ filter(trajectory => !!trajectory)
+ )
+ .subscribe({
+ next: trajectory => {
+ this.trajectory.set(trajectory[0]);
+ },
+ });
- const trajectorySub$ = this.route.data.pipe(map(r => r["data"])).subscribe(trajectory => {
- this.trajectory = trajectory;
- });
-
- trajectorySub$ && this.subscriptions$.push(trajectorySub$);
- trackIdSub && this.subscriptions$.push(trackIdSub);
+ this.router.events
+ .pipe(
+ filter((event): event is NavigationEnd => event instanceof NavigationEnd),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => {
+ this.isTaskDetail.set(this.router.url.includes("task"));
+ });
}
/**
@@ -51,4 +66,15 @@ export class TrajectoryDetailComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.subscriptions$.forEach($ => $.unsubscribe());
}
+
+ /**
+ * Перенаправляет на страницу с информацией в завивисимости от listType
+ */
+ redirectDetailInfo(trackId?: number): void {
+ if (this.trajectory()) {
+ this.router.navigateByUrl(`/trackCar/${trackId}`);
+ } else {
+ this.router.navigateByUrl("/trackCar/all");
+ }
+ }
}
diff --git a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.routes.ts b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.routes.ts
index 10715ba00..fed430607 100644
--- a/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.routes.ts
+++ b/projects/skills/src/app/trajectories/track-career/detail/trajectory-detail.routes.ts
@@ -10,14 +10,17 @@ export const TRAJECTORY_DETAIL_ROUTES = [
path: "",
component: TrajectoryDetailComponent,
canActivate: [TrajectoryInfoRequiredGuard],
-
+ resolve: {
+ data: TrajectoryDetailResolver,
+ },
children: [
{
path: "",
component: TrajectoryInfoComponent,
- resolve: {
- data: TrajectoryDetailResolver,
- },
+ },
+ {
+ path: "task",
+ loadChildren: () => import("../../../task/task.routes").then(m => m.TASK_ROUTES),
},
],
},
diff --git a/projects/skills/src/app/trajectories/track-career/list/list.component.html b/projects/skills/src/app/trajectories/track-career/list/list.component.html
index ffcd70f9b..7d4222623 100644
--- a/projects/skills/src/app/trajectories/track-career/list/list.component.html
+++ b/projects/skills/src/app/trajectories/track-career/list/list.component.html
@@ -1,10 +1,13 @@
-
- @if (hasItems()) { @if (type() === 'all') { @for (trajectory of trajectoriesList().slice(0, 2);
- track $index) {
-
- } } @else if (type() === 'my') {
-
- } }
+
+ @if (trajectoriesList().length) {
+
+ @for (trajectory of trajectoriesList(); track trajectory.id) {
+
+
+
+ }
+
+ }
diff --git a/projects/skills/src/app/trajectories/track-career/list/list.component.scss b/projects/skills/src/app/trajectories/track-career/list/list.component.scss
index 7987768d9..ef3eb00e7 100644
--- a/projects/skills/src/app/trajectories/track-career/list/list.component.scss
+++ b/projects/skills/src/app/trajectories/track-career/list/list.component.scss
@@ -1,6 +1,9 @@
-.list {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-bottom: 20px;
+.trajectories {
+ margin-top: 20px;
+
+ &__list {
+ display: grid;
+ grid-template-columns: 4fr 4fr;
+ grid-gap: 20px;
+ }
}
diff --git a/projects/skills/src/app/trajectories/track-career/list/list.component.ts b/projects/skills/src/app/trajectories/track-career/list/list.component.ts
index b628438d0..5c1de0690 100644
--- a/projects/skills/src/app/trajectories/track-career/list/list.component.ts
+++ b/projects/skills/src/app/trajectories/track-career/list/list.component.ts
@@ -4,7 +4,6 @@ import {
type AfterViewInit,
ChangeDetectorRef,
Component,
- computed,
inject,
type OnDestroy,
type OnInit,
@@ -38,12 +37,9 @@ export class TrajectoriesListComponent implements OnInit, AfterViewInit, OnDestr
totalItemsCount = signal(0);
trajectoriesList = signal
([]);
- userTrajectory = signal(undefined);
trajectoriesPage = signal(1);
perFetchTake = signal(20);
- type = signal<"all" | "my" | null>(null);
-
subscriptions$ = signal([]);
/**
@@ -51,16 +47,9 @@ export class TrajectoriesListComponent implements OnInit, AfterViewInit, OnDestr
* Определяет тип списка (all/my) и загружает начальные данные
*/
ngOnInit(): void {
- const currentType = this.router.url.split("/").pop() as "all" | "my";
- this.type.set(currentType);
-
this.route.data.pipe(map(r => r["data"])).subscribe(data => {
- if (currentType === "all") {
- this.trajectoriesList.set(data as Trajectory[]);
- this.totalItemsCount.set((data as Trajectory[]).length);
- } else {
- this.userTrajectory.set(data as Trajectory);
- }
+ this.trajectoriesList.set(data as Trajectory[]);
+ this.totalItemsCount.set((data as Trajectory[]).length);
});
}
@@ -129,8 +118,4 @@ export class TrajectoriesListComponent implements OnInit, AfterViewInit, OnDestr
map(res => res)
);
}
-
- hasItems = computed(() => {
- return this.type() === "all" ? this.trajectoriesList().length > 0 : !!this.userTrajectory();
- });
}
diff --git a/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.html b/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.html
index 23d6f2e01..7ce78cd10 100644
--- a/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.html
+++ b/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.html
@@ -1,315 +1,49 @@
-@if (trajectory) {
-
-
-
-
-
-
-
{{ trajectory.name }}
-
- @if (trajectory.description) { @if (desktopMode$ | async; as desktopMode) {
-
- } @else {
-
-
- @if (descriptionExpandable) {
-
- {{ readFullDescription ? "Скрыть" : "Читать полностью" }}
-
- }
-
- } }
-
-
-
-
-
- {{ trajectory.durationMonths }}
- {{ trajectory.durationMonths | pluralize: ["месяц", "месяца", "месяцев"] }}
-
-
-
ряд 43, место А320
-
-
-
-
-
-
- Навыки которые ты прокачаешь
-
-
Посадочный талон
-
-
-
- @if(trajectory.skills.length > 0){ @for(skill of trajectory.skills; track $index){
-
-
- {{ skill.name }}
-
- } }
-
-
-
-
-
-
-
-
-
-} @else {
-
- У вас нет выбранной траектории!
-
-}
-
-
-
-
-
-
Доступно в подписке
-
- Эта программа — не просто обучение, а полноценный старт в карьере с поддержкой на
- каждом этапе. Вот что делает её уникальной:
-
-
-
- @for(advantage of trajectoryMore; track $index){
-
-
- {{ advantage.label }}
-
- }
-
-
-
-
-
-
+@if(trajectory) {
+
+
+
+
Назад
+ {{
+ isMember()
+ ? "для участников программы"
+ : isSubs()
+ ? "доступно по подписке"
+ : "доступно всем пользователям"
+ }}
+
-
-
-
-
-
-
-
Подтверждение
-
- Теперь у вас есть доступ ко всем возможностям платформы. Начните использовать прямо сейчас!
-
-
-
+
+
{{ trajectory.name | truncate: 50 }}
+
+ {{
+ isDates()
+ ? "16.02.2026 - 16.04.2026"
+ : isDate()
+ ? "16.02.2026"
+ : isEnded()
+ ? "курс завершен"
+ : "доступен до 16.04.2026"
+ }}
+
-
-
-
-
-
У вас нет активной подписки
-
-
-
- Чтобы получить доступ ко всем возможностям платформы, оформите подписку прямо сейчас!
-
-
-
-
+
+ @if (!isStarted()) {
+
+ } @else {
+
+ {{ isStarted() ? "начать" : "продолжить обучение" }}
+
+
+ }
-
-
-
-
-
У вас уже есть активная траектория!
-
-
-
-
-
-
-
-
-
-
-
-
-
Узнай больше
-
-
- Навыки открываются раз в месяц, за месяц тебе необходимо пройти путь навыков, которые
- отображаются в разделе "Навыки на месяц
-
-
-
- Здесь отображаются,которые буду ждать тебя дальше
-
-
-
- Здесь отображаются пройденные навыки
-
-
-
- Здесь отображаются пройденные навыки
-
-
-
-
-
-
- @for(dot of dotsArray(4); track $index){
-
- }
-
-
-
-
-
-
+
+}
diff --git a/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.scss b/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.scss
index ae0306472..513120114 100644
--- a/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.scss
+++ b/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.scss
@@ -1,488 +1,76 @@
@use "styles/responsive";
@use "styles/typography";
-@mixin expandable-list {
- &__remaining {
- display: grid;
- grid-template-rows: 0fr;
- overflow: hidden;
- transition: all 0.5s ease-in-out;
-
- &--show {
- grid-template-rows: 1fr;
- margin-top: 12px;
- }
-
- ul {
- min-height: 0;
- }
-
- li {
- &:first-child {
- margin-top: 12px;
- }
-
- &:not(:last-child) {
- margin-bottom: 12px;
- }
- }
- }
-}
-
-.read-more {
- margin-top: 12px;
- color: var(--accent);
+.trajectory {
+ position: relative;
+ width: 100%;
+ max-width: 333px;
+ overflow: hidden;
cursor: pointer;
- transition: color 0.2s;
-
- &:hover {
- color: var(--accent-dark);
- }
-
- @include typography.body-14;
-}
-
-.about {
-
- /* stylelint-disable value-no-vendor-prefix */
- &__text {
- p {
- display: -webkit-box;
- overflow: hidden;
- color: var(--dark-grey);
- text-overflow: ellipsis;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 5;
- transition: all 0.7s ease-in-out;
-
- &.expanded {
- -webkit-line-clamp: unset;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+
+ &__cover {
+ padding: 35px 24px;
+ background: linear-gradient(var(--accent), var(--lime));
+ border-radius: var(--rounded-lg);
+
+ app-button {
+ ::ng-deep {
+ .button {
+ position: absolute;
+ top: 15px;
+ right: 24px;
+ width: 166px;
+ padding: 0;
+ }
}
}
- ::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);
+ app-avatar {
+ ::ng-deep {
+ .avatar {
+ img {
+ box-shadow: 0 0 8px rgba($color: #333, $alpha: 30%);
+ }
+ }
}
}
}
- /* stylelint-enable value-no-vendor-prefix */
-
- &__read-full {
- margin-top: 2px;
- color: var(--accent);
- cursor: pointer;
- }
-}
-
-.trajectory {
- position: relative;
- height: 100%;
- padding: 18px 0 20px 18px;
- margin-bottom: 30px;
- border: 1px solid var(--grey-button);
- border-radius: 15px;
-
- &__container {
+ &__info {
display: flex;
flex-direction: column;
- width: 100%;
+ gap: 2px;
+ padding: 12px 24px;
- @include responsive.apply-desktop {
- flex-direction: row;
+ &--date {
+ color: var(--grey-for-text) !important;
}
}
- &__line {
+ &__action {
position: absolute;
- bottom: 34%;
- left: 44%;
- width: 1px;
- transform: rotate(90deg);
+ right: 14px;
+ bottom: 16px;
- @include responsive.apply-desktop {
- top: 0;
- left: 37%;
- height: 100%;
- transform: rotate(0);
+ &--started {
+ color: var(--green) !important;
}
}
&__rocket {
position: absolute;
- top: 0%;
- right: 0%;
+ top: -46%;
+ right: -13%;
z-index: 0;
- display: none;
-
- @include responsive.apply-desktop {
- display: block;
- }
- }
-
- &__images {
- display: flex;
- justify-content: center;
- }
-
- &__image {
- width: 85px;
- height: 80px;
- border-radius: 50%;
- object-fit: cover;
-
- @include responsive.apply-desktop {
- width: 150px;
- height: 135px;
- }
- }
-
- &__seat {
- p {
- display: none;
-
- @include responsive.apply-desktop {
- display: block;
- }
- }
- }
-
- &__date {
- display: block !important;
- margin-top: 48px;
- margin-bottom: 36px;
-
- @include responsive.apply-desktop {
- margin: 0;
- }
- }
-
- &__description {
- margin-top: 14px;
-
- p {
- display: box;
- overflow: hidden;
- color: var(--dark-grey);
- 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);
- }
- }
-
- &__read-full {
- margin-top: 2px;
- color: var(--accent);
- cursor: pointer;
- }
- }
-
- &__info {
- display: flex;
- flex-basis: 36%;
- flex-direction: column;
- justify-content: space-between;
- }
-
- &__content {
- margin-right: 20px;
- }
-
- &__skills {
- width: 100%;
- margin: 0;
-
- @include responsive.apply-desktop {
- width: 60%;
- margin-left: 30px;
- }
-
- &-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- p {
- display: none;
-
- @include responsive.apply-desktop {
- display: inline;
- }
- }
- }
-
- &-bottom {
- position: sticky;
- z-index: 10;
- display: flex;
- flex-direction: column;
- margin-top: 20px;
-
- @include responsive.apply-desktop {
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- }
- }
-
- &-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- height: 225px;
- }
- }
-
- &__buttons-group {
- display: flex;
- gap: 5px;
- align-items: center;
- justify-content: center;
- }
-
- &__inner {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- margin-top: 20px;
- }
-
- &__skill {
- display: flex;
- gap: 15px;
- align-items: center;
-
- .skill__image {
- width: 40px;
- height: 40px;
- border-radius: 100%;
- }
- }
-}
-
-.button__pick {
- width: 90%;
-}
-
-.cancel {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- width: 340px;
- max-height: calc(100vh - 40px);
- padding: 24px 20px 20px 24px;
- overflow-y: auto;
-
- @include responsive.apply-desktop {
- width: 800px;
- padding: 52px 50px 24px 42px;
- }
-
- &__info {
- display: flex;
- flex-direction: column-reverse;
-
- @include responsive.apply-desktop {
- display: flex;
- flex-direction: row;
- gap: 80px;
- align-items: flex-start;
- justify-content: space-between;
- }
- }
-
- &__image {
- display: flex;
- align-items: center;
- align-self: center;
- justify-content: center;
- width: 130px;
- height: 110px;
- margin-bottom: 18px;
-
- @include responsive.apply-desktop {
- width: 100%;
- height: 218px;
- }
- }
-
- &__title {
- margin-bottom: 10px;
- color: var(--accent);
- }
-
- &__text {
- margin-bottom: 5px;
-
- @include responsive.apply-desktop {
- width: 108%;
- margin-bottom: 10px;
- }
- }
-
- &__subtext {
- width: 100%;
- margin-bottom: 24px;
-
- @include responsive.apply-desktop {
- width: 60%;
- }
- }
-
- &__text-block {
- display: flex;
- justify-content: center;
- }
-
- &__button {
- width: 100%;
- text-align: center;
-
- @include responsive.apply-desktop {
- width: 20%;
- }
- }
-
- &__advantages {
- display: flex;
- flex-direction: column;
- }
-
- &__advantage {
- display: flex;
- gap: 10px;
- align-items: center;
-
- p {
- width: 105%;
- }
- }
-
- &__buttons-group {
- display: flex;
- gap: 15px;
- }
-
- &__confirm {
- display: flex;
- flex-direction: column;
- align-items: center;
-
- @include responsive.apply-desktop {
- align-items: center;
- text-align: center;
- }
- }
-}
-
-.confirm {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- width: 340px;
- padding: 0;
- text-align: center;
-
- @include responsive.apply-desktop {
- width: 750px;
- padding: 20px 50px 24px;
- }
-
- &__title {
- margin-bottom: 20px;
- }
-
- &__image {
- width: 130px;
- height: 125px;
- margin-top: 24px;
- margin-bottom: 18px;
-
- @include responsive.apply-desktop {
- width: 100%;
- height: 218px;
- }
- }
-
- &__explaining {
- width: 278px;
- height: 125px;
- margin-top: 24px;
- margin-bottom: 18px;
-
- @include responsive.apply-desktop {
- width: 100%;
- height: 150px;
- }
- }
-
- &__dots-group {
- display: flex;
- gap: 40px;
- align-items: center;
- }
-
- .dots-group {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 25px;
-
- @include responsive.apply-desktop {
- gap: 10px;
- font-size: 20px;
- font-weight: 600;
- }
-
- &__icon {
- width: 32px;
- height: 32px;
- cursor: pointer;
- }
-
- &-prev {
- color: var(--dark-grey);
- }
-
- &-next {
- transform: rotate(180deg);
- }
-
- &__dots {
- display: flex;
- gap: 18px;
- align-items: center;
- }
+ width: 175px;
+ transform: rotate(-53deg);
}
- .dot {
- width: 8px;
- height: 8px;
- border-radius: 100%;
+ p,
+ i {
+ color: var(--black);
}
}
diff --git a/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.ts b/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.ts
index e31625742..6a73d4d11 100644
--- a/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.ts
+++ b/projects/skills/src/app/trajectories/track-career/shared/trajectory/trajectory.component.ts
@@ -1,30 +1,12 @@
/** @format */
import { CommonModule } from "@angular/common";
-import {
- type AfterViewInit,
- ChangeDetectorRef,
- Component,
- type ElementRef,
- inject,
- Input,
- type OnInit,
- signal,
- ViewChild,
-} from "@angular/core";
+import { Component, inject, Input, signal } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
-import { ButtonComponent } from "@ui/components";
-import { ModalComponent } from "@ui/components/modal/modal.component";
-import { TrajectoriesService } from "../../../trajectories.service";
-import { DomSanitizer } from "@angular/platform-browser";
-import { expandElement } from "@utils/expand-element";
-import { IconComponent } from "@uilib";
-import { ParseBreaksPipe, ParseLinksPipe, PluralizePipe } from "@corelib";
+import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
import { Trajectory } from "projects/skills/src/models/trajectory.model";
-import { HttpErrorResponse } from "@angular/common/http";
-import { BreakpointObserver } from "@angular/cdk/layout";
-import { map, Observable } from "rxjs";
-import { trajectoryMoreList } from "projects/core/src/consts/lists/trajectory-more-list.const";
+import { IconComponent, ButtonComponent } from "@ui/components";
+import { AvatarComponent } from "@ui/components/avatar/avatar.component";
/**
* Компонент отображения карточки траектории
@@ -39,153 +21,26 @@ import { trajectoryMoreList } from "projects/core/src/consts/lists/trajectory-mo
standalone: true,
imports: [
CommonModule,
+ RouterModule,
+ TruncatePipe,
+ IconComponent,
+ AvatarComponent,
ButtonComponent,
- ModalComponent,
IconComponent,
- RouterModule,
- ParseLinksPipe,
- PluralizePipe,
- ParseBreaksPipe,
],
templateUrl: "./trajectory.component.html",
styleUrl: "./trajectory.component.scss",
})
-export class TrajectoryComponent implements AfterViewInit, OnInit {
+export class TrajectoryComponent {
@Input() trajectory!: Trajectory;
- protected readonly dotsArray = Array;
- protected readonly trajectoryMore = trajectoryMoreList;
router = inject(Router);
- trajectoryService = inject(TrajectoriesService);
- breakpointObserver = inject(BreakpointObserver);
-
- desktopMode$: Observable = this.breakpointObserver
- .observe("(min-width: 920px)")
- .pipe(map(result => result.matches));
-
- cdRef = inject(ChangeDetectorRef);
- sanitizer = inject(DomSanitizer);
-
- @ViewChild("descEl") descEl?: ElementRef;
-
- descriptionExpandable!: boolean;
- readFullDescription = false;
-
- currentPage = 1;
-
- // Состояния модальных окон
- moreModalOpen = signal(false);
- confirmModalOpen = signal(false);
- nonConfirmerModalOpen = signal(false);
- instructionModalOpen = signal(false);
- activatedModalOpen = signal(false);
-
- type = signal<"all" | "my" | null>(null);
-
- placeholderUrl =
- "https://uch-ibadan.org.ng/wp-content/uploads/2021/10/Profile_avatar_placeholder_large.png";
-
- /**
- * Инициализация компонента
- * Определяет тип отображения (all/my) на основе текущего URL
- */
- ngOnInit() {
- this.type.set(this.router.url.split("/").slice(-1)[0] as "all" | "my");
- }
-
- /**
- * Проверка возможности расширения описания после инициализации представления
- */
- ngAfterViewInit(): void {
- const descElement = this.descEl?.nativeElement;
- this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight;
-
- this.cdRef.detectChanges();
- }
-
- /**
- * Обработчик клика по кнопке "Выбрать"
- * Активирует траекторию и обрабатывает различные сценарии ответа сервера
- */
- onOpenConfirmClick() {
- this.trajectoryService.activateTrajectory(this.trajectory.id).subscribe({
- next: () => {
- if (!this.trajectory.isActiveForUser) {
- this.confirmModalOpen.set(true);
- }
- },
- error: err => {
- if (err instanceof HttpErrorResponse) {
- if (err.status === 403) {
- this.nonConfirmerModalOpen.set(true);
- } else if (err.status === 400) {
- this.activatedModalOpen.set(true);
- this.nonConfirmerModalOpen.set(false);
- this.instructionModalOpen.set(false);
- this.confirmModalOpen.set(false);
- }
- }
- },
- });
- }
-
- /**
- * Подтверждение выбора траектории
- * Закрывает модальное окно подтверждения и открывает инструкции
- */
- onConfirmClick() {
- this.confirmModalOpen.set(false);
- this.instructionModalOpen.set(true);
- }
-
- /**
- * Переход к предыдущей странице инструкций
- */
- prevPage(): void {
- if (this.currentPage > 1) {
- this.currentPage -= 1;
- }
- }
-
- /**
- * Переход к следующей странице инструкций или к траектории
- */
- nextPage(): void {
- if (this.currentPage === 4) {
- this.navigateOnTrajectory();
- } else if (this.currentPage < 4) {
- this.currentPage += 1;
- }
- }
-
- /**
- * Навигация к детальной странице траектории
- */
- navigateOnTrajectory() {
- this.router.navigate(["/trackCar/" + this.trajectory.id]).catch(err => {
- if (err.status === 403) {
- this.nonConfirmerModalOpen.set(true);
- }
- });
- }
- /**
- * Закрытие модального окна активной траектории и переход к ней
- * @param trajectoryId - ID траектории для перехода
- */
- onCloseModalActiveTrajectory(trajectoryId: number | string) {
- this.activatedModalOpen.set(false);
- this.router.navigate([`/trackCar/${trajectoryId}`]);
- }
+ protected readonly isStarted = signal(false);
+ protected readonly isDates = signal(false);
+ protected readonly isDate = signal(false);
+ protected readonly isEnded = signal(false);
- /**
- * Переключение развернутого/свернутого состояния описания
- * @param elem - HTML элемент описания
- * @param expandedClass - CSS класс для развернутого состояния
- * @param isExpanded - текущее состояние (развернуто/свернуто)
- */
- onExpandDescription(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void {
- expandElement(elem, expandedClass, isExpanded);
- this.readFullDescription = !isExpanded;
- }
+ protected readonly isMember = signal(false);
+ protected readonly isSubs = signal(false);
}
diff --git a/projects/skills/src/app/trajectories/track-career/track-career-my.resolver.ts b/projects/skills/src/app/trajectories/track-career/track-career-my.resolver.ts
deleted file mode 100644
index 5a7557c1d..000000000
--- a/projects/skills/src/app/trajectories/track-career/track-career-my.resolver.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/** @format */
-
-import { inject } from "@angular/core";
-import { TrajectoriesService } from "../trajectories.service";
-
-/**
- * Резолвер для загрузки персональной траектории пользователя
- * Выполняется перед активацией маршрута "my"
- * @returns Observable с данными пользовательской траектории
- */
-
-/**
- * Функция-резолвер для получения персональной траектории пользователя
- * @returns Promise/Observable с данными пользовательской траектории
- */
-export const TrajectoriesMyResolver = () => {
- const trajectoriesService = inject(TrajectoriesService);
-
- return trajectoriesService.getMyTrajectory();
-};
diff --git a/projects/skills/src/app/trajectories/track-career/track-career.component.html b/projects/skills/src/app/trajectories/track-career/track-career.component.html
index 4cf213030..a89e50624 100644
--- a/projects/skills/src/app/trajectories/track-career/track-career.component.html
+++ b/projects/skills/src/app/trajectories/track-career/track-career.component.html
@@ -1,22 +1,18 @@
diff --git a/projects/skills/src/app/trajectories/track-career/track-career.component.scss b/projects/skills/src/app/trajectories/track-career/track-career.component.scss
index 22357e787..53c48f8bc 100644
--- a/projects/skills/src/app/trajectories/track-career/track-career.component.scss
+++ b/projects/skills/src/app/trajectories/track-career/track-career.component.scss
@@ -2,33 +2,9 @@
@use "styles/responsive";
.profile {
- display: flex;
- flex-direction: column;
-
- &__overlay {
- position: absolute;
- top: 0%;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 10;
- align-content: center;
- align-items: center;
- justify-content: center;
- text-align: center;
-
- @include responsive.apply-desktop {
- top: 500%;
- }
-
- &__text {
- @include typography.heading-1;
-
- color: var(--grey-for-text);
- }
-
- &__image {
- margin: 0 auto;
- }
+ &__info {
+ display: grid;
+ grid-template-columns: 8fr 2fr;
+ column-gap: 20px;
}
}
diff --git a/projects/skills/src/app/trajectories/track-career/track-career.component.ts b/projects/skills/src/app/trajectories/track-career/track-career.component.ts
index 545a5d095..56f31b633 100644
--- a/projects/skills/src/app/trajectories/track-career/track-career.component.ts
+++ b/projects/skills/src/app/trajectories/track-career/track-career.component.ts
@@ -1,9 +1,12 @@
/** @format */
import { CommonModule } from "@angular/common";
-import { Component } from "@angular/core";
+import { Component, inject } from "@angular/core";
import { RouterModule } from "@angular/router";
-import { BarComponent } from "@ui/components";
+import { BackComponent } from "@uilib";
+import { SearchComponent } from "@ui/components/search/search.component";
+import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
+import { SoonCardComponent } from "@office/shared/soon-card/soon-card.component";
/**
* Главный компонент модуля отслеживания карьерных траекторий
@@ -13,8 +16,25 @@ import { BarComponent } from "@ui/components";
@Component({
selector: "app-track-career",
standalone: true,
- imports: [CommonModule, BarComponent, RouterModule],
+ imports: [
+ CommonModule,
+ RouterModule,
+ BackComponent,
+ SearchComponent,
+ ReactiveFormsModule,
+ SoonCardComponent,
+ ],
templateUrl: "./track-career.component.html",
styleUrl: "./track-career.component.scss",
})
-export class TrackCareerComponent {}
+export class TrackCareerComponent {
+ private readonly fb = inject(FormBuilder);
+
+ constructor() {
+ this.searchForm = this.fb.group({
+ search: [""],
+ });
+ }
+
+ searchForm: FormGroup;
+}
diff --git a/projects/skills/src/app/trajectories/track-career/track-career.routes.ts b/projects/skills/src/app/trajectories/track-career/track-career.routes.ts
index 674f0454b..84c84ea52 100644
--- a/projects/skills/src/app/trajectories/track-career/track-career.routes.ts
+++ b/projects/skills/src/app/trajectories/track-career/track-career.routes.ts
@@ -4,7 +4,6 @@ import { Routes } from "@angular/router";
import { TrackCareerComponent } from "./track-career.component";
import { TrajectoriesListComponent } from "./list/list.component";
import { TrajectoriesResolver } from "./track-career.resolver";
-import { TrajectoriesMyResolver } from "./track-career-my.resolver";
/**
* Конфигурация маршрутов для модуля карьерных траекторий
@@ -32,13 +31,6 @@ export const TRACK_CAREER_ROUTES: Routes = [
data: TrajectoriesResolver,
},
},
- {
- path: "my",
- component: TrajectoriesListComponent,
- resolve: {
- data: TrajectoriesMyResolver,
- },
- },
],
},
{
diff --git a/projects/skills/src/app/trajectories/trajectories.service.ts b/projects/skills/src/app/trajectories/trajectories.service.ts
index 5ed332264..b563e7f17 100644
--- a/projects/skills/src/app/trajectories/trajectories.service.ts
+++ b/projects/skills/src/app/trajectories/trajectories.service.ts
@@ -42,20 +42,6 @@ export class TrajectoriesService {
return this.apiService.get(this.TRAJECTORY_URL);
}
- /**
- * Получает траекторию, в которой текущий пользователь активно зарегистрирован
- *
- * @returns Observable - Массив, содержащий активную траекторию пользователя (если есть)
- */
- getMyTrajectory() {
- return this.apiService.get(this.TRAJECTORY_URL).pipe(
- map(track => {
- const choosedTrajctory = track.find(trajectory => trajectory.isActiveForUser === true);
- return choosedTrajctory;
- })
- );
- }
-
/**
* Получает подробную информацию о конкретной траектории
*
diff --git a/projects/skills/src/app/webinars/list/list.component.html b/projects/skills/src/app/webinars/list/list.component.html
deleted file mode 100644
index bd85e9488..000000000
--- a/projects/skills/src/app/webinars/list/list.component.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- @if (type() === 'actual') { @if (webinarList().length) { @for(webinar of webinarList(); track
- $index){
-
- } } } @else if (type() === 'records'){ @if (recordsList().length) { @for (record of recordsList();
- track $index) {
-
- } } }
-
diff --git a/projects/skills/src/app/webinars/list/list.component.scss b/projects/skills/src/app/webinars/list/list.component.scss
deleted file mode 100644
index 7987768d9..000000000
--- a/projects/skills/src/app/webinars/list/list.component.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.list {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-bottom: 20px;
-}
diff --git a/projects/skills/src/app/webinars/list/list.component.ts b/projects/skills/src/app/webinars/list/list.component.ts
deleted file mode 100644
index d31406fb4..000000000
--- a/projects/skills/src/app/webinars/list/list.component.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-/** @format */
-
-import {
- type AfterViewInit,
- ChangeDetectorRef,
- Component,
- inject,
- type OnInit,
- signal,
-} from "@angular/core";
-import { CommonModule } from "@angular/common";
-import { ActivatedRoute, Router, RouterModule } from "@angular/router";
-import { concatMap, fromEvent, map, noop, of, type Subscription, tap, throttleTime } from "rxjs";
-import { WebinarComponent } from "../shared/webinar/webinar.component";
-import type { Webinar } from "projects/skills/src/models/webinars.model";
-import { WebinarService } from "../services/webinar.service";
-import type { ApiPagination } from "projects/skills/src/models/api-pagination.model";
-
-/**
- * Компонент списка вебинаров
- *
- * Универсальный компонент для отображения как актуальных вебинаров,
- * так и записей завершенных вебинаров. Поддерживает бесконечную прокрутку
- * для постепенной загрузки контента.
- *
- * Функциональность:
- * - Отображение списка вебинаров в зависимости от типа (актуальные/записи)
- * - Бесконечная прокрутка для загрузки дополнительного контента
- * - Автоматическое определение типа контента по URL
- * - Оптимизированная загрузка данных порциями
- */
-@Component({
- selector: "app-list",
- standalone: true,
- imports: [CommonModule, RouterModule, WebinarComponent],
- templateUrl: "./list.component.html",
- styleUrl: "./list.component.scss",
-})
-export class WebinarsListComponent implements OnInit, AfterViewInit {
- // Внедрение зависимостей
- router = inject(Router);
- route = inject(ActivatedRoute);
- webinarService = inject(WebinarService);
- cdRef = inject(ChangeDetectorRef);
-
- // Сигналы для управления состоянием
- totalItemsCount = signal(0);
- webinarList = signal([]);
- recordsList = signal([]);
- webinarPage = signal(1);
- perFetchTake = signal(20);
-
- // Тип отображаемого контента
- type = signal<"actual" | "records" | null>(null);
-
- // Управление подписками
- subscriptions$ = signal([]);
-
- /**
- * Инициализация компонента
- *
- * Определяет тип контента по URL и загружает соответствующие данные
- * из резолвера маршрута
- */
- ngOnInit(): void {
- // Определение типа контента по последнему сегменту URL
- this.type.set(this.router.url.split("/").slice(-1)[0] as "actual" | "records");
-
- // Получение данных из резолвера в зависимости от типа
- const routeData$ =
- this.type() === "actual"
- ? this.route.data.pipe(map(r => r["data"]))
- : this.route.data.pipe(map(r => r["data"]));
-
- const subscription = routeData$.subscribe((vacancy: ApiPagination) => {
- if (this.type() === "actual") {
- this.webinarList.set(vacancy.results as Webinar[]);
- } else if (this.type() === "records") {
- this.recordsList.set(vacancy.results as Webinar[]);
- }
- this.totalItemsCount.set(vacancy.count);
- });
-
- this.subscriptions$().push(subscription);
- }
-
- /**
- * Настройка бесконечной прокрутки после инициализации представления
- *
- * Подписывается на события прокрутки контейнера и запускает
- * загрузку дополнительных данных при достижении конца списка
- */
- ngAfterViewInit(): void {
- const target = document.querySelector(".office__body");
- if (target) {
- const scrollEvents$ = fromEvent(target, "scroll")
- .pipe(
- concatMap(() => this.onScroll()),
- throttleTime(500) // Ограничение частоты запросов
- )
- .subscribe(noop);
- this.subscriptions$().push(scrollEvents$);
- }
- }
-
- /**
- * Обработчик события прокрутки
- *
- * Проверяет, достиг ли пользователь конца списка, и если да,
- * загружает следующую порцию данных
- *
- * @returns Observable с новыми данными или пустой Observable
- */
- onScroll() {
- // Проверка, загружены ли все доступные элементы
- if (this.totalItemsCount() && this.recordsList().length >= this.totalItemsCount())
- return of({});
-
- if (this.totalItemsCount() && this.webinarList().length >= this.totalItemsCount())
- return of({});
-
- const target = document.querySelector(".office__body");
- if (!target) return of({});
-
- // Вычисление позиции прокрутки
- const diff = target.scrollTop - target.scrollHeight + target.clientHeight;
-
- // Если достигнут конец списка, загружаем следующую порцию
- if (diff > 0) {
- return this.onFetch(this.webinarPage() * this.perFetchTake(), this.perFetchTake()).pipe(
- tap((webinarChunk: Webinar[]) => {
- if (this.type() === "actual") {
- this.webinarPage.update(page => page + 1);
- this.webinarList.update(items => [...items, ...webinarChunk]);
- } else if (this.type() === "records") {
- this.webinarPage.update(page => page + 1);
- this.recordsList.update(items => [...items, ...webinarChunk]);
- }
- })
- );
- }
-
- return of({});
- }
-
- /**
- * Загрузка дополнительных данных
- *
- * Выполняет запрос к API для получения следующей порции вебинаров
- * в зависимости от текущего типа контента
- *
- * @param offset - смещение для пагинации
- * @param limit - количество элементов для загрузки
- * @returns Observable с новыми данными
- */
- onFetch(offset: number, limit: number) {
- if (this.type() === "actual") {
- return this.webinarService.getActualWebinars(limit, offset).pipe(
- tap((res: any) => {
- this.totalItemsCount.set(res.count);
- this.webinarList.update(items => [...items, ...res.results]);
- }),
- map(res => res)
- );
- } else if (this.type() === "records") {
- return this.webinarService.getRecords(limit, offset).pipe(
- tap((res: any) => {
- this.totalItemsCount.set(res.count);
- this.recordsList.update(items => [...items, ...res.results]);
- }),
- map(res => res)
- );
- }
-
- return of([]);
- }
-}
diff --git a/projects/skills/src/app/webinars/list/list.routes.ts b/projects/skills/src/app/webinars/list/list.routes.ts
deleted file mode 100644
index acfb2ec55..000000000
--- a/projects/skills/src/app/webinars/list/list.routes.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/** @format */
-
-import { Routes } from "@angular/router";
-import { RecordsResolver } from "./records.resolver";
-import { WebinarsListComponent } from "./list.component";
-
-export const RECORDS_ROUTES: Routes = [
- {
- path: "",
- component: WebinarsListComponent,
- resolve: {
- data: RecordsResolver,
- },
- },
-];
diff --git a/projects/skills/src/app/webinars/list/records.resolver.ts b/projects/skills/src/app/webinars/list/records.resolver.ts
deleted file mode 100644
index 88d1fecc8..000000000
--- a/projects/skills/src/app/webinars/list/records.resolver.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/** @format */
-
-import { inject } from "@angular/core";
-import { WebinarService } from "../services/webinar.service";
-
-/**
- * Резолвер для загрузки записей вебинаров
- *
- * Предоставляет начальную порцию записей завершенных вебинаров
- * для отображения в списке. Загружает первые 20 записей.
- *
- * @returns Observable с пагинированным списком записей вебинаров
- */
-export const RecordsResolver = () => {
- const webinarService = inject(WebinarService);
-
- return webinarService.getRecords(20, 0);
-};
diff --git a/projects/skills/src/app/webinars/services/webinar.service.spec.ts b/projects/skills/src/app/webinars/services/webinar.service.spec.ts
deleted file mode 100644
index ccb8f998e..000000000
--- a/projects/skills/src/app/webinars/services/webinar.service.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/** @format */
-
-import { TestBed } from "@angular/core/testing";
-
-import { WebinarService } from "../../../../webinar.service";
-
-describe("WebinarService", () => {
- let service: WebinarService;
-
- beforeEach(() => {
- TestBed.configureTestingModule({});
- service = TestBed.inject(WebinarService);
- });
-
- it("should be created", () => {
- expect(service).toBeTruthy();
- });
-});
diff --git a/projects/skills/src/app/webinars/services/webinar.service.ts b/projects/skills/src/app/webinars/services/webinar.service.ts
deleted file mode 100644
index 9c4ce45b6..000000000
--- a/projects/skills/src/app/webinars/services/webinar.service.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/** @format */
-
-import { HttpParams } from "@angular/common/http";
-import { Injectable } from "@angular/core";
-import { SkillsApiService } from "@corelib";
-import { plainToInstance } from "class-transformer";
-import { Webinar } from "projects/skills/src/models/webinars.model";
-import { map, Observable } from "rxjs";
-
-/**
- * Сервис для работы с вебинарами
- *
- * Предоставляет методы для получения информации о вебинарах,
- * управления регистрацией и доступом к записям.
- *
- * Взаимодействует с API вебинаров и обрабатывает трансформацию
- * данных в типизированные модели.
- */
-@Injectable({
- providedIn: "root",
-})
-export class WebinarService {
- private readonly WEBINAR_URL = "/webinars";
-
- constructor(private readonly apiService: SkillsApiService) {}
-
- /**
- * Получение списка актуальных (предстоящих) вебинаров
- *
- * Загружает вебинары, которые еще не прошли и доступны
- * для регистрации и участия
- *
- * @param limit - количество вебинаров для загрузки
- * @param offset - смещение для пагинации
- * @returns Observable - массив актуальных вебинаров
- */
- getActualWebinars(limit: number, offset: number): Observable {
- const params = new HttpParams();
-
- params.set("limit", limit);
- params.set("offset", offset);
-
- return this.apiService
- .get(`${this.WEBINAR_URL}/actual/`, params)
- .pipe(map(webinar => plainToInstance(Webinar, webinar)));
- }
-
- /**
- * Получение списка записей завершенных вебинаров
- *
- * Загружает вебинары, которые уже прошли и доступны
- * для просмотра в записи
- *
- * @param limit - количество записей для загрузки
- * @param offset - смещение для пагинации
- * @returns Observable - пагинированный список записей вебинаров
- */
- getRecords(limit: number, offset: number) {
- const params = new HttpParams();
-
- params.set("limit", limit);
- params.set("offset", offset);
-
- return this.apiService
- .get(`${this.WEBINAR_URL}/records/`, params)
- .pipe(map(webinar => plainToInstance(Webinar, webinar)));
- }
-
- /**
- * Получение ссылки на запись вебинара
- *
- * Предоставляет прямую ссылку для просмотра записи
- * конкретного завершенного вебинара
- *
- * @param webinarId - идентификатор вебинара
- * @returns Observable<{recordingLink: string}> - объект со ссылкой на запись
- */
- getWebinarLink(webinarId: number) {
- return this.apiService.get<{ recordingLink: string }>(
- `${this.WEBINAR_URL}/records/${webinarId}/link/`
- );
- }
-
- /**
- * Регистрация пользователя на вебинар
- *
- * Выполняет регистрацию текущего пользователя на предстоящий вебинар.
- * После успешной регистрации пользователь получит уведомления
- * и доступ к участию в вебинаре.
- *
- * @param webinarId - идентификатор вебинара для регистрации
- * @returns Observable - результат операции регистрации
- */
- registrationOnWebinar(webinarId: number) {
- return this.apiService.post(`${this.WEBINAR_URL}/actual/${webinarId}/`, {});
- }
-}
diff --git a/projects/skills/src/app/webinars/shared/webinar/webinar.component.html b/projects/skills/src/app/webinars/shared/webinar/webinar.component.html
deleted file mode 100644
index 307c63c4a..000000000
--- a/projects/skills/src/app/webinars/shared/webinar/webinar.component.html
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
-
-
-
-
{{ webinar.title }}
-
-
- @if (descriptionExpandable) {
-
- {{ readFullDescription ? "скрыть" : "подробнее" }}
-
- }
-
-
-
-
- @if (webinar.speaker; as speaker) {
-
Спикер
-
-
-
{{ speaker.fullName }}
-
{{ speaker.position }}
-
- }
-
-
-
-
-
- {{ formattedDate() }}, {{ webinar.duration / 60 | number: "1.0-1" }} часа
-
- @if(type() === "actual"){ @if(webinar.isRegistrated || isRegistrated()){
-
-
-
Вы зарегистрировались
-
- } @else {
-
- Регистрация
-
- } } @else {
-
Посмотреть запись
- }
-
-
-
-
-
-
-
-
-
- Вы зарегистрировались на вебинар “{{ webinar.title }}”. На вашу почту придет письмо с
- ссылкой на подключение
-
-
-
LETS GO
-
-
-
-
-
-
-
-
-
-
-
- {{ isSubscribedModalText() }}
-
-
-
Купить
-
-
diff --git a/projects/skills/src/app/webinars/shared/webinar/webinar.component.scss b/projects/skills/src/app/webinars/shared/webinar/webinar.component.scss
deleted file mode 100644
index b7c3e55b0..000000000
--- a/projects/skills/src/app/webinars/shared/webinar/webinar.component.scss
+++ /dev/null
@@ -1,166 +0,0 @@
-@use "styles/responsive";
-@use "styles/typography";
-
-.webinar {
- padding: 24px;
- background-color: var(--white);
- border: 1px solid var(--grey-button);
- border-radius: 15px;
-
- &__inner {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- justify-content: space-between;
-
- @include responsive.apply-desktop {
- flex-direction: row;
- align-items: center;
- }
- }
-
- &__info {
- @include responsive.apply-desktop {
- flex-basis: 46%;
- }
- }
-
- &__speaker {
- display: flex;
- flex-direction: column;
- gap: 10px;
- align-items: flex-end;
- }
-
- .speaker__info {
- display: flex;
- flex-direction: column;
- gap: 5px;
- align-items: flex-end;
- }
-
- &__description {
- margin-top: 14px;
-
- p {
- display: box;
- overflow: hidden;
- color: var(--dark-grey);
- 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);
- }
- }
-
- &__read-full {
- margin-top: 2px;
- color: var(--accent);
- cursor: pointer;
- }
- }
-
- &__registration {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 12px;
- }
-
- &__registrated {
- display: flex;
- gap: 5px;
- align-items: center;
- }
-
- &__watch-button {
- width: 35%;
-
- @include responsive.apply-desktop {
- width: 18%;
- }
- }
-}
-
-.read-more {
- margin-top: 12px;
- color: var(--accent);
- cursor: pointer;
- transition: color 0.2s;
-
- &:hover {
- color: var(--accent-dark);
- }
-
- @include typography.body-14;
-}
-
-.cancel {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- max-height: calc(100vh - 40px);
- padding: 24px;
- overflow-y: auto;
-
- &__top {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- width: 68%;
- padding-top: 14px;
- }
-
- &__image {
- display: flex;
- flex-direction: column;
- gap: 15px;
- align-items: center;
- justify-content: space-between;
-
- @include responsive.apply-desktop {
- display: flex;
- flex-direction: row;
- gap: 15px;
- align-items: center;
- justify-content: space-between;
- margin: 14px 0 5px;
- }
- }
-
- &__cross {
- position: absolute;
- top: 0;
- right: 0;
- width: 32px;
- height: 32px;
- cursor: pointer;
-
- @include responsive.apply-desktop {
- top: 8px;
- right: 8px;
- }
- }
-
- &__title {
- margin-bottom: 15px;
- text-align: center;
- }
-}
diff --git a/projects/skills/src/app/webinars/shared/webinar/webinar.component.spec.ts b/projects/skills/src/app/webinars/shared/webinar/webinar.component.spec.ts
deleted file mode 100644
index 8397b4b34..000000000
--- a/projects/skills/src/app/webinars/shared/webinar/webinar.component.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/** @format */
-
-import { ComponentFixture, TestBed } from "@angular/core/testing";
-
-import { WebinarComponent } from "./webinar.component";
-
-describe("WebinarComponent", () => {
- let component: WebinarComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [WebinarComponent],
- }).compileComponents();
-
- fixture = TestBed.createComponent(WebinarComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/projects/skills/src/app/webinars/shared/webinar/webinar.component.ts b/projects/skills/src/app/webinars/shared/webinar/webinar.component.ts
deleted file mode 100644
index 78e9daa04..000000000
--- a/projects/skills/src/app/webinars/shared/webinar/webinar.component.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/** @format */
-
-import { CommonModule, DatePipe } from "@angular/common";
-import {
- ChangeDetectorRef,
- Component,
- ElementRef,
- inject,
- Input,
- OnInit,
- signal,
- ViewChild,
-} from "@angular/core";
-import { Router, RouterModule } from "@angular/router";
-import { ParseBreaksPipe, ParseLinksPipe, YtExtractService } from "@corelib";
-import { ButtonComponent } from "@ui/components";
-import { ModalComponent } from "@ui/components/modal/modal.component";
-import { AvatarComponent, IconComponent } from "@uilib";
-import { expandElement } from "@utils/expand-element";
-import { Webinar } from "projects/skills/src/models/webinars.model";
-import { ProfileService } from "../../../profile/services/profile.service";
-import { DomSanitizer } from "@angular/platform-browser";
-import { WebinarService } from "../../services/webinar.service";
-import { tap, throwError } from "rxjs";
-import { HttpErrorResponse } from "@angular/common/http";
-
-@Component({
- selector: "app-webinar",
- standalone: true,
- imports: [
- CommonModule,
- RouterModule,
- AvatarComponent,
- ButtonComponent,
- ModalComponent,
- IconComponent,
- ],
- templateUrl: "./webinar.component.html",
- styleUrl: "./webinar.component.scss",
-})
-export class WebinarComponent implements OnInit {
- @Input() webinar!: Webinar;
-
- router = inject(Router);
- webinarService = inject(WebinarService);
-
- cdRef = inject(ChangeDetectorRef);
- sanitizer = inject(DomSanitizer);
-
- @ViewChild("descEl") descEl?: ElementRef;
-
- descriptionExpandable!: boolean;
- readFullDescription = false;
-
- type = signal<"actual" | "records" | null>(null);
- isRegistrated = signal(false);
-
- registrationModalOpen = signal(false);
- isSubscribedModalOpen = signal(false);
- isSubscribedModalText = signal("");
-
- formattedDate = signal("");
-
- ngOnInit() {
- this.type.set(this.router.url.split("/").slice(-1)[0] as "actual" | "records");
-
- this.formattedDate.set(
- new Date(this.webinar.datetimeStart).toLocaleDateString("ru-RU", {
- day: "numeric",
- month: "long",
- })
- );
- }
-
- ngAfterViewInit(): void {
- const descElement = this.descEl?.nativeElement;
- this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight;
-
- this.cdRef.detectChanges();
- }
-
- onExpandDescription(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void {
- expandElement(elem, expandedClass, isExpanded);
- this.readFullDescription = !isExpanded;
- }
-
- onRegistration(webinarId: number) {
- if (!this.webinar.isRegistrated) {
- this.webinarService.registrationOnWebinar(webinarId).subscribe();
- this.registrationModalOpen.set(true);
- this.isRegistrated.set(true);
- }
- }
-
- onWatchRecord(webinarId: number) {
- this.webinarService.getWebinarLink(webinarId).subscribe({
- next: ({ recordingLink }) => {
- window.open(recordingLink, "_blank");
- },
- error: error => {
- if (error instanceof HttpErrorResponse) {
- if (error.status === 403) {
- this.isSubscribedModalText.set(error.error.detail);
- this.isSubscribedModalOpen.set(true);
- console.log(error.error.detail);
- }
- }
- },
- });
- }
-}
diff --git a/projects/skills/src/app/webinars/webinars.component.html b/projects/skills/src/app/webinars/webinars.component.html
deleted file mode 100644
index 6c6787200..000000000
--- a/projects/skills/src/app/webinars/webinars.component.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
diff --git a/projects/skills/src/app/webinars/webinars.component.scss b/projects/skills/src/app/webinars/webinars.component.scss
deleted file mode 100644
index e69de29bb..000000000
diff --git a/projects/skills/src/app/webinars/webinars.component.spec.ts b/projects/skills/src/app/webinars/webinars.component.spec.ts
deleted file mode 100644
index 801dd27f4..000000000
--- a/projects/skills/src/app/webinars/webinars.component.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/** @format */
-
-import { ComponentFixture, TestBed } from "@angular/core/testing";
-
-import { WebinarsComponent } from "./webinars.component";
-
-describe("WebinarsComponent", () => {
- let component: WebinarsComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [WebinarsComponent],
- }).compileComponents();
-
- fixture = TestBed.createComponent(WebinarsComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/projects/skills/src/app/webinars/webinars.component.ts b/projects/skills/src/app/webinars/webinars.component.ts
deleted file mode 100644
index 55ed9da59..000000000
--- a/projects/skills/src/app/webinars/webinars.component.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/** @format */
-
-import { Component } from "@angular/core";
-import { BarComponent } from "../../../../social_platform/src/app/ui/components/bar/bar.component";
-import { CommonModule } from "@angular/common";
-import { RouterLink, RouterModule } from "@angular/router";
-
-@Component({
- selector: "app-webinars",
- standalone: true,
- imports: [BarComponent, CommonModule, RouterModule],
- templateUrl: "./webinars.component.html",
- styleUrl: "./webinars.component.scss",
-})
-export class WebinarsComponent {}
diff --git a/projects/skills/src/app/webinars/webinars.resolver.ts b/projects/skills/src/app/webinars/webinars.resolver.ts
deleted file mode 100644
index b0d1744d6..000000000
--- a/projects/skills/src/app/webinars/webinars.resolver.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/** @format */
-
-import { inject } from "@angular/core";
-import { WebinarService } from "./services/webinar.service";
-
-export const WebinarsResolver = () => {
- const webinarService = inject(WebinarService);
-
- return webinarService.getActualWebinars(20, 0);
-};
diff --git a/projects/skills/src/app/webinars/webinars.routes.ts b/projects/skills/src/app/webinars/webinars.routes.ts
deleted file mode 100644
index 0a7dbba59..000000000
--- a/projects/skills/src/app/webinars/webinars.routes.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/** @format */
-
-import { Routes } from "@angular/router";
-import { WebinarsComponent } from "./webinars.component";
-import { WebinarsResolver } from "./webinars.resolver";
-import { WebinarsListComponent } from "./list/list.component";
-
-export const WEBINARS_ROUTES: Routes = [
- {
- path: "",
- component: WebinarsComponent,
- children: [
- {
- path: "",
- redirectTo: "actual",
- pathMatch: "full",
- },
- {
- path: "actual",
- component: WebinarsListComponent,
- resolve: {
- data: WebinarsResolver,
- },
- },
- {
- path: "records",
- loadChildren: () => import("./list/list.routes").then(c => c.RECORDS_ROUTES),
- },
- ],
- },
-];
diff --git a/projects/skills/src/assets/icons/svg/folder.svg b/projects/skills/src/assets/icons/svg/folder.svg
new file mode 100644
index 000000000..bc5130c5c
--- /dev/null
+++ b/projects/skills/src/assets/icons/svg/folder.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/projects/skills/src/assets/icons/svg/lock.svg b/projects/skills/src/assets/icons/svg/lock.svg
new file mode 100644
index 000000000..3e17fbe86
--- /dev/null
+++ b/projects/skills/src/assets/icons/svg/lock.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/projects/skills/src/assets/icons/svg/logout3.svg b/projects/skills/src/assets/icons/svg/logout3.svg
new file mode 100644
index 000000000..b7c27c199
--- /dev/null
+++ b/projects/skills/src/assets/icons/svg/logout3.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/projects/skills/src/assets/icons/svg/person.svg b/projects/skills/src/assets/icons/svg/person.svg
new file mode 100644
index 000000000..8746fa9c3
--- /dev/null
+++ b/projects/skills/src/assets/icons/svg/person.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/projects/skills/src/assets/icons/svg/triangle.svg b/projects/skills/src/assets/icons/svg/triangle.svg
new file mode 100644
index 000000000..c98333c02
--- /dev/null
+++ b/projects/skills/src/assets/icons/svg/triangle.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/projects/skills/src/assets/icons/symbol/svg/sprite.css.svg b/projects/skills/src/assets/icons/symbol/svg/sprite.css.svg
index 7162e7b05..bf5d7f495 100644
--- a/projects/skills/src/assets/icons/symbol/svg/sprite.css.svg
+++ b/projects/skills/src/assets/icons/symbol/svg/sprite.css.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/projects/skills/src/assets/images/task/character.svg b/projects/skills/src/assets/images/task/character.svg
new file mode 100644
index 000000000..64f0ea9dd
--- /dev/null
+++ b/projects/skills/src/assets/images/task/character.svg
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/skills/src/assets/images/task/complete.svg b/projects/skills/src/assets/images/task/complete.svg
new file mode 100644
index 000000000..e97c7693a
--- /dev/null
+++ b/projects/skills/src/assets/images/task/complete.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/skills/src/models/skill.model.ts b/projects/skills/src/models/skill.model.ts
deleted file mode 100644
index 91d736e02..000000000
--- a/projects/skills/src/models/skill.model.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-/** @format */
-
-/**
- * Основная модель навыка в системе обучения
- *
- * Представляет навык, который пользователь может изучать.
- * Содержит информацию о доступности, прогрессе и метаданных навыка.
- */
-export interface Skill {
- /** Уникальный идентификатор навыка */
- id: number;
-
- /** Название навыка */
- name: string;
-
- /** Общее количество уровней в навыке */
- quantityOfLevels: number;
-
- /** Ссылка на изображение/иконку навыка */
- fileLink: string;
-
- /** Автор/создатель навыка */
- whoCreated: string;
-
- /** Описание навыка */
- description: string;
-
- /** Флаг принадлежности навыка к траектории обучения */
- isFromTrajectory: boolean;
-
- /** Требует ли навык активную подписку для доступа */
- requiresSubscription: boolean;
-
- /** Завершен ли навык пользователем */
- isDone: boolean;
-
- /** Доступен ли навык бесплатно */
- freeAccess: boolean;
-
- /** Просрочен ли навык (опциональное поле) */
- overdue?: boolean;
-}
-
-/**
- * Детальная информация о задаче навыка
- *
- * Содержит метаданные конкретной задачи в рамках навыка.
- */
-export interface TaskDetail {
- /** Название навыка, к которому относится задача */
- skillName: string;
-
- /** Описание задачи */
- description: string;
-
- /** Уровень сложности задачи */
- level: number;
-
- /** Путь к файлу с дополнительными материалами */
- file: string;
-}
-
-/**
- * Модель задачи в рамках навыка
- *
- * Представляет отдельную задачу с информацией о её состоянии.
- */
-export interface Task {
- /** Уникальный идентификатор задачи */
- id: number;
-
- /** Уровень задачи в навыке */
- level: number;
-
- /** Название задачи */
- name: string;
-
- /** Статус выполнения задачи (true - выполнена, false - не выполнена) */
- status: boolean;
-}
-
-/**
- * Ответ сервера со списком задач и статистикой
- *
- * Содержит задачи навыка и статистику выполнения по неделям.
- */
-export interface TasksResponse {
- /** Массив задач навыка */
- tasks: Task[];
-
- /** Статистика выполнения по неделям */
- statsOfWeeks: {
- /** Выполнено ли вовремя (null если неприменимо) */
- doneOnTime: boolean | null;
-
- /** Выполнено ли вообще */
- isDone: boolean;
-
- /** Номер недели */
- week: number;
- }[];
-
- /** Общий прогресс выполнения навыка в процентах (0-100) */
- progress: number;
-}
-
-/**
- * Шаг выполнения задачи
- *
- * Представляет отдельный шаг в рамках задачи с определенным типом взаимодействия.
- */
-export interface TaskStep {
- /** Уникальный идентификатор шага */
- id: number;
-
- /** Выполнен ли шаг */
- isDone: boolean;
-
- /** Тип шага задачи */
- type:
- | "exclude_question" // Вопрос на исключение
- | "question_single_answer" // Вопрос с одним ответом
- | "question_connect" // Вопрос на соединение
- | "info_slide" // Информационный слайд
- | "question_write"; // Вопрос с письменным ответом
-
- /** Порядковый номер шага в задаче */
- ordinalNumber: number;
-}
-
-/**
- * Ответ сервера с шагами задачи и дополнительной информацией
- *
- * Расширяет TaskDetail дополнительными данными о прогрессе и шагах.
- */
-export interface TaskStepsResponse extends TaskDetail {
- /** Общее количество шагов в задаче */
- count: number;
-
- /** Текущий уровень пользователя в навыке */
- currentLevel: number;
-
- /** Следующий уровень навыка */
- nextLevel: number;
-
- /** Название навыка */
- skillName: string;
-
- /** Ссылка на логотип навыка для отображения баллов */
- skillPointLogo: string;
-
- /** Ссылка на превью навыка */
- skillPreview: string;
-
- /** Массив шагов задачи */
- stepData: TaskStep[];
-}
-
-/**
- * Результаты выполнения задачи
- *
- * Содержит информацию о результатах прохождения задачи пользователем.
- */
-export interface TaskResults {
- /** Количество полученных баллов за выполнение */
- pointsGained: number;
-
- /** Количество правильно выполненных шагов */
- quantityDoneCorrect: number;
-
- /** Общее количество шагов в задаче */
- quantityAll: number;
-
- /** Прогресс выполнения в процентах (0-100) */
- progress: number;
-
- /** ID следующей задачи (null если это последняя задача) */
- nextTaskId: null | number;
-
- /** Уровень задачи */
- level: number;
-
- /** Название навыка */
- skillName: string;
-}
diff --git a/projects/skills/src/models/step.model.ts b/projects/skills/src/models/step.model.ts
index bd61e7535..7df3b4b13 100644
--- a/projects/skills/src/models/step.model.ts
+++ b/projects/skills/src/models/step.model.ts
@@ -78,6 +78,7 @@ export interface SingleQuestion extends BaseStep {
isAnswered: boolean; // Был ли вопрос уже отвечен
text: string; // Основной текст вопроса
popups: Popup[]; // Всплывающие окна для отображения после ответа
+ videoUrl: string;
}
/**
diff --git a/projects/skills/src/models/trajectory.model.ts b/projects/skills/src/models/trajectory.model.ts
index ddf1a075c..9d2f9eae8 100644
--- a/projects/skills/src/models/trajectory.model.ts
+++ b/projects/skills/src/models/trajectory.model.ts
@@ -1,7 +1,7 @@
/** @format */
import type { UserData } from "./profile.model";
-import type { Skill } from "./skill.model";
+import type { Skill } from "../../../social_platform/src/app/office/models/skill.model";
/**
* Информация о навыке в контексте траектории
diff --git a/projects/skills/src/styles.scss b/projects/skills/src/styles.scss
index d19efbe44..5f3eec7e4 100644
--- a/projects/skills/src/styles.scss
+++ b/projects/skills/src/styles.scss
@@ -20,7 +20,7 @@
@import "styles/pages/project-detail.scss";
:root {
- --app-height: 100%;
+ --app-height: 100vh;
}
html,
diff --git a/projects/skills/src/styles/_colors.scss b/projects/skills/src/styles/_colors.scss
index c30eec8b2..5f321991a 100644
--- a/projects/skills/src/styles/_colors.scss
+++ b/projects/skills/src/styles/_colors.scss
@@ -12,25 +12,24 @@
--accent-light: #9a80e6;
// GOLD
- --gold: #f6ff8b;
- --gold-dark: #f7cf4d;
+ --gold: #e5b25d;
+ --gold-dark: #c69849;
// GRAY
- --white: #fafafa;
--light-white: #fff;
+ --white: #fafafa;
--black: #333;
- --dark-grey: #e7e7e7;
+ --dark-grey: #a59fb9;
--gray: #d3d3d3;
--light-gray: #f9f9f9;
--grey-button: #e5e5e5e5;
- --gray-for-shadow: rgb(159 159 159 / 15%);
--medium-grey-for-outline: #eee;
- --grey-for-text: #a59fb9;
+ --grey-for-text: #827e80;
// FUNCTIONAL
- --green: #92e3a9;
- --light-green: #e3f8e9;
- --red: rgb(242 66 72 / 50%);
- --light-red: #ffd2d2;
+ --green: #88c9a1;
+ --green-dark: #297373;
+ --lime: #d6ff54;
+ --red: #d48a9e;
--red-dark: #{color.adjust(#d48a9e, $blackness: 10%)};
}
diff --git a/projects/skills/src/styles/_global.scss b/projects/skills/src/styles/_global.scss
index 124c807ba..e29a38145 100644
--- a/projects/skills/src/styles/_global.scss
+++ b/projects/skills/src/styles/_global.scss
@@ -10,10 +10,10 @@
margin: 0;
}
-html,
-body {
- overflow-y: hidden;
-}
+// html,
+// body {
+// overflow-y: hidden;
+// }
ul,
li,
@@ -31,10 +31,6 @@ img {
max-width: 100%;
}
-iframe {
- border-radius: 15px;
-}
-
a,
.link {
color: var(--accent);
@@ -74,16 +70,21 @@ button {
label.field-label {
display: block;
margin-bottom: 6px;
+ margin-left: 12px;
color: var(--black);
- @include body-14;
+ @include body-12;
}
.error {
display: block;
gap: 10px;
margin-top: 6px;
- color: var(--red);
+ color: var(--red) !important;
+
+ i {
+ color: var(--red) !important;
+ }
p {
&:not(:last-child) {
diff --git a/projects/skills/src/styles/_typography.scss b/projects/skills/src/styles/_typography.scss
index 2142c754e..e39bad451 100644
--- a/projects/skills/src/styles/_typography.scss
+++ b/projects/skills/src/styles/_typography.scss
@@ -66,9 +66,9 @@
@mixin heading-1 {
font-family: Mont, sans-serif;
- font-size: 36px;
+ font-size: 18px;
font-style: normal;
- font-weight: 700;
+ font-weight: 400;
line-height: 150%;
}
@@ -76,11 +76,23 @@
@include heading-1;
}
+@mixin heading-1-bold {
+ font-family: Mont, sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 150%;
+}
+
+.text-heading-1-bold {
+ @include heading-1-bold;
+}
+
@mixin heading-2 {
font-family: Mont, sans-serif;
- font-size: 28px;
+ font-size: 16px;
font-style: normal;
- font-weight: 700;
+ font-weight: 400;
line-height: 150%;
}
@@ -88,6 +100,18 @@
@include heading-2;
}
+@mixin heading-2-bold {
+ font-family: Mont, sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 150%;
+}
+
+.text-heading-2-bold {
+ @include heading-2-bold;
+}
+
@mixin heading-3 {
font-family: Mont, sans-serif;
font-size: 24px;
@@ -168,7 +192,7 @@
line-height: 150%;
}
-.text-bold-body-14 {
+.text-body-bold-14 {
@include bold-body-14;
}
@@ -176,7 +200,7 @@
font-family: Mont, sans-serif;
font-size: 14px;
font-style: normal;
- font-weight: 600;
+ font-weight: 400;
line-height: 150%;
}
@@ -188,7 +212,7 @@
font-family: Mont, sans-serif;
font-size: 12px;
font-style: normal;
- font-weight: 600;
+ font-weight: 400;
line-height: 130%;
}
@@ -196,6 +220,18 @@
@include body-12;
}
+@mixin body-bold-12 {
+ font-family: Mont, sans-serif;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 130%;
+}
+
+.text-body-bold-12 {
+ @include body-bold-12;
+}
+
@mixin body-10 {
font-family: Mont, sans-serif;
font-size: 10px;
@@ -208,3 +244,15 @@
.text-body-10 {
@include body-10;
}
+
+@mixin body-6 {
+ font-family: Mont, sans-serif;
+ font-size: 6px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 130%;
+}
+
+.text-body-6 {
+ @include body-6;
+}
diff --git a/projects/skills/src/styles/pages/auth.scss b/projects/skills/src/styles/pages/auth.scss
index d901d3d94..b82a86567 100644
--- a/projects/skills/src/styles/pages/auth.scss
+++ b/projects/skills/src/styles/pages/auth.scss
@@ -4,21 +4,25 @@
@use "styles/typography";
.auth {
+ padding: 100px 0;
+
&__logo {
position: fixed;
- top: 85px;
- left: 15px;
- display: none;
+ top: 45px;
+ left: 18px;
+ display: block;
+ width: 95px;
@include responsive.apply-desktop {
- top: 50px;
- left: 100px;
- display: block;
+ top: 120px;
+ left: 200px;
+ width: 157px;
+ height: 30px;
}
}
&__greeting {
- max-width: 915px;
+ max-width: 510px;
margin-bottom: 20px;
@include responsive.apply-desktop {
@@ -28,7 +32,7 @@
&__wrapper {
@include responsive.apply-desktop {
- max-width: 415px;
+ max-width: 333px;
}
}
@@ -40,7 +44,7 @@
@include typography.heading-4;
}
- @include typography.heading-2;
+ @include typography.heading-1;
@include responsive.apply-desktop {
&--register {
@@ -50,11 +54,7 @@
}
&__info {
- @include typography.body-12;
-
- @include responsive.apply-desktop {
- @include typography.body-14;
- }
+ @include typography.heading-2;
}
&__field {
@@ -125,17 +125,9 @@
}
&__toggle {
- @include typography.body-14;
-
- margin-top: 8vh;
- text-align: center;
-
- @include responsive.apply-desktop {
- @include typography.body-16;
+ @include typography.body-12;
- margin-top: 20px;
- text-align: left;
- }
+ margin-top: 4vh;
}
a {
diff --git a/projects/social_platform/src/app/auth/models/user.model.ts b/projects/social_platform/src/app/auth/models/user.model.ts
index 402ca5dea..dce1893ac 100644
--- a/projects/social_platform/src/app/auth/models/user.model.ts
+++ b/projects/social_platform/src/app/auth/models/user.model.ts
@@ -2,7 +2,7 @@
import { Project } from "@models/project.model";
import { FileModel } from "@office/models/file.model";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { Program } from "@office/program/models/program.model";
/**
diff --git a/projects/social_platform/src/app/auth/services/profile.service.ts b/projects/social_platform/src/app/auth/services/profile.service.ts
index 984a0e909..ae1363520 100644
--- a/projects/social_platform/src/app/auth/services/profile.service.ts
+++ b/projects/social_platform/src/app/auth/services/profile.service.ts
@@ -5,7 +5,7 @@ import { ApiService } from "projects/core";
import { Achievement } from "../models/user.model";
import { map, Observable } from "rxjs";
import { plainToInstance } from "class-transformer";
-import { Approve } from "@office/models/skill";
+import { Approve } from "@office/models/skill.model";
/**
* Сервис управления профилем пользователя
diff --git a/projects/social_platform/src/app/core/services/file.service.ts b/projects/social_platform/src/app/core/services/file.service.ts
index 8f3bb4819..d21f68697 100644
--- a/projects/social_platform/src/app/core/services/file.service.ts
+++ b/projects/social_platform/src/app/core/services/file.service.ts
@@ -33,17 +33,44 @@ export class FileService {
formData.append("file", file);
return new Observable<{ url: string }>(observer => {
- fetch(`${environment.apiUrl}${this.FILES_URL}/`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${this.tokenService.getTokens()?.access}`,
- },
- body: formData,
- })
- .then(res => res.json())
+ const doFetch = (token: string) =>
+ fetch(`${environment.apiUrl}${this.FILES_URL}/`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${token}` },
+ body: formData,
+ });
+
+ const token = this.tokenService.getTokens()?.access;
+ if (!token) {
+ observer.error(new Error("No access token"));
+ return;
+ }
+
+ doFetch(token)
+ .then(res => {
+ if (res.status === 401) {
+ this.tokenService.refreshTokens().subscribe({
+ next: newTokens => {
+ this.tokenService.memTokens(newTokens);
+ doFetch(newTokens.access)
+ .then(r => r.json())
+ .then(data => {
+ observer.next(data);
+ observer.complete();
+ })
+ .catch(err => observer.error(err));
+ },
+ error: err => observer.error(err),
+ });
+ return;
+ }
+ return res.json();
+ })
.then(res => {
- observer.next(res);
- observer.complete();
+ if (res) {
+ observer.next(res);
+ observer.complete();
+ }
})
.catch(err => observer.error(err));
});
diff --git a/projects/social_platform/src/app/office/courses/courses.component.html b/projects/social_platform/src/app/office/courses/courses.component.html
new file mode 100644
index 000000000..d8faa75eb
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/courses.component.html
@@ -0,0 +1,18 @@
+
+
+
diff --git a/projects/social_platform/src/app/office/courses/courses.component.scss b/projects/social_platform/src/app/office/courses/courses.component.scss
new file mode 100644
index 000000000..53c48f8bc
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/courses.component.scss
@@ -0,0 +1,10 @@
+@use "styles/typography";
+@use "styles/responsive";
+
+.profile {
+ &__info {
+ display: grid;
+ grid-template-columns: 8fr 2fr;
+ column-gap: 20px;
+ }
+}
diff --git a/projects/skills/src/app/profile/shared/info-block/info-block.component.spec.ts b/projects/social_platform/src/app/office/courses/courses.component.spec.ts
similarity index 53%
rename from projects/skills/src/app/profile/shared/info-block/info-block.component.spec.ts
rename to projects/social_platform/src/app/office/courses/courses.component.spec.ts
index 107ae0586..a58d1165f 100644
--- a/projects/skills/src/app/profile/shared/info-block/info-block.component.spec.ts
+++ b/projects/social_platform/src/app/office/courses/courses.component.spec.ts
@@ -2,18 +2,18 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { InfoBlockComponent } from "./info-block.component";
+import { TrackCareerComponent } from "./courses.component";
-describe("InfoBlockComponent", () => {
- let component: InfoBlockComponent;
- let fixture: ComponentFixture;
+describe("TrackCareerComponent", () => {
+ let component: TrackCareerComponent;
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [InfoBlockComponent],
+ imports: [TrackCareerComponent],
}).compileComponents();
- fixture = TestBed.createComponent(InfoBlockComponent);
+ fixture = TestBed.createComponent(TrackCareerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/projects/social_platform/src/app/office/courses/courses.component.ts b/projects/social_platform/src/app/office/courses/courses.component.ts
new file mode 100644
index 000000000..14673e447
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/courses.component.ts
@@ -0,0 +1,40 @@
+/** @format */
+
+import { CommonModule } from "@angular/common";
+import { Component, inject } from "@angular/core";
+import { RouterModule } from "@angular/router";
+import { BackComponent } from "@uilib";
+import { SearchComponent } from "@ui/components/search/search.component";
+import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
+import { SoonCardComponent } from "@office/shared/soon-card/soon-card.component";
+
+/**
+ * Главный компонент модуля отслеживания карьерных траекторий
+ * Служит контейнером для дочерних компонентов и маршрутизации
+ * Отображает навигационную панель с вкладками "Траектории" и "Моя траектория"
+ */
+@Component({
+ selector: "app-track-career",
+ standalone: true,
+ imports: [
+ CommonModule,
+ RouterModule,
+ BackComponent,
+ SearchComponent,
+ ReactiveFormsModule,
+ SoonCardComponent,
+ ],
+ templateUrl: "./courses.component.html",
+ styleUrl: "./courses.component.scss",
+})
+export class CoursesComponent {
+ private readonly fb = inject(FormBuilder);
+
+ constructor() {
+ this.searchForm = this.fb.group({
+ search: [""],
+ });
+ }
+
+ searchForm: FormGroup;
+}
diff --git a/projects/social_platform/src/app/office/courses/courses.resolver.ts b/projects/social_platform/src/app/office/courses/courses.resolver.ts
new file mode 100644
index 000000000..8d269ba6f
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/courses.resolver.ts
@@ -0,0 +1,20 @@
+/** @format */
+
+import { inject } from "@angular/core";
+import { CoursesService } from "./courses.service";
+
+/**
+ * Резолвер для загрузки списка всех доступных траекторий
+ * Выполняется перед активацией маршрута для предзагрузки данных
+ * @returns Observable с массивом траекторий (20 элементов с offset 0)
+ */
+
+/**
+ * Функция-резолвер для получения списка траекторий
+ * @returns Promise/Observable с данными траекторий
+ */
+export const CoursesResolver = () => {
+ const coursesService = inject(CoursesService);
+
+ return coursesService.getCourses();
+};
diff --git a/projects/social_platform/src/app/office/courses/courses.routes.ts b/projects/social_platform/src/app/office/courses/courses.routes.ts
new file mode 100644
index 000000000..ac341aa9f
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/courses.routes.ts
@@ -0,0 +1,39 @@
+/** @format */
+
+import { Routes } from "@angular/router";
+import { CoursesListComponent } from "./list/list.component";
+import { CoursesComponent } from "./courses.component";
+import { CoursesResolver } from "./courses.resolver";
+
+/**
+ * Конфигурация маршрутов для модуля карьерных траекторий
+ * Определяет структуру навигации:
+ * - "" - редирект на "all"
+ * - "all" - список всех доступных траекторий
+ * - ":courseId" - детальная информация о конкретном курсе
+ */
+
+export const COURSES_ROUTES: Routes = [
+ {
+ path: "",
+ component: CoursesComponent,
+ children: [
+ {
+ path: "",
+ redirectTo: "all",
+ pathMatch: "full",
+ },
+ {
+ path: "all",
+ component: CoursesListComponent,
+ resolve: {
+ data: CoursesResolver,
+ },
+ },
+ ],
+ },
+ {
+ path: ":courseId",
+ loadChildren: () => import("./detail/course-detail.routes").then(c => c.COURSE_DETAIL_ROUTES),
+ },
+];
diff --git a/projects/social_platform/src/app/office/courses/courses.service.ts b/projects/social_platform/src/app/office/courses/courses.service.ts
new file mode 100644
index 000000000..629fa14ea
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/courses.service.ts
@@ -0,0 +1,87 @@
+/** @format */
+
+import { Injectable } from "@angular/core";
+import { ApiService } from "@corelib";
+import {
+ CourseCard,
+ CourseDetail,
+ CourseLesson,
+ CourseStructure,
+ TaskAnswerResponse,
+} from "@office/models/courses.model";
+import { Observable } from "rxjs";
+
+/**
+ * Сервис Курсов
+ *
+ * Управляет всеми операциями, связанными с курсами, включая:
+ * - Отслеживание прогресса пользователя по курсу
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class CoursesService {
+ private readonly COURSE_URL = "/courses";
+
+ constructor(private readonly apiService: ApiService) {}
+
+ /**
+ * Получает доступные курсов
+ *
+ * @returns Observable - Список доступных курсов
+ */
+ getCourses(): Observable {
+ return this.apiService.get(`${this.COURSE_URL}/`);
+ }
+
+ /**
+ * Получает подробную информацию о конкретном курсе
+ *
+ * @param id - Уникальный идентификатор курса
+ * @returns Observable - Полная информация о курсе
+ */
+ getCourseDetail(id: number): Observable {
+ return this.apiService.get(`${this.COURSE_URL}/${id}/`);
+ }
+
+ /**
+ * Получает подробную информацию о структуре курса
+ *
+ * @param id - Уникальный идентификатор курса
+ * @returns Observable - Полную структуру курса
+ */
+ getCourseStructure(id: number): Observable {
+ return this.apiService.get(`${this.COURSE_URL}/${id}/structure/`);
+ }
+
+ /**
+ * Получает полную информацию по отдельному уроку внутри курса
+ *
+ * @param id - Уникальный идентификатор урока
+ * @returns Observable - Полная информация для урока конкретного
+ */
+ getCourseLesson(id: number): Observable {
+ return this.apiService.get(`${this.COURSE_URL}/lessons/${id}/`);
+ }
+
+ /**
+ *
+ * @param id - Уникальный идентификатор задачи
+ * @param answerText - Текст ответа
+ * @param optionIds - id ответов выбранных
+ * @param fileIds - id файлов загруженных
+ * @returns Observable - Информация от прохождении урока
+ */
+ postAnswerQuestion(
+ id: number,
+ answerText?: any,
+ optionIds?: number[],
+ fileIds?: number[]
+ ): Observable {
+ return this.apiService.post(`${this.COURSE_URL}/tasks/${id}/answer/`, {
+ answerText,
+ optionIds,
+ fileIds,
+ });
+ }
+}
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
new file mode 100644
index 000000000..e874a8146
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/course-detail.component.html
@@ -0,0 +1,47 @@
+
+
+
+
+ @if(course()) {
+
+
+
+
+
+ @if (!isTaskDetail()) {
+
{{ course()!.title }}
+ }
+
+
+
+
+
+
{{ isTaskDetail() ? "назад к модулю" : "назад" }}
+
+
вернуться в программу
+
+
+
+
+ }
+
+
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
new file mode 100644
index 000000000..5383399fb
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/course-detail.component.scss
@@ -0,0 +1,114 @@
+@use "styles/responsive";
+@use "styles/typography";
+
+$detail-bar-height: 63px;
+$detail-bar-mb: 12px;
+
+.detail {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ max-height: 100%;
+ padding-top: 20px;
+
+ &__body {
+ flex-grow: 1;
+ max-height: calc(100% - #{$detail-bar-height} - #{$detail-bar-mb});
+ padding-bottom: 12px;
+ }
+}
+
+.info {
+ $body-slide: 15px;
+
+ position: relative;
+ padding: 0;
+ border: none;
+ border-radius: $body-slide;
+
+ &__cover {
+ position: relative;
+ width: 100%;
+ height: 136px;
+ border-radius: 15px 15px 0 0;
+
+ img {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ &__body {
+ position: relative;
+ z-index: 2;
+ }
+
+ &__avatar {
+ position: absolute;
+ bottom: -10px;
+ left: 50%;
+ z-index: 100;
+ display: block;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: pointer;
+ border-radius: 50%;
+
+ &--program {
+ bottom: 15px;
+ }
+
+ @include responsive.apply-desktop {
+ transform: translate(-50%, 50%);
+ }
+ }
+
+ &__row {
+ display: flex;
+ gap: 20px;
+ align-items: center;
+ justify-content: center;
+ margin-top: 2px;
+
+ @include responsive.apply-desktop {
+ justify-content: unset;
+ margin-top: 0;
+ }
+ }
+
+ &__title {
+ margin-top: 10px;
+ overflow: hidden;
+ color: var(--black);
+ text-align: center;
+ text-overflow: ellipsis;
+
+ &--project {
+ transform: translateX(-31%);
+ }
+ }
+
+ &__text {
+ color: var(--dark-grey);
+ }
+
+ &__actions {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 180px;
+ align-items: center;
+ padding: 24px 0 30px;
+
+ &--disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+ }
+}
diff --git a/projects/skills/src/app/rating/shared/basic-rating-card/basic-rating-card.component.spec.ts b/projects/social_platform/src/app/office/courses/detail/course-detail.component.spec.ts
similarity index 52%
rename from projects/skills/src/app/rating/shared/basic-rating-card/basic-rating-card.component.spec.ts
rename to projects/social_platform/src/app/office/courses/detail/course-detail.component.spec.ts
index 9a29e05c2..f79e3c7d4 100644
--- a/projects/skills/src/app/rating/shared/basic-rating-card/basic-rating-card.component.spec.ts
+++ b/projects/social_platform/src/app/office/courses/detail/course-detail.component.spec.ts
@@ -2,18 +2,18 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { BasicRatingCardComponent } from "./basic-rating-card.component";
+import { VacanciesDetailComponent } from "./trajectory-detail.component";
-describe("BasicRatingCardComponent", () => {
- let component: BasicRatingCardComponent;
- let fixture: ComponentFixture;
+describe("VacanciesDetailComponent", () => {
+ let component: VacanciesDetailComponent;
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [BasicRatingCardComponent],
+ imports: [VacanciesDetailComponent],
}).compileComponents();
- fixture = TestBed.createComponent(BasicRatingCardComponent);
+ fixture = TestBed.createComponent(VacanciesDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
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
new file mode 100644
index 000000000..b66e3a03b
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/course-detail.component.ts
@@ -0,0 +1,82 @@
+/** @format */
+
+import { CommonModule } from "@angular/common";
+import { Component, DestroyRef, 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 { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { CourseDetail, CourseStructure } from "@office/models/courses.model";
+
+/**
+ * Компонент детального просмотра траектории
+ * Отображает навигационную панель и служит контейнером для дочерних компонентов
+ * Управляет состоянием выбранной траектории и ID траектории из URL
+ */
+@Component({
+ selector: "app-course-detail",
+ standalone: true,
+ imports: [CommonModule, RouterOutlet, AvatarComponent, ButtonComponent],
+ templateUrl: "./course-detail.component.html",
+ styleUrl: "./course-detail.component.scss",
+})
+export class CourseDetailComponent implements OnInit {
+ private readonly route = inject(ActivatedRoute);
+ private readonly router = inject(Router);
+ private readonly destroyRef = inject(DestroyRef);
+
+ protected readonly isTaskDetail = signal(false);
+ protected readonly isDisabled = signal(false);
+
+ protected readonly courseModules = signal([]);
+ protected readonly course = signal(undefined);
+
+ /**
+ * Инициализация компонента
+ * Подписывается на параметры маршрута и данные траектории
+ */
+ ngOnInit(): void {
+ this.route.data
+ .pipe(
+ map(data => data["data"]),
+ filter(course => !!course),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe({
+ next: ([course, _]: [CourseDetail, CourseStructure]) => {
+ this.course.set(course);
+
+ if (!course.partnerProgramId) {
+ this.isDisabled.set(true);
+ }
+ },
+ });
+
+ this.isTaskDetail.set(this.router.url.includes("lesson"));
+
+ this.router.events
+ .pipe(
+ filter((event): event is NavigationEnd => event instanceof NavigationEnd),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => {
+ this.isTaskDetail.set(this.router.url.includes("lesson"));
+ });
+ }
+
+ /**
+ * Перенаправляет на страницу с информацией в завивисимости от listType
+ */
+ redirectDetailInfo(courseId?: number): void {
+ if (courseId != null) {
+ this.router.navigateByUrl(`/office/courses/${courseId}`);
+ } else {
+ this.router.navigateByUrl("/office/courses/all");
+ }
+ }
+
+ redirectToProgram(): void {
+ this.router.navigateByUrl(`/office/program/${this.course()?.partnerProgramId}`);
+ }
+}
diff --git a/projects/social_platform/src/app/office/courses/detail/course-detail.resolver.ts b/projects/social_platform/src/app/office/courses/detail/course-detail.resolver.ts
new file mode 100644
index 000000000..dc4553380
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/course-detail.resolver.ts
@@ -0,0 +1,30 @@
+/** @format */
+
+import { inject } from "@angular/core";
+import type { ActivatedRouteSnapshot } from "@angular/router";
+import { Router } from "@angular/router";
+import { CoursesService } from "../courses.service";
+import { forkJoin, tap } from "rxjs";
+
+/**
+ * Резолвер для получения детальной информации о курсе
+ * Также проверяет isAvailable — если false, редиректит на список курсов
+ * @param route - снимок маршрута содержащий параметр courseId
+ * @returns Observable с данными о курсе
+ */
+export const CoursesDetailResolver = (route: ActivatedRouteSnapshot) => {
+ const coursesService = inject(CoursesService);
+ const router = inject(Router);
+ const courseId = route.parent?.params["courseId"];
+
+ return forkJoin([
+ coursesService.getCourseDetail(courseId).pipe(
+ tap(course => {
+ if (!course.isAvailable) {
+ router.navigate(["/office/courses/all"]);
+ }
+ })
+ ),
+ coursesService.getCourseStructure(courseId),
+ ]);
+};
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
new file mode 100644
index 000000000..8b168483c
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/course-detail.routes.ts
@@ -0,0 +1,27 @@
+/** @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";
+
+export const COURSE_DETAIL_ROUTES: Routes = [
+ {
+ path: "",
+ component: CourseDetailComponent,
+ runGuardsAndResolvers: "always",
+ resolve: {
+ data: CoursesDetailResolver,
+ },
+ children: [
+ {
+ path: "",
+ component: TrajectoryInfoComponent,
+ },
+ {
+ path: "lesson",
+ loadChildren: () => import("../lesson/lesson.routes").then(m => m.LESSON_ROUTES),
+ },
+ ],
+ },
+];
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
new file mode 100644
index 000000000..1902dec97
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/info/info.component.html
@@ -0,0 +1,80 @@
+
+
+@if (courseStructure()) {
+
+
+
+
+
+
+
+
прогресс по курсу
+
{{ courseStructure()!.percent }}%
+
+
+
+
+ @for (courseModule of courseStructure()!.modules; track courseModule) {
+
+ } @empty {
+
на данные момент модулей нет!
+ }
+
+
+
+
+
+
+
о курсе
+
+
+ @if (courseDetail()!.description) {
+
+
+ @if (descriptionExpandable) {
+
+ {{ readFullDescription ? "cкрыть" : "подробнее" }}
+
+ }
+
+ }
+
+
+
+
+
+
+
+
+ {{ isCourseCompleted() ? "ты прошел курс!" : "ты прошел модуль!" }}
+
+
+
+
+
отлично
+
+
+
+}
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
new file mode 100644
index 000000000..fa96d9440
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/info/info.component.scss
@@ -0,0 +1,181 @@
+@use "styles/responsive";
+@use "styles/typography";
+
+.course {
+ padding-top: 12px;
+ padding-bottom: 100px;
+
+ @include responsive.apply-desktop {
+ padding-bottom: 0;
+ }
+
+ &__main {
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ &__right {
+ display: flex;
+ flex-direction: column;
+ max-height: 226px;
+ }
+
+ &__left {
+ max-width: 157px;
+ }
+
+ &__section {
+ padding: 24px;
+ margin-bottom: 14px;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ }
+
+ &__info {
+ @include responsive.apply-desktop {
+ grid-column: span 3;
+ }
+ }
+
+ &__details {
+ display: grid;
+ grid-template-columns: 2fr 5fr 3fr;
+ grid-gap: 20px;
+ }
+
+ &__progress {
+ position: relative;
+ height: 48px;
+ padding: 10px;
+ margin-bottom: 18px;
+ overflow: hidden;
+ text-align: center;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+
+ &--cover {
+ position: absolute;
+ top: 0%;
+ left: 0%;
+ height: 48px;
+ background-color: var(--accent);
+ border-radius: var(--rounded-lg);
+ opacity: 0.15;
+ }
+
+ &--percent {
+ position: relative;
+ z-index: 1;
+ color: var(--green-dark);
+ }
+
+ &--complete {
+ background-color: var(--green-light);
+
+ .course__progress--percent {
+ color: var(--black);
+ }
+
+ .course__progress--cover {
+ background-color: var(--green-dark);
+ }
+ }
+ }
+
+ &__modules {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-top: 18px;
+ }
+}
+
+.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;
+ align-items: center;
+ justify-content: center;
+ max-height: calc(100vh - 40px);
+ padding: 0 200px;
+
+ &__text {
+ color: var(--dark-grey);
+ text-align: center;
+ }
+
+ &__img {
+ margin: 30px 0;
+ }
+}
diff --git a/projects/skills/src/app/skills/detail/detail.component.spec.ts b/projects/social_platform/src/app/office/courses/detail/info/info.component.spec.ts
similarity index 56%
rename from projects/skills/src/app/skills/detail/detail.component.spec.ts
rename to projects/social_platform/src/app/office/courses/detail/info/info.component.spec.ts
index b92741065..6ced6bd14 100644
--- a/projects/skills/src/app/skills/detail/detail.component.spec.ts
+++ b/projects/social_platform/src/app/office/courses/detail/info/info.component.spec.ts
@@ -2,18 +2,18 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { DetailComponent } from "./detail.component";
+import { InfoComponent } from "./info.component";
-describe("DetailComponent", () => {
- let component: DetailComponent;
- let fixture: ComponentFixture;
+describe("InfoComponent", () => {
+ let component: InfoComponent;
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [DetailComponent],
+ imports: [InfoComponent],
}).compileComponents();
- fixture = TestBed.createComponent(DetailComponent);
+ fixture = TestBed.createComponent(InfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
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
new file mode 100644
index 000000000..5e15b937d
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/detail/info/info.component.ts
@@ -0,0 +1,127 @@
+/** @format */
+
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ DestroyRef,
+ ElementRef,
+ inject,
+ OnInit,
+ signal,
+ ViewChild,
+} 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 { map } from "rxjs";
+import { CommonModule } from "@angular/common";
+import { SoonCardComponent } from "@office/shared/soon-card/soon-card.component";
+import { ModalComponent } from "@ui/components/modal/modal.component";
+import { ButtonComponent } from "@ui/components";
+import { CourseModuleCardComponent } from "@office/courses/shared/course-module-card/course-module-card.component";
+import { CourseDetail, CourseStructure } from "@office/models/courses.model";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+
+/**
+ * Компонент детальной информации о траектории
+ * Отображает полную информацию о выбранной траектории пользователя:
+ * - Основную информацию (название, изображение, описание)
+ * - Временную шкалу траектории
+ * - Информацию о наставнике
+ * - Навыки (персональные, текущие, будущие, пройденные)
+ *
+ * Поддерживает навигацию к отдельным навыкам и взаимодействие с наставником
+ */
+@Component({
+ selector: "app-detail",
+ standalone: true,
+ imports: [
+ IconComponent,
+ RouterModule,
+ ParseBreaksPipe,
+ ParseLinksPipe,
+ CommonModule,
+ SoonCardComponent,
+ ModalComponent,
+ ButtonComponent,
+ CourseModuleCardComponent,
+ ],
+ templateUrl: "./info.component.html",
+ styleUrl: "./info.component.scss",
+})
+export class TrajectoryInfoComponent implements OnInit, AfterViewInit {
+ @ViewChild("descEl") descEl?: ElementRef;
+
+ 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);
+ protected readonly isCourseCompleted = signal(false);
+
+ /**
+ * Инициализация компонента
+ * Загружает данные траектории, пользовательскую информацию и настраивает навыки
+ */
+ ngOnInit(): void {
+ this.route.parent?.data
+ .pipe(
+ map(r => r["data"]),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(([courseDetail, courseStructure]: [CourseDetail, CourseStructure]) => {
+ this.courseStructure.set(courseStructure);
+ this.courseDetail.set(courseDetail);
+
+ const completedModuleIds = courseStructure.modules
+ .filter(m => m.progressStatus === "completed")
+ .map(m => m.id);
+
+ const unseenModule = completedModuleIds.find(
+ id =>
+ !localStorage.getItem(`course_${courseStructure.courseId}_module_${id}_complete_seen`)
+ );
+
+ if (unseenModule) {
+ const allModulesCompleted = courseStructure.modules.every(
+ m => m.progressStatus === "completed"
+ );
+ this.isCourseCompleted.set(allModulesCompleted);
+ this.isCompleteModule.set(true);
+ localStorage.setItem(
+ `course_${courseStructure.courseId}_module_${unseenModule}_complete_seen`,
+ "true"
+ );
+ }
+ });
+ }
+
+ 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.html b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.html
new file mode 100644
index 000000000..b17df2859
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
поздравялем!
+
урок пройден
+
+
+
отлично
+
+
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
new file mode 100644
index 000000000..0e2a04514
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.scss
@@ -0,0 +1,24 @@
+@use "styles/responsive";
+@use "styles/typography";
+
+.complete {
+ min-height: 358px;
+ padding: 24px;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+
+ &__block {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin: 50px 0;
+ text-align: center;
+ }
+
+ &__text {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ }
+}
diff --git a/projects/skills/src/app/task/complete/complete.component.spec.ts b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/task/complete/complete.component.spec.ts
rename to projects/social_platform/src/app/office/courses/lesson/complete/complete.component.spec.ts
diff --git a/projects/skills/src/app/task/complete/complete.component.ts b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.ts
similarity index 64%
rename from projects/skills/src/app/task/complete/complete.component.ts
rename to projects/social_platform/src/app/office/courses/lesson/complete/complete.component.ts
index ff35c1d18..16059464f 100644
--- a/projects/skills/src/app/task/complete/complete.component.ts
+++ b/projects/social_platform/src/app/office/courses/lesson/complete/complete.component.ts
@@ -1,13 +1,9 @@
/** @format */
-import { Component, inject } from "@angular/core";
+import { Component, inject, OnInit, signal } from "@angular/core";
import { CommonModule } from "@angular/common";
-import { CircleProgressBarComponent } from "../../shared/circle-progress-bar/circle-progress-bar.component";
-import { IconComponent } from "@uilib";
import { ButtonComponent } from "@ui/components";
import { ActivatedRoute, Router } from "@angular/router";
-import { map, type Observable } from "rxjs";
-import type { TaskResults } from "../../../models/skill.model";
/**
* Компонент завершения задачи
@@ -22,14 +18,22 @@ import type { TaskResults } from "../../../models/skill.model";
@Component({
selector: "app-complete",
standalone: true,
- imports: [CommonModule, CircleProgressBarComponent, IconComponent, ButtonComponent],
+ imports: [CommonModule, ButtonComponent],
templateUrl: "./complete.component.html",
styleUrl: "./complete.component.scss",
})
-export class TaskCompleteComponent {
+export class TaskCompleteComponent implements OnInit {
route = inject(ActivatedRoute); // Сервис для работы с активным маршрутом
router = inject(Router); // Сервис для навигации
+ courseId = signal(null);
- // Получаем результаты задачи из данных маршрута
- results = this.route.data.pipe(map(r => r["data"])) as Observable;
+ ngOnInit(): void {
+ const courseId = Number(this.route.parent?.parent?.parent?.snapshot.paramMap.get("courseId"));
+ this.courseId.set(isNaN(courseId) ? null : courseId);
+ }
+
+ routeToCourses(): void {
+ const id = this.courseId();
+ this.router.navigate(id ? ["/office/courses", id] : ["/office/courses/all"]);
+ }
}
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
new file mode 100644
index 000000000..7afe4c7cf
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/lesson.component.html
@@ -0,0 +1,100 @@
+
+
+@if (lessonInfo()) {
+
+
+
+
модуль {{ lessonInfo()?.moduleOrder }}
+
+
+
+ @for (task of tasks(); track task.id) {
+
+ }
+
+
+
+ @if (loading()) {
+
+ } @else {
+
+
+
+ @if (!isComplete() && currentTask(); as task) {
+
+
+ @if (task.answerType === null && (task.informationalType === 'text' || task.informationalType
+ === 'video_text' || task.informationalType === 'text_image')) {
+
+ }
+
+
+ @if (task.answerType === 'text' || task.answerType === 'text_and_files') {
+
+ }
+
+
+ @if (task.answerType === 'multiple_choice') {
+
+ }
+
+
+ @if (task.answerType === 'single_choice') {
+
+ }
+
+
+ @if (task.answerType === 'files') {
+
+ }
+
+
+
+
+
+ {{ isLastTask() ? "завершить тест" : "следующее задание" }}
+
+
+
+ }
+
+ }
+
+}
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
new file mode 100644
index 000000000..26c394963
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/lesson.component.scss
@@ -0,0 +1,80 @@
+@use "styles/typography";
+@use "styles/responsive";
+
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: auto;
+}
+
+.task {
+ display: grid;
+ grid-template-columns: 68px 1fr;
+ gap: 20px;
+
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__tasks {
+ margin-bottom: 12px;
+ text-align: center;
+ }
+
+ &__skill-name {
+ color: var(--black);
+ }
+
+ &__name {
+ color: var(--dark-grey);
+ }
+
+ &__progress {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ &__main {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__complete {
+ display: block;
+ }
+
+ &__actions {
+ display: grid;
+ grid-template-columns: 6.2fr 4fr;
+ grid-gap: 20px;
+ margin-top: 20px;
+
+ :last-child {
+ width: 333px;
+ }
+ }
+}
+
+.progress {
+ &__task {
+ height: 23px;
+ padding: 4px 0;
+ text-align: center;
+ background-color: var(--light-white);
+ border-radius: var(--rounded-xl);
+
+ &--done {
+ color: var(--light-white);
+ background-color: var(--green);
+ border: 0 !important;
+ }
+
+ &--current {
+ border: 0.5px solid var(--accent);
+ }
+ }
+}
diff --git a/projects/skills/src/app/task/task/task.component.spec.ts b/projects/social_platform/src/app/office/courses/lesson/lesson.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/task/task/task.component.spec.ts
rename to projects/social_platform/src/app/office/courses/lesson/lesson.component.spec.ts
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
new file mode 100644
index 000000000..ba51d4543
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/lesson.component.ts
@@ -0,0 +1,228 @@
+/** @format */
+
+import { Component, computed, DestroyRef, 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";
+import { filter, map, tap } from "rxjs";
+import { CourseLesson, Task } from "@office/models/courses.model";
+import { CoursesService } from "../courses.service";
+import { ButtonComponent } from "@ui/components";
+import { SnackbarService } from "@ui/services/snackbar.service";
+import { InfoTaskComponent } from "./shared/video-task/info-task.component";
+import { WriteTaskComponent } from "./shared/write-task/write-task.component";
+import { ExcludeTaskComponent } from "./shared/exclude-task/exclude-task.component";
+import { RadioSelectTaskComponent } from "./shared/radio-select-task/radio-select-task.component";
+import { FileTaskComponent } from "./shared/file-task/file-task.component";
+import { LoaderComponent } from "@ui/components/loader/loader.component";
+
+@Component({
+ selector: "app-lesson",
+ standalone: true,
+ imports: [
+ CommonModule,
+ RouterOutlet,
+ ButtonComponent,
+ InfoTaskComponent,
+ WriteTaskComponent,
+ ExcludeTaskComponent,
+ RadioSelectTaskComponent,
+ FileTaskComponent,
+ LoaderComponent,
+ ],
+ templateUrl: "./lesson.component.html",
+ styleUrl: "./lesson.component.scss",
+})
+export class LessonComponent implements OnInit {
+ private readonly router = inject(Router);
+ private readonly route = inject(ActivatedRoute);
+ private readonly destroyRef = inject(DestroyRef);
+ private readonly coursesService = inject(CoursesService);
+ private readonly snackbarService = inject(SnackbarService);
+
+ protected readonly lessonInfo = signal(undefined);
+ protected readonly isComplete = signal(false);
+ protected readonly currentTaskId = signal(null);
+
+ protected readonly loader = signal(false);
+ protected readonly loading = signal(false);
+ protected readonly success = signal(false);
+
+ protected readonly answerBody = signal(null);
+ protected readonly hasError = signal(false);
+ protected readonly completedTaskIds = signal>(new Set());
+
+ protected readonly tasks = computed(() => this.lessonInfo()?.tasks ?? []);
+
+ protected readonly currentTask = computed(() => {
+ const id = this.currentTaskId();
+ return this.tasks().find(t => t.id === id) ?? null;
+ });
+
+ protected readonly isLastTask = computed(() => {
+ const allTasksLength = this.tasks().length;
+ return allTasksLength === this.currentTask()?.order;
+ });
+
+ protected readonly isSubmitDisabled = computed(() => {
+ const task = this.currentTask();
+ const body = this.answerBody();
+ if (!task) return true;
+
+ switch (task.answerType) {
+ case "text":
+ return !body || (typeof body === "string" && !body.trim());
+ case "text_and_files":
+ return !body?.text?.trim() || !body?.fileUrls?.length;
+ case "single_choice":
+ case "multiple_choice":
+ return !body || (Array.isArray(body) && body.length === 0);
+ case "files":
+ return !body || (Array.isArray(body) && body.length === 0);
+ default:
+ return false;
+ }
+ });
+
+ ngOnInit() {
+ this.route.data
+ .pipe(
+ map(data => data["data"] as CourseLesson),
+ tap(() => {
+ this.loading.set(true);
+ }),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe({
+ next: lessonInfo => {
+ this.lessonInfo.set(lessonInfo);
+
+ // Если курс уже завершен, редирект на results
+ if (lessonInfo.progressStatus === "completed") {
+ setTimeout(() => {
+ this.loading.set(false);
+ this.router.navigate(["results"], { relativeTo: this.route });
+ }, 500);
+ return;
+ }
+
+ const nextTaskId =
+ lessonInfo.currentTaskId ??
+ lessonInfo.tasks.find(t => t.isAvailable && !t.isCompleted)?.id ??
+ null;
+
+ const allCompleted = lessonInfo.tasks.every(t => t.isCompleted);
+ const onResultsPage = this.router.url.includes("results");
+
+ if (onResultsPage && !allCompleted) {
+ // Находимся на results, но не все задания выполнены — редирект обратно
+ this.currentTaskId.set(nextTaskId);
+ setTimeout(() => {
+ this.loading.set(false);
+ this.router.navigate(["./"], { relativeTo: this.route });
+ }, 500);
+ } else if (nextTaskId === null && allCompleted) {
+ // Все задания выполнены, редирект на results
+ setTimeout(() => {
+ this.loading.set(false);
+ this.router.navigate(["results"], { relativeTo: this.route });
+ }, 500);
+ } else {
+ this.currentTaskId.set(nextTaskId);
+ setTimeout(() => this.loading.set(false), 500);
+ }
+ },
+ complete: () => {
+ setTimeout(() => this.loading.set(false), 500);
+ },
+ });
+
+ this.router.events
+ .pipe(
+ filter((event): event is NavigationEnd => event instanceof NavigationEnd),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => {
+ this.isComplete.set(this.router.url.includes("results"));
+ });
+
+ this.isComplete.set(this.router.url.includes("results"));
+ }
+
+ isCurrent(taskId: number): boolean {
+ return this.currentTaskId() === taskId;
+ }
+
+ isDone(task: Task): boolean {
+ return task.isCompleted || this.completedTaskIds().has(task.id);
+ }
+
+ onSubmitAnswer() {
+ const task = this.currentTask();
+ if (!task) return;
+
+ this.loader.set(true);
+
+ const body = this.answerBody();
+ const isTextFile = task.answerType === "text_and_files";
+ const answerText = task.answerType === "text" || isTextFile ? body?.text : undefined;
+ const optionIds =
+ task.answerType === "single_choice" || task.answerType === "multiple_choice"
+ ? body
+ : undefined;
+ const fileIds = task.answerType === "files" ? body : isTextFile ? body?.fileUrls : undefined;
+
+ this.coursesService
+ .postAnswerQuestion(task.id, answerText, optionIds, fileIds)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ next: res => {
+ this.loader.set(false);
+
+ if (res.isCorrect) {
+ this.success.set(true);
+ this.hasError.set(false);
+ this.completedTaskIds.update(ids => new Set([...ids, task.id]));
+ this.snackbarService.success("правильный ответ, продолжайте дальше");
+ } else {
+ this.hasError.set(true);
+ this.success.set(false);
+ this.snackbarService.error("неверный ответ, попробуйте еще раз!");
+ setTimeout(() => this.hasError.set(false), 1000);
+ return;
+ }
+
+ if (!res.canContinue) return;
+
+ setTimeout(() => {
+ const nextId = res.nextTaskId ?? this.getNextTask()?.id ?? null;
+
+ if (nextId) {
+ this.currentTaskId.set(nextId);
+ this.success.set(false);
+ this.answerBody.set(null);
+ } else {
+ this.router.navigate(["results"], { relativeTo: this.route });
+ }
+ }, 1000);
+ },
+ error: () => {
+ this.loader.set(false);
+ this.hasError.set(true);
+ this.snackbarService.error("неверный ответ, попробуйте еще раз!");
+ },
+ });
+ }
+
+ private getNextTask(): Task | null {
+ const currentId = this.currentTaskId();
+ const allTasks = this.tasks();
+ const currentIndex = allTasks.findIndex(t => t.id === currentId);
+ const next = allTasks.slice(currentIndex + 1).find(t => t.isAvailable && !t.isCompleted);
+ return next ?? null;
+ }
+
+ onAnswerChange(value: any) {
+ this.answerBody.set(value);
+ }
+}
diff --git a/projects/skills/src/app/task/task.resolver.ts b/projects/social_platform/src/app/office/courses/lesson/lesson.resolver.ts
similarity index 62%
rename from projects/skills/src/app/task/task.resolver.ts
rename to projects/social_platform/src/app/office/courses/lesson/lesson.resolver.ts
index 89155558f..3c05ac9e7 100644
--- a/projects/skills/src/app/task/task.resolver.ts
+++ b/projects/social_platform/src/app/office/courses/lesson/lesson.resolver.ts
@@ -2,8 +2,8 @@
import type { ResolveFn } from "@angular/router";
import { inject } from "@angular/core";
-import { TaskService } from "./services/task.service";
-import type { TaskStepsResponse } from "../../models/skill.model";
+import { CourseLesson } from "@office/models/courses.model";
+import { CoursesService } from "../courses.service";
/**
* Резолвер для получения данных задачи
@@ -11,11 +11,12 @@ import type { TaskStepsResponse } from "../../models/skill.model";
*
* @param route - объект маршрута, содержащий параметры URL (включая taskId)
* @param _state - состояние маршрутизатора (не используется)
- * @returns Promise - промис с данными о шагах задачи
+ * @returns Promise - промис с данными о шагах задачи
*/
-export const taskDetailResolver: ResolveFn = (route, _state) => {
- const taskService = inject(TaskService);
+export const lessonDetailResolver: ResolveFn = (route, _state) => {
+ const coursesService = inject(CoursesService);
+ const lessonId = route.params["lessonId"];
// Получаем ID задачи из параметров маршрута и загружаем шаги задачи
- return taskService.fetchSteps(route.params["taskId"]);
+ return coursesService.getCourseLesson(lessonId);
};
diff --git a/projects/social_platform/src/app/office/courses/lesson/lesson.routes.ts b/projects/social_platform/src/app/office/courses/lesson/lesson.routes.ts
new file mode 100644
index 000000000..7983a6cd4
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/lesson.routes.ts
@@ -0,0 +1,30 @@
+/** @format */
+
+import type { Routes } from "@angular/router";
+import { LessonComponent } from "./lesson.component";
+import { TaskCompleteComponent } from "./complete/complete.component";
+import { lessonDetailResolver } from "./lesson.resolver";
+
+/**
+ * Конфигурация маршрутов для модуля уроков
+ * Определяет структуру навигации и связывает компоненты с URL-путями
+ *
+ * Структура маршрутов:
+ * - /:lessonId - основной компонент урока
+ * - /results - компонент результатов выполнения урока
+ */
+export const LESSON_ROUTES: Routes = [
+ {
+ path: ":lessonId",
+ component: LessonComponent,
+ resolve: {
+ data: lessonDetailResolver,
+ },
+ children: [
+ {
+ path: "results",
+ component: TaskCompleteComponent,
+ },
+ ],
+ },
+];
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.html b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.html
new file mode 100644
index 000000000..4c44360ac
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.html
@@ -0,0 +1,40 @@
+
+
+
+
+
задание {{ data.order }}
+
+ @if (getSafeVideoUrl(); as videoUrl) {
+
+ } @else if (data.imageUrl) {
+
+
{{ data.title | truncate: 50 }}
+ }
+
{{ data.bodyText | truncate: 700 }}
+
+
+
+
+
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
new file mode 100644
index 000000000..4ebd7b812
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.scss
@@ -0,0 +1,123 @@
+@use "styles/responsive";
+@use "styles/typography";
+
+.exclude {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+
+ // &--hasContent {
+ // display: flex;
+ // flex-direction: row-reverse;
+ // }
+
+ &__answer {
+ color: var(--grey-for-text);
+ }
+
+ &__text {
+ margin: 23px 0;
+
+ &--hasContent {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ align-self: center;
+ justify-content: center;
+
+ iframe {
+ display: block;
+ width: 100%;
+ max-width: 400px;
+ height: auto;
+ aspect-ratio: 16 / 9;
+ border: 0;
+ border-radius: var(--rounded-lg);
+ }
+ }
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ &__item {
+ display: flex;
+ gap: 14px;
+ align-items: center;
+
+ &--success {
+ ::ng-deep {
+ app-checkbox {
+ .field--checked {
+ background-color: var(--green) !important;
+ border-color: var(--green) !important;
+ }
+ }
+ }
+ }
+
+ &--error {
+ ::ng-deep {
+ app-checkbox {
+ .field--checked {
+ background-color: var(--red) !important;
+ border-color: var(--red) !important;
+ }
+ }
+ }
+ }
+
+ label {
+ cursor: pointer;
+ }
+ }
+
+ &__description {
+ text-align: justify;
+ }
+
+ &__image {
+ width: 100%;
+ max-width: 300px;
+ height: 100%;
+ max-height: 150px;
+ margin: 22px 0;
+ object-fit: cover;
+ border-radius: var(--rounded-lg);
+ }
+
+ &__body,
+ &__content {
+ flex-grow: 1;
+ min-height: 358px;
+ padding: 24px;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ }
+
+ // &__body {
+ // &--hasContent {
+ // max-width: 333px;
+ // }
+ // }
+
+ &__content {
+ &--hasContent {
+ min-width: 333px;
+ }
+
+ &:not(&--hasContent) {
+ max-width: 333px;
+ }
+ }
+
+ :host ::ng-deep &__hint p {
+ margin-top: 20px;
+
+ @include typography.body-14;
+ }
+}
diff --git a/projects/skills/src/app/task/shared/exclude-task/exclude-task.component.spec.ts b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/task/shared/exclude-task/exclude-task.component.spec.ts
rename to projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.spec.ts
diff --git a/projects/skills/src/app/task/shared/exclude-task/exclude-task.component.ts b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.ts
similarity index 57%
rename from projects/skills/src/app/task/shared/exclude-task/exclude-task.component.ts
rename to projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.ts
index 499bc451e..3d6cfff03 100644
--- a/projects/skills/src/app/task/shared/exclude-task/exclude-task.component.ts
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/exclude-task/exclude-task.component.ts
@@ -2,9 +2,12 @@
import { Component, EventEmitter, inject, Input, type OnInit, Output, signal } from "@angular/core";
import { CommonModule } from "@angular/common";
-import type { ExcludeQuestion, ExcludeQuestionResponse } from "../../../../models/step.model";
-import { DomSanitizer, type SafeResourceUrl } from "@angular/platform-browser";
-import { ParseBreaksPipe, YtExtractService } from "@corelib";
+import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
+import { CheckboxComponent } from "@ui/components";
+import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
+import { Task } from "@office/models/courses.model";
+import { resolveVideoUrlForIframe } from "@utils/video-url-embed";
+import { ImagePreviewDirective } from "../image-preview/image-preview.directive";
/**
* Компо��ент задачи на исключение лишнего
@@ -29,39 +32,50 @@ import { ParseBreaksPipe, YtExtractService } from "@corelib";
@Component({
selector: "app-exclude-task",
standalone: true,
- imports: [CommonModule, ParseBreaksPipe],
+ imports: [CommonModule, TruncatePipe, CheckboxComponent, ImagePreviewDirective],
templateUrl: "./exclude-task.component.html",
styleUrl: "./exclude-task.component.scss",
})
export class ExcludeTaskComponent implements OnInit {
- @Input({ required: true }) data!: ExcludeQuestion; // Данные вопроса
+ private readonly sanitizer = inject(DomSanitizer);
+
+ @Input({ required: true }) data!: Task; // Данные вопроса
@Input() hint!: string; // Текст подсказки
@Output() update = new EventEmitter(); // Событие обновления выбранных ответов
@Input() success = false; // Флаг успешного выполнения
- // Сеттер для обработки ошибок и сброса состояния
@Input()
- set error(value: ExcludeQuestionResponse | null) {
+ set error(value: boolean) {
this._error.set(value);
- value !== null && this.result.set([]); // Сбрасываем выбранные ответы при ошибке
+ if (value) {
+ setTimeout(() => {
+ this.result.set([]);
+ this._error.set(false);
+ }, 1000);
+ }
}
get error() {
return this._error();
}
- // Состояние компонента
- result = signal([]); // Массив ID выбранных ответов
- _error = signal(null); // Состояние ошибки
+ result = signal([]);
+ _error = signal(false);
+
+ getSafeVideoUrl(): SafeResourceUrl | null {
+ const iframeUrl = resolveVideoUrlForIframe(this.data?.videoUrl);
+ if (!iframeUrl) {
+ return null;
+ }
- sanitizer = inject(DomSanitizer); // Сервис для безопасной работы с HTML
- ytExtractService = inject(YtExtractService); // Сервис для извлечения YouTube ссылок
+ return this.sanitizer.bypassSecurityTrustResourceUrl(iframeUrl);
+ }
- videoUrl?: SafeResourceUrl; // Безопасная ссылка на видео
- description: any; // Обработанное описание
- sanitizedFileUrl?: SafeResourceUrl; // Безопасная ссылка на файл
+ hasVideoUrl(): boolean {
+ return !!resolveVideoUrlForIframe(this.data?.videoUrl);
+ }
/**
* Обработчик выбора/отмены выбора варианта ответа
@@ -80,19 +94,5 @@ export class ExcludeTaskComponent implements OnInit {
this.update.emit(this.result());
}
- ngOnInit(): void {
- // Извлекаем YouTube ссылку из описания
- const res = this.ytExtractService.transform(this.data.description);
-
- if (res.extractedLink)
- this.videoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(res.extractedLink);
-
- // Обрабатываем файлы, если они есть
- if (this.data.files.length) {
- this.sanitizedFileUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.data.files[0]);
- }
-
- // Безопасно обрабатываем HTML в описании
- this.description = this.sanitizer.bypassSecurityTrustHtml(this.data.description);
- }
+ ngOnInit(): void {}
}
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.html b/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.html
new file mode 100644
index 000000000..89c07e9a5
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
ответ
+
{{ data.answerTitle | truncate: 80 }}
+
+
+
+
+
+
загрузите файл до 100 MB
+
+
+
+ @for (file of uploadedFiles(); track $index) {
+
+ }
+
+
+
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
new file mode 100644
index 000000000..af53fa8b2
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.scss
@@ -0,0 +1,181 @@
+@use "styles/responsive";
+@use "styles/typography";
+
+.file-task {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+
+ @include responsive.apply-desktop {
+ &--hasVideo {
+ flex-direction: row;
+ }
+ }
+
+ &__header,
+ &__content {
+ flex-grow: 1;
+ min-height: 358px;
+ max-height: 393px;
+ padding: 24px;
+ background: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ }
+
+ &__content {
+ min-width: 333px;
+ padding: 12px 24px;
+
+ ::ng-deep {
+ app-upload-file {
+ .control {
+ max-height: 83px;
+ border-radius: var(--rounded-lg);
+ }
+ }
+ }
+
+ &--success {
+ border-color: var(--green);
+ }
+
+ &--error {
+ border-color: var(--red);
+ }
+ }
+
+ &__files {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 12px;
+ }
+
+ &__file-row {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ }
+
+ &__delete {
+ color: var(--red);
+ cursor: pointer;
+ transition: color 0.2s;
+
+ &:hover {
+ color: var(--red-dark);
+ }
+ }
+
+ &__upload {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ align-items: center;
+ text-align: center;
+
+ p {
+ color: var(--grey-for-text);
+ }
+ }
+
+ &__description,
+ &__text {
+ display: flex;
+ flex-direction: column;
+ margin-top: 23px;
+ white-space: pre-line;
+
+ img {
+ align-self: center;
+ }
+
+ &--hasVideo {
+ display: flex;
+ align-items: center;
+ align-self: center;
+ justify-content: center;
+ }
+
+ iframe {
+ display: block;
+ width: 100%;
+ max-width: 400px;
+ height: auto;
+ aspect-ratio: 16 / 9;
+ border: 0;
+ border-radius: var(--rounded-lg);
+ }
+ }
+
+ &__label {
+ margin-bottom: 12px;
+ color: var(--grey-for-text);
+ }
+
+ &__task {
+ color: var(--grey-for-text);
+ }
+
+ &__image {
+ width: 100%;
+ max-width: 300px;
+ height: 100%;
+ max-height: 150px;
+ border-radius: var(--rounded-lg);
+
+ &-text {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+
+ p {
+ text-align: center;
+ }
+ }
+ }
+
+ &__desc {
+ max-height: 12em;
+ overflow: hidden;
+ text-align: justify;
+ transition: max-height 0.5s ease-in-out;
+
+ &:not(&--expanded) {
+ display: box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 8;
+ }
+
+ &--expanded {
+ max-height: 210px;
+ overflow-y: auto;
+ }
+ }
+
+ &__file {
+ &-text {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 10px;
+ }
+ }
+
+ .read-more {
+ margin-top: 12px;
+ color: var(--accent);
+ cursor: pointer;
+ transition: color 0.2s;
+
+ &:hover {
+ color: var(--accent-dark);
+ }
+ }
+
+ :host ::ng-deep &__hint p {
+ margin-top: 20px;
+
+ @include typography.body-14;
+ }
+}
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.ts b/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.ts
new file mode 100644
index 000000000..b4b885498
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/file-task/file-task.component.ts
@@ -0,0 +1,138 @@
+/** @format */
+
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ inject,
+ Input,
+ Output,
+ signal,
+ ViewChild,
+} from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
+import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
+import { TruncateHtmlPipe } from "projects/core/src/lib/pipes/truncate-html.pipe";
+import { UploadFileComponent } from "@ui/components/upload-file/upload-file.component";
+import { IconComponent } from "@ui/components";
+import { FileItemComponent } from "@ui/components/file-item/file-item.component";
+import { FileService } from "@core/services/file.service";
+import { Task } from "@office/models/courses.model";
+import { FileModel } from "@office/models/file.model";
+import { resolveVideoUrlForIframe } from "@utils/video-url-embed";
+import { ImagePreviewDirective } from "../image-preview/image-preview.directive";
+
+@Component({
+ selector: "app-file-task",
+ standalone: true,
+ imports: [
+ CommonModule,
+ TruncatePipe,
+ TruncateHtmlPipe,
+ UploadFileComponent,
+ IconComponent,
+ FileItemComponent,
+ ImagePreviewDirective,
+ ],
+ templateUrl: "./file-task.component.html",
+ styleUrl: "./file-task.component.scss",
+})
+export class FileTaskComponent implements AfterViewInit {
+ private readonly fileService = inject(FileService);
+ private readonly sanitizer = inject(DomSanitizer);
+ private readonly cdRef = inject(ChangeDetectorRef);
+
+ @ViewChild("descEl") descEl?: ElementRef;
+
+ @Input({ required: true }) data!: Task;
+ @Input() success = false;
+ @Input() hint = "";
+
+ @Input()
+ set error(value: boolean) {
+ this._error.set(value);
+
+ if (value) {
+ setTimeout(() => {
+ this.uploadedFiles.set([]);
+ this._error.set(false);
+ this.update.emit([]);
+ }, 1000);
+ }
+ }
+
+ get error() {
+ return this._error();
+ }
+
+ @Output() update = new EventEmitter();
+
+ _error = signal(false);
+ uploadedFiles = signal([]);
+ descriptionExpandable = false;
+ readFullDescription = false;
+
+ ngAfterViewInit(): void {
+ const el = this.descEl?.nativeElement;
+ if (el) {
+ this.descriptionExpandable = el.scrollHeight > el.clientHeight;
+ this.cdRef.detectChanges();
+ }
+ }
+
+ onToggleDescription(): void {
+ this.readFullDescription = !this.readFullDescription;
+ }
+
+ getSafeVideoUrl(): SafeResourceUrl | null {
+ const iframeUrl = resolveVideoUrlForIframe(this.data?.videoUrl);
+ if (!iframeUrl) {
+ return null;
+ }
+
+ return this.sanitizer.bypassSecurityTrustResourceUrl(iframeUrl);
+ }
+
+ hasVideoUrl(): boolean {
+ return !!resolveVideoUrlForIframe(this.data?.videoUrl);
+ }
+
+ onFileUploaded(event: { url: string; name: string; size: number; mimeType: string }) {
+ const ext = event.name.split(".").pop()?.toLowerCase() || "";
+ const file: FileModel = {
+ name: event.name,
+ size: event.size,
+ mimeType: event.mimeType,
+ link: event.url,
+ extension: ext,
+ datetimeUploaded: new Date().toISOString(),
+ user: 0,
+ };
+
+ this.uploadedFiles.update(files => [...files, file]);
+ this.emitLinks();
+ }
+
+ onFileRemoved(index: number) {
+ const file = this.uploadedFiles()[index];
+ if (!file) return;
+
+ this.fileService.deleteFile(file.link).subscribe({
+ next: () => {
+ this.uploadedFiles.update(files => files.filter((_, i) => i !== index));
+ this.emitLinks();
+ },
+ error: () => {
+ this.uploadedFiles.update(files => files.filter((_, i) => i !== index));
+ this.emitLinks();
+ },
+ });
+ }
+
+ private emitLinks() {
+ this.update.emit(this.uploadedFiles().map(f => f.link));
+ }
+}
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/image-preview/image-preview.directive.ts b/projects/social_platform/src/app/office/courses/lesson/shared/image-preview/image-preview.directive.ts
new file mode 100644
index 000000000..0d1fd81f5
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/image-preview/image-preview.directive.ts
@@ -0,0 +1,44 @@
+/** @format */
+
+import { Directive, ElementRef, HostListener, inject, Renderer2 } from "@angular/core";
+
+@Directive({
+ selector: "img[appImagePreview]",
+ standalone: true,
+})
+export class ImagePreviewDirective {
+ private readonly el = inject(ElementRef);
+ private readonly renderer = inject(Renderer2);
+
+ constructor() {
+ this.el.nativeElement.style.cursor = "pointer";
+ }
+
+ @HostListener("click")
+ onClick(): void {
+ const src = this.el.nativeElement.src;
+ if (!src) return;
+
+ const dialog = this.renderer.createElement("dialog") as HTMLDialogElement;
+ dialog.classList.add("image-preview-dialog");
+
+ const img = this.renderer.createElement("img") as HTMLImageElement;
+ img.src = src;
+ img.classList.add("image-preview-dialog__img");
+
+ this.renderer.appendChild(dialog, img);
+ this.renderer.appendChild(document.body, dialog);
+
+ dialog.showModal();
+
+ dialog.addEventListener("click", (e: MouseEvent) => {
+ if (e.target === dialog) {
+ dialog.close();
+ }
+ });
+
+ dialog.addEventListener("close", () => {
+ dialog.remove();
+ });
+ }
+}
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/image-preview/image-preview.scss b/projects/social_platform/src/app/office/courses/lesson/shared/image-preview/image-preview.scss
new file mode 100644
index 000000000..7a9a86c09
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/image-preview/image-preview.scss
@@ -0,0 +1,51 @@
+.image-preview-dialog {
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100vw;
+ max-width: 100vw;
+ height: 100vh;
+ max-height: 100vh;
+ padding: 40px;
+ margin: 0;
+ background: transparent;
+ border: none;
+
+ &::backdrop {
+ background: rgb(0 0 0 / 60%);
+ animation: fade-in 0.25s ease-out;
+ }
+
+ &__img {
+ max-width: 90vw;
+ max-height: 85vh;
+ object-fit: contain;
+ border-radius: var(--rounded-lg);
+ animation: zoom-in 0.25s ease-out;
+ }
+}
+
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes zoom-in {
+ from {
+ opacity: 0;
+ transform: scale(0.85);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.html b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.html
new file mode 100644
index 000000000..0a6e0961b
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.html
@@ -0,0 +1,48 @@
+
+
+
+
+
задание {{ data.order }}
+ @if (getSafeVideoUrl(); as videoUrl) {
+
+ } @if (data.imageUrl) {
+
+
+
{{ data.title | truncate: 50 }}
+
+ }
+
+
{{ data.bodyText | truncate: 700 }}
+ @if (data.attachmentUrl) {
+
+ } @if (hint.length) {
+
+ }
+
+
+
+
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
new file mode 100644
index 000000000..6b9ccd070
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.scss
@@ -0,0 +1,145 @@
+@use "styles/typography";
+@use "styles/responsive";
+
+.radio {
+ @include responsive.apply-desktop {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+
+ // &--hasContent {
+ // flex-direction: row-reverse;
+ // }
+ }
+
+ &__answer {
+ align-self: baseline;
+ color: var(--grey-for-text);
+ }
+
+ &__title {
+ margin: 23px 0;
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 17px;
+ }
+
+ &__description {
+ text-align: justify;
+ }
+
+ &__img {
+ width: 100%;
+ max-width: 300px;
+ height: 100%;
+ max-height: 150px;
+ margin: 22px 0;
+ object-fit: cover;
+ border-radius: var(--rounded-lg);
+
+ &-text {
+ text-align: center;
+ }
+ }
+
+ &__item {
+ display: flex;
+ gap: 14px;
+ align-items: center;
+ cursor: pointer;
+
+ label {
+ cursor: pointer;
+ }
+
+ input[type="radio"] {
+ position: relative;
+ min-width: 18px;
+ min-height: 18px;
+ cursor: pointer;
+ background: transparent;
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: 50%;
+ outline: none;
+ appearance: none;
+ }
+
+ input[type="radio"]:checked {
+ background-color: var(--accent);
+ }
+
+ input[type="radio"]:checked::after {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 8px;
+ height: 8px;
+ content: "";
+ background-color: var(--light-white);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &--success {
+ input[type="radio"]:checked {
+ background-color: var(--green);
+ }
+ }
+
+ &--error {
+ input[type="radio"]:checked {
+ background-color: var(--red);
+ }
+ }
+ }
+
+ &__body,
+ &__content {
+ flex-grow: 1;
+ min-height: 358px;
+ padding: 24px;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ }
+
+ &__body {
+ &--hasContent {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+
+ iframe {
+ display: block;
+ width: 100%;
+ max-width: 288px;
+ height: auto;
+ aspect-ratio: 16 / 9;
+ border: 0;
+ border-radius: var(--rounded-lg);
+ }
+ }
+ }
+
+ &__content {
+ &--hasContent {
+ min-width: 333px;
+ }
+
+ &:not(&--hasContent) {
+ max-width: 333px;
+ }
+ }
+
+ :host ::ng-deep &__hint p {
+ margin-top: 20px;
+
+ @include typography.body-14;
+ }
+}
diff --git a/projects/skills/src/app/task/shared/radio-select-task/radio-select-task.component.spec.ts b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/task/shared/radio-select-task/radio-select-task.component.spec.ts
rename to projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.spec.ts
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.ts b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.ts
new file mode 100644
index 000000000..f2bb6128f
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/radio-select-task/radio-select-task.component.ts
@@ -0,0 +1,64 @@
+/** @format */
+
+import { Component, EventEmitter, Input, Output, signal } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
+import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
+import { Task } from "@office/models/courses.model";
+import { resolveVideoUrlForIframe } from "@utils/video-url-embed";
+import { FileItemComponent } from "@ui/components/file-item/file-item.component";
+import { ImagePreviewDirective } from "../image-preview/image-preview.directive";
+
+@Component({
+ selector: "app-radio-select-task",
+ standalone: true,
+ imports: [CommonModule, TruncatePipe, FileItemComponent, ImagePreviewDirective],
+ templateUrl: "./radio-select-task.component.html",
+ styleUrl: "./radio-select-task.component.scss",
+})
+export class RadioSelectTaskComponent {
+ @Input({ required: true }) data!: Task;
+ @Input() success = false;
+ @Input() hint = "";
+
+ @Input()
+ set error(value: boolean) {
+ this._error.set(value);
+
+ if (value) {
+ setTimeout(() => {
+ this.result.set({ answerId: null });
+ this._error.set(false);
+ }, 1000);
+ }
+ }
+
+ get error() {
+ return this._error();
+ }
+
+ @Output() update = new EventEmitter<{ answerId: number }>();
+
+ result = signal<{ answerId: number | null }>({ answerId: null });
+ _error = signal(false);
+
+ constructor(private sanitizer: DomSanitizer) {}
+
+ getSafeVideoUrl(): SafeResourceUrl | null {
+ const iframeUrl = resolveVideoUrlForIframe(this.data?.videoUrl);
+ if (!iframeUrl) {
+ return null;
+ }
+
+ return this.sanitizer.bypassSecurityTrustResourceUrl(iframeUrl);
+ }
+
+ hasVideoUrl(): boolean {
+ return !!resolveVideoUrlForIframe(this.data?.videoUrl);
+ }
+
+ onSelect(id: number) {
+ this.result.set({ answerId: id });
+ this.update.emit({ answerId: id });
+ }
+}
diff --git a/projects/skills/src/app/task/shared/relations-task/relations-task.component.html b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.html
similarity index 100%
rename from projects/skills/src/app/task/shared/relations-task/relations-task.component.html
rename to projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.html
diff --git a/projects/skills/src/app/task/shared/relations-task/relations-task.component.scss b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.scss
similarity index 100%
rename from projects/skills/src/app/task/shared/relations-task/relations-task.component.scss
rename to projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.scss
diff --git a/projects/skills/src/app/task/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
similarity index 100%
rename from projects/skills/src/app/task/shared/relations-task/relations-task.component.spec.ts
rename to projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.spec.ts
diff --git a/projects/skills/src/app/task/shared/relations-task/relations-task.component.ts b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.ts
similarity index 99%
rename from projects/skills/src/app/task/shared/relations-task/relations-task.component.ts
rename to projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.ts
index 6d4cd1251..de13975f9 100644
--- a/projects/skills/src/app/task/shared/relations-task/relations-task.component.ts
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/relations-task/relations-task.component.ts
@@ -20,12 +20,12 @@ import { CommonModule } from "@angular/common";
import { DomSanitizer } from "@angular/platform-browser";
import { fromEvent, type Subscription } from "rxjs";
import { debounceTime } from "rxjs/operators";
-import type {
+import { ParseBreaksPipe, ParseLinksPipe } from "@corelib";
+import {
ConnectQuestion,
ConnectQuestionRequest,
ConnectQuestionResponse,
-} from "../../../../models/step.model";
-import { ParseBreaksPipe, ParseLinksPipe } from "@corelib";
+} from "projects/skills/src/models/step.model";
/**
* Компонент задачи на установление связей
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.html b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.html
new file mode 100644
index 000000000..7ce4b2c17
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.html
@@ -0,0 +1,59 @@
+
+
+
+
+ @if ((data.videoUrl || data.attachmentUrl) && !data.imageUrl) {
+
задание {{ data.order }}
+ }
+
+
+ @if (data.videoUrl) { @if (getSafeVideoUrl(); as videoUrl) {
+
+ } } @if (data.imageUrl) {
+
+ }
+
+
+ @if (!data.videoUrl) {
+
задание {{ data.order }}
+ }
+
+
+ {{ data.title | truncate: (hasVideoUrl() ? 80 : data.imageUrl ? 100 : 200) }}
+
+
+
+ @if (data.attachmentUrl) {
+
+ }
+
+
+
+
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
new file mode 100644
index 000000000..0174367ec
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.scss
@@ -0,0 +1,75 @@
+@use "styles/responsive";
+
+.video {
+ width: 100%;
+
+ &__body {
+ display: flex;
+ flex-direction: row;
+ gap: 24px;
+ min-height: 358px;
+ padding: 24px;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+
+ iframe {
+ display: block;
+ width: 100%;
+ max-width: 400px;
+ height: auto;
+ aspect-ratio: 16 / 9;
+ border: 0;
+ border-radius: var(--rounded-lg);
+ }
+
+ &--hasImage {
+ flex-direction: row-reverse;
+ }
+
+ &--hasVideo {
+ flex-direction: column;
+ }
+ }
+
+ &__wrapper {
+ display: flex;
+ gap: 24px;
+ width: 100%;
+
+ &--hasVideo {
+ flex-direction: row;
+ flex-grow: 1;
+ }
+
+ &--hasImage {
+ flex-direction: row-reverse;
+ flex-grow: 1;
+ }
+ }
+
+ &__task {
+ color: var(--grey-for-text);
+ }
+
+ &__img {
+ align-self: center;
+ width: 100%;
+ max-width: 300px;
+ height: 100%;
+ max-height: 200px;
+ object-fit: cover;
+ border-radius: var(--rounded-lg);
+ }
+
+ &__text-content {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ width: 100%;
+ }
+
+ &__text {
+ white-space: pre-line;
+ }
+}
diff --git a/projects/skills/src/app/task/shared/video-task/info-task.component.spec.ts b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/task/shared/video-task/info-task.component.spec.ts
rename to projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.spec.ts
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.ts b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.ts
new file mode 100644
index 000000000..2eed6cf9d
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/video-task/info-task.component.ts
@@ -0,0 +1,58 @@
+/** @format */
+
+import { Component, inject, Input } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { DomSanitizer, type SafeResourceUrl } from "@angular/platform-browser";
+import { TruncateHtmlPipe } from "projects/core/src/lib/pipes/truncate-html.pipe";
+import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
+import { resolveVideoUrlForIframe } from "@utils/video-url-embed";
+import { ImagePreviewDirective } from "../image-preview/image-preview.directive";
+import { Task } from "@office/models/courses.model";
+import { FileItemComponent } from "@ui/components/file-item/file-item.component";
+
+/**
+ * Компонент информационного слайда с видео/изображением
+ * Отображает информационный контент с поддержкой различных медиа-форматов
+ *
+ * Входные параметры:
+ * @Input data - данные информационной задачи типа Task
+ *
+ * Функциональность:
+ * - Отображает текст и описание слайда
+ * - Поддерживает видео в iframe (YouTube, RuTube, Google Drive, прямые видео-файлы)
+ * - Автоматически определяет тип контента по URL/расширению файла
+ * - Адаптивная компоновка для разных типов медиа
+ */
+@Component({
+ selector: "app-info-task",
+ standalone: true,
+ imports: [CommonModule, TruncateHtmlPipe, TruncatePipe, ImagePreviewDirective, FileItemComponent],
+ templateUrl: "./info-task.component.html",
+ styleUrl: "./info-task.component.scss",
+})
+export class InfoTaskComponent {
+ @Input({ required: true }) data!: Task; // Данные информационной задачи
+
+ private readonly sanitizer = inject(DomSanitizer); // Сервис для безопасной работы с HTML
+
+ private getVideoUrl(): string | null {
+ return resolveVideoUrlForIframe(this.data?.videoUrl);
+ }
+
+ getSafeVideoUrl(): SafeResourceUrl | null {
+ const iframeUrl = this.getVideoUrl();
+ if (!iframeUrl) {
+ return null;
+ }
+
+ return this.sanitizer.bypassSecurityTrustResourceUrl(iframeUrl);
+ }
+
+ hasVideoUrl(): boolean {
+ return !!this.getVideoUrl();
+ }
+
+ hasContent(): boolean {
+ return this.hasVideoUrl() || !!this.data?.imageUrl;
+ }
+}
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
new file mode 100644
index 000000000..3cc8710c1
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
ответ
+
+
+
+
+ {{ currentLength() }} / {{ maxLength }}
+
+
+
+
+ @if (type === 'text-file') {
+
+
+
+
+
загрузите файл до 100 MB
+
+
+
+ @for (file of uploadedFiles(); track $index) {
+
+ }
+
+ }
+
+
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
new file mode 100644
index 000000000..f56118d29
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.scss
@@ -0,0 +1,130 @@
+@use "styles/responsive";
+
+.write-task {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+
+ @include responsive.apply-desktop {
+ &--hasVideo {
+ flex-direction: row;
+ }
+ }
+
+ &__header,
+ &__content {
+ flex-grow: 1;
+ min-height: 358px;
+ padding: 24px;
+ background: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ }
+
+ &__content {
+ min-width: 333px;
+ padding: 12px 24px;
+ }
+
+ &__description,
+ &__text {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 23px;
+ white-space: pre-line;
+
+ &--hasVideo {
+ display: flex;
+ align-items: center;
+ align-self: center;
+ justify-content: center;
+ }
+
+ iframe {
+ display: block;
+ width: 100%;
+ max-width: 400px;
+ height: auto;
+ aspect-ratio: 16 / 9;
+ border: 0;
+ border-radius: var(--rounded-lg);
+ }
+ }
+
+ &__input {
+ position: relative;
+ margin-top: 12px;
+ color: var(--grey-for-text);
+ }
+
+ &__textarea {
+ display: block;
+ width: 100%;
+ padding: 12px 18px;
+ resize: none;
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ outline: none;
+ scrollbar-width: none;
+ }
+
+ &__task {
+ color: var(--grey-for-text);
+ }
+
+ &__block {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ align-items: center;
+ align-self: center;
+ text-align: center;
+ }
+
+ &__image {
+ width: 100%;
+ max-width: 300px;
+ max-height: 150px;
+ object-fit: cover;
+ border-radius: var(--rounded-lg);
+ }
+
+ &__counter {
+ position: absolute;
+ right: 2%;
+ bottom: 2%;
+
+ p {
+ color: var(--grey) !important;
+ }
+ }
+
+ &__files {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 12px;
+
+ ::ng-deep {
+ app-upload-file {
+ .control {
+ max-height: 83px;
+ border-radius: var(--rounded-lg);
+ }
+ }
+ }
+ }
+
+ &__upload {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ align-items: center;
+ text-align: center;
+
+ p {
+ color: var(--grey-for-text);
+ }
+ }
+}
diff --git a/projects/skills/src/app/task/shared/write-task/write-task.component.spec.ts b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/task/shared/write-task/write-task.component.spec.ts
rename to projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.spec.ts
diff --git a/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.ts b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.ts
new file mode 100644
index 000000000..242e9da35
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/lesson/shared/write-task/write-task.component.ts
@@ -0,0 +1,114 @@
+/** @format */
+
+import { Component, EventEmitter, inject, Input, Output, signal } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
+import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
+import { UploadFileComponent } from "@ui/components/upload-file/upload-file.component";
+import { IconComponent } from "@ui/components";
+import { FileItemComponent } from "@ui/components/file-item/file-item.component";
+import { FileService } from "@core/services/file.service";
+import { Task } from "@models/courses.model";
+import { FileModel } from "@office/models/file.model";
+import { resolveVideoUrlForIframe } from "@utils/video-url-embed";
+import { ImagePreviewDirective } from "../image-preview/image-preview.directive";
+
+@Component({
+ selector: "app-write-task",
+ standalone: true,
+ imports: [
+ CommonModule,
+ TruncatePipe,
+ UploadFileComponent,
+ IconComponent,
+ FileItemComponent,
+ ImagePreviewDirective,
+ ],
+ templateUrl: "./write-task.component.html",
+ styleUrl: "./write-task.component.scss",
+})
+export class WriteTaskComponent {
+ private readonly fileService = inject(FileService);
+ private readonly sanitizer = inject(DomSanitizer);
+
+ @Input({ required: true }) data!: Task;
+ @Input() type: "text" | "text-file" = "text";
+ @Input() success = false;
+
+ @Output() update = new EventEmitter<{ text: string; fileUrls?: string[] }>();
+
+ readonly maxLength = 1000;
+
+ uploadedFiles = signal([]);
+ currentLength = signal(0);
+ private currentText = "";
+
+ getSafeVideoUrl(): SafeResourceUrl | null {
+ const iframeUrl = resolveVideoUrlForIframe(this.data?.videoUrl);
+ if (!iframeUrl) {
+ return null;
+ }
+
+ return this.sanitizer.bypassSecurityTrustResourceUrl(iframeUrl);
+ }
+
+ hasVideoUrl(): boolean {
+ return !!resolveVideoUrlForIframe(this.data?.videoUrl);
+ }
+
+ onKeyUp(event: Event) {
+ const target = event.target as HTMLTextAreaElement;
+
+ target.style.height = "0px";
+ target.style.height = target.scrollHeight + "px";
+
+ this.currentText = target.value;
+ this.currentLength.set(target.value.length);
+ this.emitUpdate();
+ }
+
+ onFileUploaded(event: { url: string; name: string; size: number; mimeType: string }) {
+ if (!event.url) return;
+
+ const ext = event.name.split(".").pop()?.toLowerCase() || "";
+ const file: FileModel = {
+ name: event.name,
+ size: event.size,
+ mimeType: event.mimeType,
+ link: event.url,
+ extension: ext,
+ datetimeUploaded: new Date().toISOString(),
+ user: 0,
+ };
+
+ this.uploadedFiles.update(files => [...files, file]);
+ this.emitUpdate();
+ }
+
+ onFileRemoved(index: number) {
+ const file = this.uploadedFiles()[index];
+ if (!file) return;
+
+ this.fileService.deleteFile(file.link).subscribe({
+ next: () => {
+ this.uploadedFiles.update(files => files.filter((_, i) => i !== index));
+ this.emitUpdate();
+ },
+ error: () => {
+ this.uploadedFiles.update(files => files.filter((_, i) => i !== index));
+ this.emitUpdate();
+ },
+ });
+ }
+
+ private emitUpdate() {
+ if (this.type === "text-file") {
+ this.update.emit({
+ text: this.currentText,
+ fileUrls: this.uploadedFiles().map(f => f.link),
+ });
+ } else {
+ this.update.emit({ text: this.currentText });
+ }
+ }
+}
diff --git a/projects/social_platform/src/app/office/courses/list/list.component.html b/projects/social_platform/src/app/office/courses/list/list.component.html
new file mode 100644
index 000000000..ae371b7ce
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/list/list.component.html
@@ -0,0 +1,13 @@
+
+
+
+ @if (coursesList().length) {
+
+ @for (course of coursesList(); track course.id) {
+
+
+
+ }
+
+ }
+
diff --git a/projects/social_platform/src/app/office/courses/list/list.component.scss b/projects/social_platform/src/app/office/courses/list/list.component.scss
new file mode 100644
index 000000000..f7601a355
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/list/list.component.scss
@@ -0,0 +1,9 @@
+.courses {
+ margin-top: 20px;
+
+ &__list {
+ display: grid;
+ grid-template-columns: 4fr 4fr;
+ grid-gap: 20px;
+ }
+}
diff --git a/projects/skills/src/app/webinars/list/list.component.spec.ts b/projects/social_platform/src/app/office/courses/list/list.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/webinars/list/list.component.spec.ts
rename to projects/social_platform/src/app/office/courses/list/list.component.spec.ts
diff --git a/projects/social_platform/src/app/office/courses/list/list.component.ts b/projects/social_platform/src/app/office/courses/list/list.component.ts
new file mode 100644
index 000000000..32241bcfe
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/list/list.component.ts
@@ -0,0 +1,46 @@
+/** @format */
+
+import { Component, inject, type OnDestroy, type OnInit, signal } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { ActivatedRoute, RouterModule } from "@angular/router";
+import { map, Subscription } from "rxjs";
+import { CourseComponent } from "../shared/course/course.component";
+import { CourseCard } from "@office/models/courses.model";
+
+/**
+ * Компонент списка траекторий
+ * Отображает список доступных траекторий с поддержкой пагинации
+ * Поддерживает два режима: "all" (все траектории) и "my" (пользовательские)
+ * Реализует бесконечную прокрутку для загрузки дополнительных элементов
+ */
+@Component({
+ selector: "app-list",
+ standalone: true,
+ imports: [CommonModule, RouterModule, CourseComponent],
+ templateUrl: "./list.component.html",
+ styleUrl: "./list.component.scss",
+})
+export class CoursesListComponent implements OnInit, OnDestroy {
+ private readonly route = inject(ActivatedRoute);
+
+ protected readonly coursesList = signal([]);
+
+ private readonly subscriptions$: Subscription[] = [];
+
+ /**
+ * Инициализация компонента
+ * Определяет тип списка (all/my) и загружает начальные данные
+ */
+ ngOnInit(): void {
+ this.route.data.pipe(map(r => r["data"])).subscribe(courses => {
+ this.coursesList.set(courses);
+ });
+ }
+
+ /**
+ * Очистка ресурсов при уничтожении компонента
+ */
+ ngOnDestroy(): void {
+ this.subscriptions$.forEach(s => s.unsubscribe());
+ }
+}
diff --git a/projects/skills/src/app/webinars/list/list.resolver.spec.ts b/projects/social_platform/src/app/office/courses/list/list.resolver.spec.ts
similarity index 100%
rename from projects/skills/src/app/webinars/list/list.resolver.spec.ts
rename to projects/social_platform/src/app/office/courses/list/list.resolver.spec.ts
diff --git a/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.html b/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.html
new file mode 100644
index 000000000..3264b1a61
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.html
@@ -0,0 +1,37 @@
+
+
+
+ @if (mode === 'progress') {
+
{{ progress }}%
+ } @else { @if (appereance === 'open') {
+
+ } @else {
+
+ } }
+
+
+
+
+
+
+
+ @if (haveDate) {
+
c 16.03.26
+ }
+
diff --git a/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.scss b/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.scss
new file mode 100644
index 000000000..f532a9b47
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.scss
@@ -0,0 +1,57 @@
+@use "styles/typography";
+@use "styles/responsive";
+
+.progress-bar {
+ position: relative;
+
+ &__text {
+ position: absolute;
+ top: 49%;
+ left: 49%;
+ color: var(--dark-grey);
+ transform: translate(-49%, -49%);
+
+ &--icon {
+ top: 45%;
+ left: 55%;
+ color: var(--accent-light);
+ }
+
+ &--closed {
+ top: 43%;
+ left: 50%;
+ color: var(--red);
+ }
+ }
+
+ &__date {
+ position: absolute;
+ bottom: -15%;
+ left: 5%;
+ }
+}
+
+.track,
+.filled {
+ fill: none;
+ stroke-width: 7;
+}
+
+.track {
+ opacity: 0.2;
+ stroke: var(--accent-light);
+
+ &--closed {
+ opacity: 1;
+ stroke: var(--red);
+ }
+}
+
+.filled {
+ stroke: var(--accent);
+ transition: all 0.2s;
+
+ &--complete {
+ stroke: var(--green);
+ }
+}
diff --git a/projects/skills/src/app/shared/circle-progress-bar/circle-progress-bar.component.spec.ts b/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.spec.ts
similarity index 100%
rename from projects/skills/src/app/shared/circle-progress-bar/circle-progress-bar.component.spec.ts
rename to projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.spec.ts
diff --git a/projects/skills/src/app/shared/circle-progress-bar/circle-progress-bar.component.ts b/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.ts
similarity index 91%
rename from projects/skills/src/app/shared/circle-progress-bar/circle-progress-bar.component.ts
rename to projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.ts
index 593765cea..4b57daa79 100644
--- a/projects/skills/src/app/shared/circle-progress-bar/circle-progress-bar.component.ts
+++ b/projects/social_platform/src/app/office/courses/shared/circle-progress-bar/circle-progress-bar.component.ts
@@ -2,6 +2,7 @@
import { Component, Input } from "@angular/core";
import { CommonModule } from "@angular/common";
+import { IconComponent } from "@ui/components";
/**
* Компонент круглого прогресс-бара
@@ -15,7 +16,7 @@ import { CommonModule } from "@angular/common";
@Component({
selector: "app-circle-progress-bar",
standalone: true,
- imports: [CommonModule],
+ imports: [CommonModule, IconComponent],
templateUrl: "./circle-progress-bar.component.html",
styleUrl: "./circle-progress-bar.component.scss",
})
@@ -27,6 +28,12 @@ export class CircleProgressBarComponent {
*/
@Input() progress = 0;
+ @Input() mode: "button" | "progress" = "progress";
+
+ @Input() appereance?: "open" | "closed";
+
+ @Input() haveDate?: boolean = false;
+
/**
* Радиус круга прогресс-бара в пикселях
* Используется для расчета окружности и отступов
diff --git a/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.html b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.html
new file mode 100644
index 000000000..77f899455
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
Модуль {{ courseModule.order }}
+
+
+ {{ courseModule.title }}
+
+
+
+
+ {{ courseModule.lessons.length }}
+ {{ courseModule.lessons.length | pluralize: ["урок", "урока", "уроков"] }}
+
+
+
+
+
+
+
+
+ @if (courseModule.lessons.length) {
+
+ @if (isExpanded) {
+
+ } @else {
+
+ }
+
+ }
+
+
+
+
+
+ @for (lesson of courseModule.lessons; track lesson.id) {
+
+
+
Урок {{ lesson.order }}.
+ {{ lesson.title }}
+
+ {{ lesson.taskCount }}
+ {{ lesson.taskCount | pluralize: ["задание", "задания", "заданий"] }}
+
+
+
+
+
+ }
+
+
+
diff --git a/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.scss b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.scss
new file mode 100644
index 000000000..fd009a50b
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.scss
@@ -0,0 +1,119 @@
+.skill-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+
+ &__inner {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &--wrapper {
+ position: relative;
+ z-index: 10;
+ cursor: pointer;
+ }
+
+ &__info {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__title {
+ color: var(--accent);
+ }
+
+ &__text {
+ color: var(--black);
+ }
+
+ &__text-block {
+ display: flex;
+ gap: 3px;
+ }
+
+ &__level {
+ color: var(--grey-for-text);
+ }
+
+ &__expand {
+ position: absolute;
+ bottom: -9px;
+ left: 17%;
+ z-index: 10;
+ z-index: -10;
+ width: 300px;
+ height: 14px;
+ cursor: pointer;
+ background-color: var(--medium-grey-for-outline);
+ border-radius: var(--rounded-xl);
+
+ i {
+ ::ng-deep {
+ .icon {
+ position: absolute;
+ bottom: 8%;
+ left: 50%;
+ color: var(--dark-grey);
+ transform: rotate(180deg);
+ }
+ }
+ }
+ }
+
+ &__expanded {
+ i {
+ ::ng-deep {
+ .icon {
+ transform: rotate(0deg);
+ }
+ }
+ }
+ }
+}
+
+.expandable {
+ display: none;
+
+ &--expanded {
+ display: block;
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 20px;
+ }
+
+ &__item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px;
+ cursor: pointer;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+ }
+}
+
+.topic {
+ &__number {
+ color: var(--accent);
+ }
+
+ &__name {
+ color: var(--black);
+ }
+
+ &__levels {
+ color: var(--grey-for-text);
+ }
+}
diff --git a/projects/skills/src/app/skills/shared/skill-card/skill-card.component.spec.ts b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.spec.ts
similarity index 89%
rename from projects/skills/src/app/skills/shared/skill-card/skill-card.component.spec.ts
rename to projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.spec.ts
index 9eaabb6a6..d43aca54f 100644
--- a/projects/skills/src/app/skills/shared/skill-card/skill-card.component.spec.ts
+++ b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.spec.ts
@@ -2,7 +2,7 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { SkillCardComponent } from "./skill-card.component";
+import { SkillCardComponent } from "./course-module-card.component";
describe("SkillCardComponent", () => {
let component: SkillCardComponent;
diff --git a/projects/skills/src/app/skills/shared/skill-card/skill-card.component.ts b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.ts
similarity index 53%
rename from projects/skills/src/app/skills/shared/skill-card/skill-card.component.ts
rename to projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.ts
index 902d1ee4c..0f7b6f6f4 100644
--- a/projects/skills/src/app/skills/shared/skill-card/skill-card.component.ts
+++ b/projects/social_platform/src/app/office/courses/shared/course-module-card/course-module-card.component.ts
@@ -3,8 +3,11 @@
import { Component, Input } from "@angular/core";
import { CommonModule } from "@angular/common";
import { AvatarComponent } from "@uilib";
-import { Skill } from "../../../../models/skill.model";
import { PluralizePipe } from "@corelib";
+import { IconComponent } from "@ui/components";
+import { CircleProgressBarComponent } from "../circle-progress-bar/circle-progress-bar.component";
+import { CourseDetail, CourseModule } from "@office/models/courses.model";
+import { RouterLink } from "@angular/router";
/**
* Компонент карточки навыка
@@ -19,15 +22,32 @@ import { PluralizePipe } from "@corelib";
* - Поддержка двух визуальных стилей
* - Индикация статуса навыка (подписка, просрочка, выполнение)
* - Плюрализация для количества уровней
+ * - Раскрывающийся список тем и действий
*/
@Component({
- selector: "app-skill-card",
+ selector: "app-course-module-card",
standalone: true,
- imports: [CommonModule, AvatarComponent, PluralizePipe],
- templateUrl: "./skill-card.component.html",
- styleUrl: "./skill-card.component.scss",
+ imports: [
+ CommonModule,
+ CircleProgressBarComponent,
+ IconComponent,
+ RouterLink,
+ PluralizePipe,
+ AvatarComponent,
+ ],
+ templateUrl: "./course-module-card.component.html",
+ styleUrl: "./course-module-card.component.scss",
})
-export class SkillCardComponent {
- @Input({ required: true }) skill!: Skill;
+export class CourseModuleCardComponent {
+ @Input({ required: true }) courseModule!: CourseModule;
@Input() type: "personal" | "base" = "base";
+
+ isExpanded = false;
+
+ toggleExpand(event: Event): void {
+ if (this.courseModule.lessons.length) {
+ event.stopPropagation();
+ this.isExpanded = !this.isExpanded;
+ }
+ }
}
diff --git a/projects/social_platform/src/app/office/courses/shared/course/course.component.html b/projects/social_platform/src/app/office/courses/shared/course/course.component.html
new file mode 100644
index 000000000..cfee7e26a
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/shared/course/course.component.html
@@ -0,0 +1,43 @@
+
+
+@if(course) {
+
+
+
+
+
+ {{
+ isMember()
+ ? "для участников программы"
+ : isSubs()
+ ? "доступно по подписке"
+ : "доступно всем пользователям"
+ }}
+
+
+
+
+
+
+
{{ course.title | truncate: 50 }}
+
+ {{ course.dateLabel }}
+
+
+
+
+ @if (isLock()) {
+
+ } @else {
+
+ {{ course.progressStatus === "not_started" ? "начать" : "продолжить обучение" }}
+
+
+ }
+
+
+}
diff --git a/projects/social_platform/src/app/office/courses/shared/course/course.component.scss b/projects/social_platform/src/app/office/courses/shared/course/course.component.scss
new file mode 100644
index 000000000..5f73c704f
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/shared/course/course.component.scss
@@ -0,0 +1,89 @@
+@use "styles/responsive";
+@use "styles/typography";
+
+.course {
+ position: relative;
+ width: 100%;
+ max-width: 333px;
+ overflow: hidden;
+ cursor: pointer;
+ background-color: var(--light-white);
+ border: 0.5px solid var(--medium-grey-for-outline);
+ border-radius: var(--rounded-lg);
+
+ &__cover {
+ border-radius: var(--rounded-lg);
+
+ app-button {
+ ::ng-deep {
+ .button {
+ position: absolute;
+ top: 15px;
+ right: 24px;
+ width: 166px;
+ padding: 0;
+ }
+ }
+ }
+
+ app-avatar {
+ ::ng-deep {
+ .avatar {
+ img {
+ position: absolute;
+ top: 35px;
+ left: 24px;
+ box-shadow: 0 0 8px rgba($color: #333, $alpha: 30%);
+ }
+ }
+ }
+ }
+ }
+
+ &__image {
+ width: 100%;
+ max-width: 333px;
+ height: 100%;
+ max-height: 137px;
+ border-radius: var(--rounded-lg);
+ }
+
+ &__info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding: 12px 24px;
+
+ &--date {
+ color: var(--grey-for-text) !important;
+ }
+ }
+
+ &__action {
+ position: absolute;
+ right: 14px;
+ bottom: 16px;
+ display: flex;
+ gap: 5px;
+ align-items: center;
+
+ &--started,
+ &-icon {
+ color: var(--green) !important;
+ }
+ }
+
+ &__rocket {
+ position: absolute;
+ top: -46%;
+ right: -13%;
+ z-index: 0;
+ width: 175px;
+ transform: rotate(-53deg);
+ }
+
+ p,
+ i {
+ color: var(--black);
+ }
+}
diff --git a/projects/skills/src/app/profile/profile.component.spec.ts b/projects/social_platform/src/app/office/courses/shared/course/course.component.spec.ts
similarity index 55%
rename from projects/skills/src/app/profile/profile.component.spec.ts
rename to projects/social_platform/src/app/office/courses/shared/course/course.component.spec.ts
index 0f8e275dd..ab7e087af 100644
--- a/projects/skills/src/app/profile/profile.component.spec.ts
+++ b/projects/social_platform/src/app/office/courses/shared/course/course.component.spec.ts
@@ -1,19 +1,18 @@
/** @format */
import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { CourseComponent } from "./course.component";
-import { ProfileComponent } from "./profile.component";
-
-describe("ProfileComponent", () => {
- let component: ProfileComponent;
- let fixture: ComponentFixture;
+describe("CourseComponent", () => {
+ let component: CourseComponent;
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [ProfileComponent],
+ imports: [CourseComponent],
}).compileComponents();
- fixture = TestBed.createComponent(ProfileComponent);
+ fixture = TestBed.createComponent(CourseComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/projects/social_platform/src/app/office/courses/shared/course/course.component.ts b/projects/social_platform/src/app/office/courses/shared/course/course.component.ts
new file mode 100644
index 000000000..166337651
--- /dev/null
+++ b/projects/social_platform/src/app/office/courses/shared/course/course.component.ts
@@ -0,0 +1,68 @@
+/** @format */
+
+import { CommonModule } from "@angular/common";
+import { Component, Input, OnInit, signal } from "@angular/core";
+import { RouterModule } from "@angular/router";
+import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
+import { IconComponent, ButtonComponent } from "@ui/components";
+import { AvatarComponent } from "@ui/components/avatar/avatar.component";
+import { CourseCard } from "@office/models/courses.model";
+
+/**
+ * Компонент отображения карточки траектории
+ * Показывает информацию о траектории: название, описание, навыки, длительность
+ * Поддерживает различные модальные окна для взаимодействия с пользователем
+ * Обрабатывает выбор траектории и навигацию к детальной информации
+ *
+ * @Input trajectory - объект траектории для отображения
+ */
+@Component({
+ selector: "app-course",
+ standalone: true,
+ imports: [
+ CommonModule,
+ RouterModule,
+ TruncatePipe,
+ IconComponent,
+ AvatarComponent,
+ ButtonComponent,
+ IconComponent,
+ ],
+ templateUrl: "./course.component.html",
+ styleUrl: "./course.component.scss",
+})
+export class CourseComponent implements OnInit {
+ @Input() course!: CourseCard;
+
+ ngOnInit(): void {
+ this.accessType();
+ this.actions();
+ }
+
+ protected readonly isLock = signal(false);
+
+ protected readonly isMember = signal(false);
+ protected readonly isSubs = signal(false);
+
+ private accessType() {
+ switch (this.course.accessType) {
+ case "program_members": {
+ this.isMember.set(true);
+ break;
+ }
+
+ case "subscription_stub":
+ this.isSubs.set(true);
+ break;
+ }
+ }
+
+ private actions() {
+ switch (this.course.actionState) {
+ case "lock": {
+ this.isLock.set(true);
+ break;
+ }
+ }
+ }
+}
diff --git a/projects/social_platform/src/app/office/features/approve-skill/approve-skill.component.ts b/projects/social_platform/src/app/office/features/approve-skill/approve-skill.component.ts
index 3a1db27d9..aad91d08e 100644
--- a/projects/social_platform/src/app/office/features/approve-skill/approve-skill.component.ts
+++ b/projects/social_platform/src/app/office/features/approve-skill/approve-skill.component.ts
@@ -2,14 +2,11 @@
import { CommonModule } from "@angular/common";
import { ChangeDetectorRef, Component, inject, Input, OnDestroy, OnInit } from "@angular/core";
-import { PluralizePipe } from "@corelib";
-import { Skill } from "@office/models/skill";
-import { AvatarComponent } from "@ui/components/avatar/avatar.component";
+import { Skill } from "@office/models/skill.model";
import { ButtonComponent } from "@ui/components";
import { map, of, Subscription, switchMap } from "rxjs";
import { AuthService } from "@auth/services";
import { ActivatedRoute } from "@angular/router";
-import { ProfileService } from "projects/skills/src/app/profile/services/profile.service";
import { ProfileService as profileApproveSkillService } from "@auth/services/profile.service";
import { SnackbarService } from "@ui/services/snackbar.service";
import { HttpErrorResponse } from "@angular/common/http";
@@ -28,14 +25,7 @@ import { ApproveSkillPeopleComponent } from "@office/shared/approve-skill-people
styleUrl: "./approve-skill.component.scss",
templateUrl: "./approve-skill.component.html",
standalone: true,
- imports: [
- CommonModule,
- AvatarComponent,
- PluralizePipe,
- ButtonComponent,
- ModalComponent,
- ApproveSkillPeopleComponent,
- ],
+ imports: [CommonModule, ButtonComponent, ModalComponent, ApproveSkillPeopleComponent],
})
export class ApproveSkillComponent implements OnInit, OnDestroy {
private readonly authService = inject(AuthService);
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 9b2173ab2..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
@@ -9,7 +9,7 @@ import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { AuthService } from "@auth/services";
import { AvatarComponent } from "@ui/components/avatar/avatar.component";
import { TooltipComponent } from "@ui/components/tooltip/tooltip.component";
-import { concatMap, EMPTY, filter, map, Observable, of, Subscription, tap } from "rxjs";
+import { concatMap, filter, map, Subscription, tap } from "rxjs";
import { User } from "@auth/models/user.model";
import { Collaborator } from "@office/models/collaborator.model";
import { ProjectService } from "@office/services/project.service";
@@ -20,7 +20,6 @@ import { ProgramDataService } from "@office/program/services/program-data.servic
import { ChatService } from "@office/services/chat.service";
import { calculateProfileProgress } from "@utils/calculateProgress";
import { ProfileDataService } from "@office/profile/detail/services/profile-date.service";
-import { ProfileService } from "projects/skills/src/app/profile/services/profile.service";
import { SnackbarService } from "@ui/services/snackbar.service";
import { ApproveSkillComponent } from "../approve-skill/approve-skill.component";
import { ProgramService } from "@office/program/services/program.service";
@@ -30,7 +29,7 @@ import {
projectNewAdditionalProgramVields,
} from "@office/models/partner-program-fields.model";
import { saveFile } from "@utils/helpers/export-file";
-import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
+import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
import { ControlErrorPipe, ValidationService } from "@corelib";
import { ErrorMessage } from "@error/models/error-message";
@@ -69,7 +68,6 @@ export class DeatilComponent implements OnInit, OnDestroy {
private readonly router = inject(Router);
private readonly location = inject(Location);
private readonly profileDataService = inject(ProfileDataService);
- public readonly skillsProfileService = inject(ProfileService);
public readonly chatService = inject(ChatService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly programService = inject(ProgramService);
@@ -207,17 +205,6 @@ export class DeatilComponent implements OnInit, OnDestroy {
this.projectAdditionalService.setAssignProjectToProgramError(error);
}
- /**
- * Переключатель для модалки выбора проекта
- */
- // toggleSubmitProjectModal(): void {
- // this.showSubmitProjectModal.set(!this.showSubmitProjectModal());
-
- // if (!this.showSubmitProjectModal()) {
- // this.selectedProjectId = null;
- // }
- // }
-
/** Показать подсказку */
showTooltip(): void {
this.isTooltipVisible = true;
@@ -240,25 +227,6 @@ export class DeatilComponent implements OnInit, OnDestroy {
}
}
- /**
- * Добавление проекта на программу
- */
- // selectProject(): void {
- // if (this.selectedProjectId === null) {
- // return;
- // }
-
- // const selectedProject = this.memberProjects.find(
- // project => project.id === this.selectedProjectId
- // );
-
- // console.log(selectedProject);
- // }
-
- // get isProjectSelected(): boolean {
- // return this.selectedProjectId !== null;
- // }
-
addNewProject(): void {
const newFieldsFormValues: projectNewAdditionalProgramVields[] = [];
diff --git a/projects/social_platform/src/app/office/members/filters/members-filters.component.ts b/projects/social_platform/src/app/office/members/filters/members-filters.component.ts
index 11a96157d..6bd0bd0cb 100644
--- a/projects/social_platform/src/app/office/members/filters/members-filters.component.ts
+++ b/projects/social_platform/src/app/office/members/filters/members-filters.component.ts
@@ -13,10 +13,10 @@ import { RangeInputComponent } from "@ui/components/range-input/range-input.comp
import { MembersComponent } from "@office/members/members.component";
import { ReactiveFormsModule } from "@angular/forms";
import { AutoCompleteInputComponent } from "@ui/components/autocomplete-input/autocomplete-input.component";
-import { Specialization } from "@office/models/specialization";
+import { Specialization } from "@office/models/specialization.model";
import { SpecializationsService } from "@office/services/specializations.service";
import { SkillsService } from "@office/services/skills.service";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { ActivatedRoute, Router } from "@angular/router";
import { CheckboxComponent } from "../../../ui/components/checkbox/checkbox.component";
diff --git a/projects/social_platform/src/app/office/models/courses.model.ts b/projects/social_platform/src/app/office/models/courses.model.ts
new file mode 100644
index 000000000..83e435941
--- /dev/null
+++ b/projects/social_platform/src/app/office/models/courses.model.ts
@@ -0,0 +1,163 @@
+/**
+ * Как в базе
+ *
+ * id — ID курса (создается автоматически).
+ * title — название курса (до 45 символов).
+ * description — описание курса (до 600 символов, можно оставить пустым).
+ * access_type — тип доступа: для всех, для участников программы, по подписке.
+ * partner_program — связанная программа (может быть пустой, кроме сценария “для участников программы”).
+ * avatar_file — аватар курса (файл, необязательно).
+ * card_cover_file — обложка карточки курса в каталоге (файл, необязательно).
+ * header_cover_file — обложка шапки внутри страницы курса (файл, необязательно).
+ * start_date — дата старта курса (может быть пустой).
+ * end_date — дата окончания курса (может быть пустой).
+ * status — статус контента курса: черновик, опубликован, завершен.
+ * is_completed — логический флаг завершения курса.
+ * completed_at — дата/время завершения курса.
+ * datetime_created — дата/время создания.
+ * datetime_updated — дата/время обновления.
+ *
+ * @format
+ */
+
+export interface CourseCard {
+ id: number;
+ title: string;
+ accessType: "all_users" | "program_members" | "subscription_stub";
+ status: "draft" | "published" | "ended";
+ avatarUrl: string;
+ cardCoverUrl: string;
+ startDate: Date;
+ endDate: Date;
+ dateLabel: string;
+ isAvailable: boolean;
+ progressStatus: "not_started" | "in_progress" | "completed" | "blocked";
+ percent: number;
+ actionState: "start" | "continue" | "lock";
+}
+
+export interface CourseDetail {
+ id: number;
+ title: string;
+ description: string;
+ accessType: "all_users" | "program_members" | "subscription_stub";
+ status: "draft" | "published" | "ended";
+ avatarUrl: string;
+ headerCoverUrl: string;
+ startDate: Date;
+ endDate: Date;
+ dateLabel: string;
+ isAvailable: boolean;
+ partnerProgramId: number;
+ progressStatus: "not_started" | "in_progress" | "completed" | "blocked";
+ percent: number;
+ analyticsStub: any;
+}
+
+/**
+ * Как в базе
+ *
+ id — уникальный идентификатор модуля.
+ course — курс, к которому относится модуль (обязательная связь).
+ title — название модуля, максимум 40 символов.
+ avatar_file — аватар модуля (необязательный файл).
+ start_date — дата старта модуля (обязательная).
+ status — статус модуля: draft (черновик), published (опубликован)
+ order — порядковый номер модуля внутри курса (по нему сортируется вывод).
+ datetime_created — дата/время создания.
+ datetime_updated — дата/время последнего обновления.
+ *
+ */
+
+export interface CourseLessons {
+ id: number;
+ moduleId: number;
+ title: string;
+ order: number;
+ status: string;
+ isAvailable: boolean;
+ progressStatus: "not_started" | "in_progress" | "completed" | "blocked";
+ percent: number;
+ currentTaskId: number;
+ taskCount: number;
+}
+
+export interface CourseModule {
+ id: number;
+ courseId: number;
+ title: string;
+ order: number;
+ avatarUrl: string;
+ startDate: Date;
+ status: string;
+ isAvailable: boolean;
+ progressStatus: "not_started" | "in_progress" | "completed" | "blocked";
+ percent: number;
+ lessons: CourseLessons[];
+}
+
+export interface CourseStructure {
+ courseId: number;
+ progressStatus: "not_started" | "in_progress" | "completed" | "blocked";
+ percent: number;
+ modules: CourseModule[];
+}
+
+/**
+ * Как в базе
+ *
+ id — уникальный идентификатор урока.
+ module — модуль, к которому относится урок (обязательная связь).
+ title — название урока, максимум 45 символов.
+ status — статус урока: draft (черновик), published (опубликован)
+ order — порядковый номер урока внутри модуля.
+ datetime_created — дата/время создания.
+ datetime_updated — дата/время последнего обновления.
+ */
+
+export interface Option {
+ id: number;
+ order: number;
+ text: string;
+}
+
+export interface Task {
+ id: number;
+ order: number;
+ title: string;
+ answerTitle: string;
+ status: string;
+ taskKind: "question" | "informational";
+ checkType: string | null;
+ informationalType: string | null;
+ questionType: string | null;
+ answerType: string | null;
+ bodyText: string;
+ videoUrl: string | null;
+ imageUrl: string | null;
+ attachmentUrl: string | null;
+ isAvailable: boolean;
+ isCompleted: boolean;
+ options: Option[];
+}
+
+export interface CourseLesson {
+ id: number;
+ moduleId: number;
+ courseId: number;
+ title: string;
+ progressStatus: "not_started" | "in_progress" | "completed" | "blocked";
+ percent: number;
+ currentTaskId: number;
+ moduleOrder: number;
+ tasks: Task[];
+}
+
+export interface TaskAnswerResponse {
+ answerId: number;
+ status: "submitted" | "pending_review";
+ isCorrect: boolean;
+ canContinue: boolean;
+ nextTaskId: number | null;
+ submittedAt: Date;
+}
diff --git a/projects/social_platform/src/app/office/models/skill.ts b/projects/social_platform/src/app/office/models/skill.model.ts
similarity index 100%
rename from projects/social_platform/src/app/office/models/skill.ts
rename to projects/social_platform/src/app/office/models/skill.model.ts
diff --git a/projects/social_platform/src/app/office/models/skills-group.ts b/projects/social_platform/src/app/office/models/skills-group.model.ts
similarity index 87%
rename from projects/social_platform/src/app/office/models/skills-group.ts
rename to projects/social_platform/src/app/office/models/skills-group.model.ts
index 037c38a5c..0897e1398 100644
--- a/projects/social_platform/src/app/office/models/skills-group.ts
+++ b/projects/social_platform/src/app/office/models/skills-group.model.ts
@@ -1,6 +1,6 @@
/** @format */
-import { Skill } from "./skill"; // Assuming Skill is defined in a separate file
+import { Skill } from "./skill.model"; // Assuming Skill is defined in a separate file
/**
* Интерфейс для группы навыков
diff --git a/projects/social_platform/src/app/office/models/specialization.ts b/projects/social_platform/src/app/office/models/specialization.model.ts
similarity index 100%
rename from projects/social_platform/src/app/office/models/specialization.ts
rename to projects/social_platform/src/app/office/models/specialization.model.ts
diff --git a/projects/social_platform/src/app/office/models/specializations-group.ts b/projects/social_platform/src/app/office/models/specializations-group.model.ts
similarity index 88%
rename from projects/social_platform/src/app/office/models/specializations-group.ts
rename to projects/social_platform/src/app/office/models/specializations-group.model.ts
index 55149beb9..49717c780 100644
--- a/projects/social_platform/src/app/office/models/specializations-group.ts
+++ b/projects/social_platform/src/app/office/models/specializations-group.model.ts
@@ -1,6 +1,6 @@
/** @format */
-import type { Specialization } from "./specialization";
+import type { Specialization } from "./specialization.model";
/**
* Модель группы специализаций
diff --git a/projects/social_platform/src/app/office/models/step.model.ts b/projects/social_platform/src/app/office/models/step.model.ts
new file mode 100644
index 000000000..7df3b4b13
--- /dev/null
+++ b/projects/social_platform/src/app/office/models/step.model.ts
@@ -0,0 +1,139 @@
+/** @format */
+
+/**
+ * Base interface for all step types
+ * Contains common properties shared across different question types
+ */
+interface BaseStep {
+ id: number; // Unique identifier for the step
+}
+
+/**
+ * Всплывающее окно с дополнительной информацией
+ * Отображается после завершения шага для предоставления дополнительного контекста
+ */
+export interface Popup {
+ title: string | null; // Заголовок всплывающего окна
+ text: string | null; // Текстовое содержимое
+ fileLink: string | null; // URL к связанному файлу или ресурсу
+ ordinalNumber: number; // Порядковый номер для сортировки
+}
+
+/**
+ * Информационный слайд
+ *
+ * Отображает образовательный контент без требования взаимодействия пользователя.
+ * Используется для представления концепций, объяснений или инструкций.
+ */
+export interface InfoSlide extends BaseStep {
+ text: string; // Основной текстовый контент слайда
+ description: string; // Дополнительное описание или контекст
+ files: string[]; // Массив URL файлов для отображения (изображения, документы)
+ popups: Popup[]; // Всплывающие окна для отображения после просмотра
+ videoUrl?: string; // Ссылка для видео
+}
+
+/**
+ * Вопрос на соединение/сопоставление
+ *
+ * Требует от пользователей сопоставления элементов из двух колонок или соединения связанных концепций.
+ * Проверяет понимание отношений между различными элементами.
+ */
+export interface ConnectQuestion extends BaseStep {
+ connectLeft: { id: number; text?: string; file?: string }[]; // Элементы левой колонки
+ connectRight: { id: number; text?: string; file?: string }[]; // Элементы правой колонки
+ description: string; // Инструкции по выполнению сопоставления
+ files: string[]; // Дополнительные файлы для контекста
+ isAnswered: boolean; // Был ли вопрос уже отвечен
+ text: string; // Основной текст вопроса
+ popups: Popup[]; // Всплывающие окна для отображения после ответа
+}
+
+/**
+ * Структура запроса для вопросов на соединение
+ * Массив пар соединений, выбранных пользователем
+ */
+export type ConnectQuestionRequest = { leftId: number; rightId: number }[];
+
+/**
+ * Структура ответа для вопросов на соединение
+ * Показывает правильность каждого соединения
+ */
+export type ConnectQuestionResponse = {
+ leftId: number;
+ rightId: number;
+ isCorrect: boolean; // Правильно ли это соединение
+}[];
+
+/**
+ * Вопрос с единственным правильным ответом
+ *
+ * Представляет вопрос с несколькими вариантами, где только один ответ правильный.
+ * Наиболее распространенный тип оценочного вопроса.
+ */
+export interface SingleQuestion extends BaseStep {
+ answers: { id: number; text: string }[]; // Доступные варианты ответов
+ description: string; // Дополнительное описание или контекст
+ files: string[]; // Связанные файлы (изображения, документы)
+ isAnswered: boolean; // Был ли вопрос уже отвечен
+ text: string; // Основной текст вопроса
+ popups: Popup[]; // Всплывающие окна для отображения после ответа
+ videoUrl: string;
+}
+
+/**
+ * Ответ об ошибке для вопросов с единственным ответом
+ * Возвращается, когда пользователь выбирает неправильный ответ
+ */
+export interface SingleQuestionError {
+ correctAnswer: number; // ID правильного варианта
+ isCorrect: boolean; // Был ли ответ правильным (всегда false для ошибок)
+}
+
+/**
+ * Вопрос на исключение
+ *
+ * Представляет несколько элементов, где пользователи должны определить, какой не принадлежит
+ * или какие элементы должны быть исключены из группы.
+ */
+export interface ExcludeQuestion extends BaseStep {
+ answers: { id: number; text: string }[]; // Элементы для рассмотрения
+ description: string; // Инструкции для задачи исключения
+ files: string[]; // Связанные файлы для контекста
+ isAnswered: boolean; // Был ли вопрос уже отвечен
+ text: string; // Основной текст вопроса
+ popups: Popup[]; // Всплывающие окна для отображения после ответа
+}
+
+/**
+ * Структура ответа для вопросов на исключение
+ */
+export interface ExcludeQuestionResponse {
+ isCorrect: boolean; // Был ли ответ пользователя правильным
+ wrongAnswers: number[]; // ID неправильно выбранных элементов
+}
+
+/**
+ * Вопрос с письменным ответом
+ *
+ * Требует от пользователей предоставления текстового ответа.
+ * Может использоваться для коротких ответов, эссе или отправки кода.
+ */
+export interface WriteQuestion extends BaseStep {
+ answer: string | null; // Текущий ответ пользователя (если есть)
+ description: string; // Инструкции или дополнительный контекст
+ files: string[]; // Связанные файлы для справки
+ text: string; // Основной текст вопроса или подсказка
+ popups: Popup[]; // Всплывающие окна для отображения после отправки
+}
+
+/**
+ * Объединенный тип, представляющий все возможные типы шагов
+ * Используется для типобезопасной обработки различных вариаций шагов
+ */
+export type StepType =
+ | InfoSlide
+ | ConnectQuestion
+ | SingleQuestion
+ | ExcludeQuestion
+ | WriteQuestion;
diff --git a/projects/social_platform/src/app/office/models/vacancy.model.ts b/projects/social_platform/src/app/office/models/vacancy.model.ts
index bf4d49a93..0030391d9 100644
--- a/projects/social_platform/src/app/office/models/vacancy.model.ts
+++ b/projects/social_platform/src/app/office/models/vacancy.model.ts
@@ -1,7 +1,7 @@
/** @format */
import { Project } from "@models/project.model";
-import { Skill } from "./skill";
+import { Skill } from "./skill.model";
/**
* Модель вакансии в проекте
diff --git a/projects/social_platform/src/app/office/office.component.ts b/projects/social_platform/src/app/office/office.component.ts
index a5a52a6ca..e3dbb6063 100644
--- a/projects/social_platform/src/app/office/office.component.ts
+++ b/projects/social_platform/src/app/office/office.component.ts
@@ -233,14 +233,8 @@ export class OfficeComponent implements OnInit, OnDestroy {
{ name: "проекты", icon: "projects", link: "projects" },
{ name: "участники", icon: "people-bold", link: "members" },
{ name: "программы", icon: "program", link: "program" },
+ { name: "курсы", icon: "trajectories", link: "courses" },
{ name: "вакансии", icon: "search-sidebar", link: "vacancies" },
- {
- name: "траектории",
- icon: "trajectories",
- link: "skills",
- isExternal: true,
- isActive: false,
- },
{ name: "чаты", icon: "message", link: "chats" },
];
}
diff --git a/projects/social_platform/src/app/office/office.routes.ts b/projects/social_platform/src/app/office/office.routes.ts
index 53548cbbf..109482981 100644
--- a/projects/social_platform/src/app/office/office.routes.ts
+++ b/projects/social_platform/src/app/office/office.routes.ts
@@ -36,24 +36,21 @@ export const OFFICE_ROUTES: Routes = [
redirectTo: "program",
},
{
- path: "feed",
- loadChildren: () => import("./feed/feed.routes").then(c => c.FEED_ROUTES),
- },
- {
- path: "vacancies",
- loadChildren: () => import("./vacancies/vacancies.routes").then(c => c.VACANCIES_ROUTES),
+ path: "profile/edit",
+ component: ProfileEditComponent,
},
{
- path: "projects",
- loadChildren: () => import("./projects/projects.routes").then(c => c.PROJECTS_ROUTES),
+ path: "profile/:id",
+ loadChildren: () =>
+ import("./profile/detail/profile-detail.routes").then(c => c.PROFILE_DETAIL_ROUTES),
},
{
- path: "program",
- loadChildren: () => import("./program/program.routes").then(c => c.PROGRAM_ROUTES),
+ path: "feed",
+ loadChildren: () => import("./feed/feed.routes").then(c => c.FEED_ROUTES),
},
{
- path: "chats",
- loadChildren: () => import("./chat/chat.routes").then(c => c.CHAT_ROUTES),
+ path: "projects",
+ loadChildren: () => import("./projects/projects.routes").then(c => c.PROJECTS_ROUTES),
},
{
path: "members",
@@ -63,13 +60,20 @@ export const OFFICE_ROUTES: Routes = [
},
},
{
- path: "profile/edit",
- component: ProfileEditComponent,
+ path: "program",
+ loadChildren: () => import("./program/program.routes").then(c => c.PROGRAM_ROUTES),
},
{
- path: "profile/:id",
- loadChildren: () =>
- import("./profile/detail/profile-detail.routes").then(c => c.PROFILE_DETAIL_ROUTES),
+ path: "courses",
+ loadChildren: () => import("./courses/courses.routes").then(c => c.COURSES_ROUTES),
+ },
+ {
+ path: "vacancies",
+ loadChildren: () => import("./vacancies/vacancies.routes").then(c => c.VACANCIES_ROUTES),
+ },
+ {
+ path: "chats",
+ loadChildren: () => import("./chat/chat.routes").then(c => c.CHAT_ROUTES),
},
{
path: "**",
diff --git a/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.component.ts b/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.component.ts
index 81728037a..303045aa5 100644
--- a/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.component.ts
+++ b/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.component.ts
@@ -10,9 +10,9 @@ import { OnboardingService } from "../services/onboarding.service";
import { ButtonComponent, IconComponent } from "@ui/components";
import { CommonModule } from "@angular/common";
import { AutoCompleteInputComponent } from "@ui/components/autocomplete-input/autocomplete-input.component";
-import { SpecializationsGroup } from "@office/models/specializations-group";
+import { SpecializationsGroup } from "@office/models/specializations-group.model";
import { SpecializationsGroupComponent } from "@office/shared/specializations-group/specializations-group.component";
-import { Specialization } from "@office/models/specialization";
+import { Specialization } from "@office/models/specialization.model";
import { SpecializationsService } from "@office/services/specializations.service";
import { ErrorMessage } from "@error/models/error-message";
import { TooltipComponent } from "@ui/components/tooltip/tooltip.component";
diff --git a/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.resolver.ts b/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.resolver.ts
index 4e8f05f6a..220dedbad 100644
--- a/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.resolver.ts
+++ b/projects/social_platform/src/app/office/onboarding/stage-one/stage-one.resolver.ts
@@ -3,7 +3,7 @@
import { inject } from "@angular/core";
import { ResolveFn } from "@angular/router";
import { SpecializationsService } from "@office/services/specializations.service";
-import { SpecializationsGroup } from "@office/models/specializations-group";
+import { SpecializationsGroup } from "@office/models/specializations-group.model";
/**
* РЕЗОЛВЕР ПЕРВОГО ЭТАПА ОНБОРДИНГА
diff --git a/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.ts b/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.ts
index 193e4b4a2..363d5bf21 100644
--- a/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.ts
+++ b/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.component.ts
@@ -10,9 +10,9 @@ import { OnboardingService } from "../services/onboarding.service";
import { ButtonComponent, IconComponent } from "@ui/components";
import { CommonModule } from "@angular/common";
import { AutoCompleteInputComponent } from "@ui/components/autocomplete-input/autocomplete-input.component";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { SkillsService } from "@office/services/skills.service";
-import { SkillsGroup } from "@office/models/skills-group";
+import { SkillsGroup } from "@office/models/skills-group.model";
import { SkillsGroupComponent } from "@office/shared/skills-group/skills-group.component";
import { SkillsBasketComponent } from "@office/shared/skills-basket/skills-basket.component";
import { ModalComponent } from "@ui/components/modal/modal.component";
diff --git a/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.resolver.ts b/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.resolver.ts
index 38d91200e..462270cee 100644
--- a/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.resolver.ts
+++ b/projects/social_platform/src/app/office/onboarding/stage-two/stage-two.resolver.ts
@@ -3,7 +3,7 @@
import { inject } from "@angular/core";
import { ResolveFn } from "@angular/router";
import { SkillsService } from "@office/services/skills.service";
-import { SkillsGroup } from "@office/models/skills-group";
+import { SkillsGroup } from "@office/models/skills-group.model";
/**
* РЕЗОЛВЕР ВТОРОГО ЭТАПА ОНБОРДИНГА
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 462626376..8687dddd5 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
@@ -32,7 +32,7 @@ import { AsyncPipe, CommonModule, NgTemplateOutlet } from "@angular/common";
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 { Skill } from "@office/models/skill.model";
import { HttpErrorResponse } from "@angular/common/http";
import { NewsFormComponent } from "@office/features/news-form/news-form.component";
import { NewsCardComponent } from "@office/features/news-card/news-card.component";
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 ef14a51dc..b0c575b52 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
@@ -29,14 +29,14 @@ import { EditorSubmitButtonDirective } from "@ui/directives/editor-submit-button
import { TextareaComponent } from "@ui/components/textarea/textarea.component";
import { AvatarControlComponent } from "@ui/components/avatar-control/avatar-control.component";
import { AsyncPipe, CommonModule } from "@angular/common";
-import { Specialization } from "@office/models/specialization";
+import { Specialization } from "@office/models/specialization.model";
import { SpecializationsService } from "@office/services/specializations.service";
import { AutoCompleteInputComponent } from "@ui/components/autocomplete-input/autocomplete-input.component";
import { SkillsGroupComponent } from "@office/shared/skills-group/skills-group.component";
import { SpecializationsGroupComponent } from "@office/shared/specializations-group/specializations-group.component";
import { SkillsBasketComponent } from "@office/shared/skills-basket/skills-basket.component";
import { ModalComponent } from "@ui/components/modal/modal.component";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { SkillsService } from "@office/services/skills.service";
import {
educationUserLevel,
@@ -1110,6 +1110,8 @@ export class ProfileEditComponent implements OnInit, OnDestroy, AfterViewInit {
: this.profileForm.value.phoneNumber,
};
+ console.log(newProfile);
+
this.authService
.saveProfile(newProfile)
.pipe(concatMap(() => this.authService.getProfile()))
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 6cf76bc09..7806eeaf4 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
@@ -13,7 +13,7 @@ import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { ErrorMessage } from "@error/models/error-message";
import { Invite } from "@models/invite.model";
import { Project } from "@models/project.model";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { ProgramService } from "@office/program/services/program.service";
import { SkillsService } from "@office/services/skills.service";
import { SkillsGroupComponent } from "@office/shared/skills-group/skills-group.component";
diff --git a/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts b/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts
index bcadeb069..0eb24ec3d 100644
--- a/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts
+++ b/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts
@@ -4,7 +4,7 @@ import { inject, Injectable, signal } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { ValidationService } from "@corelib";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { Vacancy } from "@office/models/vacancy.model";
import { VacancyService } from "@office/services/vacancy.service";
import { stripNullish } from "@utils/stripNull";
diff --git a/projects/social_platform/src/app/office/projects/edit/shared/project-vacancy-step/project-vacancy-step.component.ts b/projects/social_platform/src/app/office/projects/edit/shared/project-vacancy-step/project-vacancy-step.component.ts
index e17b9a273..b5a14958e 100644
--- a/projects/social_platform/src/app/office/projects/edit/shared/project-vacancy-step/project-vacancy-step.component.ts
+++ b/projects/social_platform/src/app/office/projects/edit/shared/project-vacancy-step/project-vacancy-step.component.ts
@@ -9,7 +9,7 @@ import { ErrorMessage } from "@error/models/error-message";
import { AutoCompleteInputComponent } from "@ui/components/autocomplete-input/autocomplete-input.component";
import { SkillsBasketComponent } from "@office/shared/skills-basket/skills-basket.component";
import { VacancyCardComponent } from "@office/features/vacancy-card/vacancy-card.component";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { ProjectVacancyService } from "../../services/project-vacancy.service";
import { ActivatedRoute } from "@angular/router";
import { IconComponent } from "@uilib";
diff --git a/projects/social_platform/src/app/office/services/skills.service.ts b/projects/social_platform/src/app/office/services/skills.service.ts
index 528320d79..a9f0852fb 100644
--- a/projects/social_platform/src/app/office/services/skills.service.ts
+++ b/projects/social_platform/src/app/office/services/skills.service.ts
@@ -1,10 +1,10 @@
/** @format */
import { Injectable } from "@angular/core";
-import { SkillsGroup } from "../models/skills-group";
+import { SkillsGroup } from "../models/skills-group.model";
import { Observable } from "rxjs";
import { ApiService } from "@corelib";
-import { Skill } from "../models/skill";
+import { Skill } from "../models/skill.model";
import { ApiPagination } from "@office/models/api-pagination.model";
import { HttpParams } from "@angular/common/http";
diff --git a/projects/social_platform/src/app/office/services/specializations.service.ts b/projects/social_platform/src/app/office/services/specializations.service.ts
index 5d4a3ac2e..786749a84 100644
--- a/projects/social_platform/src/app/office/services/specializations.service.ts
+++ b/projects/social_platform/src/app/office/services/specializations.service.ts
@@ -1,10 +1,10 @@
/** @format */
import { Injectable } from "@angular/core";
-import { SpecializationsGroup } from "../models/specializations-group";
+import { SpecializationsGroup } from "../models/specializations-group.model";
import { Observable } from "rxjs";
import { ApiService } from "@corelib";
-import { Specialization } from "../models/specialization";
+import { Specialization } from "../models/specialization.model";
import { ApiPagination } from "@office/models/api-pagination.model";
import { HttpParams } from "@angular/common/http";
diff --git a/projects/social_platform/src/app/office/shared/approve-skill-people/approve-skill-people.component.ts b/projects/social_platform/src/app/office/shared/approve-skill-people/approve-skill-people.component.ts
index cf3d051dd..984311dce 100644
--- a/projects/social_platform/src/app/office/shared/approve-skill-people/approve-skill-people.component.ts
+++ b/projects/social_platform/src/app/office/shared/approve-skill-people/approve-skill-people.component.ts
@@ -3,7 +3,7 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { PluralizePipe } from "@corelib";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { AvatarComponent } from "@ui/components/avatar/avatar.component";
@Component({
diff --git a/projects/social_platform/src/app/office/shared/skills-basket/skills-basket.component.ts b/projects/social_platform/src/app/office/shared/skills-basket/skills-basket.component.ts
index 564c2b69b..91a1a9920 100644
--- a/projects/social_platform/src/app/office/shared/skills-basket/skills-basket.component.ts
+++ b/projects/social_platform/src/app/office/shared/skills-basket/skills-basket.component.ts
@@ -2,7 +2,7 @@
import { CommonModule } from "@angular/common";
import { Component, forwardRef, Input, signal } from "@angular/core";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
import { IconComponent } from "@ui/components";
import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { noop } from "rxjs";
diff --git a/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.ts b/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.ts
index cce0b02be..c53fc5e95 100644
--- a/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.ts
+++ b/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.ts
@@ -9,7 +9,7 @@ import {
signal,
} from "@angular/core";
import { IconComponent } from "@ui/components";
-import { Skill } from "@office/models/skill";
+import { Skill } from "@office/models/skill.model";
/**
* Компонент группы навыков с возможностью множественного выбора
diff --git a/projects/social_platform/src/app/office/shared/specializations-group/specializations-group.component.ts b/projects/social_platform/src/app/office/shared/specializations-group/specializations-group.component.ts
index 70bb7cb3f..47faab9bf 100644
--- a/projects/social_platform/src/app/office/shared/specializations-group/specializations-group.component.ts
+++ b/projects/social_platform/src/app/office/shared/specializations-group/specializations-group.component.ts
@@ -10,7 +10,7 @@ import {
signal,
} from "@angular/core";
import { IconComponent } from "@ui/components";
-import { Specialization } from "@office/models/specialization";
+import { Specialization } from "@office/models/specialization.model";
/**
* Компонент группы специализаций с возможностью сворачивания/разворачивания
diff --git a/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.html b/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.html
index 541b87c8f..a3e68b892 100644
--- a/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.html
+++ b/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.html
@@ -1,4 +1,10 @@
-
+
diff --git a/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.scss b/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.scss
index 94ed06479..373ebf399 100644
--- a/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.scss
+++ b/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.scss
@@ -17,6 +17,6 @@
}
&__check {
- color: var(--white);
+ color: var(--light-white);
}
}
diff --git a/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.ts b/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.ts
index fcc8b59ab..d86affbf9 100644
--- a/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.ts
+++ b/projects/social_platform/src/app/ui/components/checkbox/checkbox.component.ts
@@ -27,6 +27,8 @@ export class CheckboxComponent implements OnInit {
/** Состояние чекбокса */
@Input({ required: true }) checked = false;
+ @Input() size?: string;
+
/** Событие изменения состояния */
@Output() checkedChange = new EventEmitter();
diff --git a/projects/social_platform/src/app/ui/components/file-item/file-item.component.html b/projects/social_platform/src/app/ui/components/file-item/file-item.component.html
index e2ba2ecad..aece7cfb1 100644
--- a/projects/social_platform/src/app/ui/components/file-item/file-item.component.html
+++ b/projects/social_platform/src/app/ui/components/file-item/file-item.component.html
@@ -2,32 +2,38 @@
@if (name && link) {
-
+
+ @if (mode === 'preview') {
+
+ } @else {
+
+ }
-
-
- {{ name }}
-
+
+
+ {{ name }}
+
- @if (type) {
-
- {{ type.includes("/") ? (type | fileType) : (type | uppercase) }}
- {{ size | formatedFileSize }}
+ @if (type) {
+
+ {{ type.includes("/") ? (type | fileType) : (type | uppercase) }}
+ {{ size | formatedFileSize }}
+
+ }
- }
@if (canDelete) {
diff --git a/projects/social_platform/src/app/ui/components/file-item/file-item.component.scss b/projects/social_platform/src/app/ui/components/file-item/file-item.component.scss
index 6efc7cf0b..b45dc26e9 100644
--- a/projects/social_platform/src/app/ui/components/file-item/file-item.component.scss
+++ b/projects/social_platform/src/app/ui/components/file-item/file-item.component.scss
@@ -4,6 +4,7 @@
display: flex;
gap: 10px;
align-items: center;
+ justify-content: space-between;
&__name {
overflow: hidden;
@@ -16,6 +17,12 @@
}
}
+ &__left {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ }
+
&__meta {
color: var(--dark-grey);
}
@@ -25,10 +32,11 @@
align-items: center;
align-self: center;
justify-content: center;
- width: 20px;
- height: 20px;
+ width: 35px;
+ height: 35px;
+ cursor: pointer;
border: 0.5px solid var(--medium-grey-for-outline);
- border-radius: var(--rounded-xl);
+ border-radius: var(--rounded-xxl);
&--delete {
color: var(--red);
@@ -39,5 +47,10 @@
color: var(--accent);
cursor: pointer;
}
+
+ &--file {
+ color: var(--accent);
+ cursor: pointer;
+ }
}
}
diff --git a/projects/social_platform/src/app/ui/components/file-item/file-item.component.ts b/projects/social_platform/src/app/ui/components/file-item/file-item.component.ts
index 79d9d9d64..aab60452d 100644
--- a/projects/social_platform/src/app/ui/components/file-item/file-item.component.ts
+++ b/projects/social_platform/src/app/ui/components/file-item/file-item.component.ts
@@ -1,5 +1,5 @@
/** @format */
-import { Component, inject, Input, OnInit } from "@angular/core";
+import { Component, EventEmitter, inject, Input, OnInit, Output } from "@angular/core";
import { FileTypePipe } from "@ui/pipes/file-type.pipe";
import { IconComponent } from "@ui/components";
import { UpperCasePipe } from "@angular/common";
@@ -33,6 +33,12 @@ export class FileItemComponent implements OnInit {
@Input() canDelete = false;
+ /** Режим отображения: 'default' — скачивание + удаление через сервис, 'preview' — только просмотр + удаление через Output */
+ @Input() mode: "default" | "preview" = "default";
+
+ /** Событие удаления файла (используется в режиме preview) */
+ @Output() deleted = new EventEmitter
();
+
/** MIME-тип файла */
@Input() type = "file";
@@ -61,11 +67,17 @@ export class FileItemComponent implements OnInit {
/**
* Удаление файла
- * Удаляет файл с сервера и из списка прикрепленных файлов
+ * В режиме preview — эмитит событие наружу
+ * В режиме default — удаляет через FileService
*/
onDeleteFile(): void {
if (!this.link) return;
+ if (this.mode === "preview") {
+ this.deleted.emit();
+ return;
+ }
+
this.fileService.deleteFile(this.link).subscribe(() => {
this.link = "";
this.name = "";
diff --git a/projects/social_platform/src/app/ui/components/modal/modal.component.scss b/projects/social_platform/src/app/ui/components/modal/modal.component.scss
index e37d9310e..f9d47022c 100644
--- a/projects/social_platform/src/app/ui/components/modal/modal.component.scss
+++ b/projects/social_platform/src/app/ui/components/modal/modal.component.scss
@@ -8,7 +8,7 @@
right: 0;
bottom: 0;
left: 0;
- z-index: 10;
+ z-index: 10000;
&__overlay {
position: absolute;
diff --git a/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.html b/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.html
index e361d9af1..04c97ce83 100644
--- a/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.html
+++ b/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.html
@@ -12,7 +12,11 @@
@slideInOut
>
{{ snack.text }}
-
+
}
diff --git a/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.scss b/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.scss
index d274c275d..26cb93c9c 100644
--- a/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.scss
+++ b/projects/social_platform/src/app/ui/components/snackbar/snackbar.component.scss
@@ -33,4 +33,9 @@
color: var(--white);
background-color: var(--red);
}
+
+ &--info {
+ color: var(--black);
+ background-color: var(--lime);
+ }
}
diff --git a/projects/social_platform/src/app/ui/components/upload-file/upload-file.component.ts b/projects/social_platform/src/app/ui/components/upload-file/upload-file.component.ts
index d358c2a07..2c8223666 100644
--- a/projects/social_platform/src/app/ui/components/upload-file/upload-file.component.ts
+++ b/projects/social_platform/src/app/ui/components/upload-file/upload-file.component.ts
@@ -1,6 +1,6 @@
/** @format */
-import { Component, forwardRef, Input, OnInit } from "@angular/core";
+import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { FileService } from "@core/services/file.service";
import { nanoid } from "nanoid";
@@ -38,7 +38,7 @@ import { LoaderComponent } from "../loader/loader.component";
},
],
standalone: true,
- imports: [IconComponent, SlicePipe, LoaderComponent],
+ imports: [IconComponent, LoaderComponent],
})
export class UploadFileComponent implements OnInit, ControlValueAccessor {
constructor(private fileService: FileService) {}
@@ -49,6 +49,17 @@ export class UploadFileComponent implements OnInit, ControlValueAccessor {
/** Состояние ошибки */
@Input() error = false;
+ /** Режим: после загрузки сбросить в пустое состояние и не показывать "файл успешно загружен" */
+ @Input() resetAfterUpload = false;
+
+ /** Событие с данными загруженного файла (url + метаданные оригинального файла) */
+ @Output() uploaded = new EventEmitter<{
+ url: string;
+ name: string;
+ size: number;
+ mimeType: string;
+ }>();
+
ngOnInit(): void {}
/** Уникальный ID для элемента input */
@@ -79,18 +90,30 @@ export class UploadFileComponent implements OnInit, ControlValueAccessor {
/** Обработчик загрузки файла */
onUpdate(event: Event): void {
- const files = (event.currentTarget as HTMLInputElement).files;
+ const input = event.currentTarget as HTMLInputElement;
+ const files = input.files;
if (!files?.length) {
return;
}
+ const originalFile = files[0];
this.loading = true;
- this.fileService.uploadFile(files[0]).subscribe(res => {
+ this.fileService.uploadFile(originalFile).subscribe(res => {
this.loading = false;
- this.value = res.url;
- this.onChange(res.url);
+ if (this.resetAfterUpload) {
+ this.uploaded.emit({
+ url: res.url,
+ name: originalFile.name,
+ size: originalFile.size,
+ mimeType: originalFile.type,
+ });
+ input.value = "";
+ } else {
+ this.value = res.url;
+ this.onChange(res.url);
+ }
});
}
diff --git a/projects/social_platform/src/app/utils/video-url-embed.ts b/projects/social_platform/src/app/utils/video-url-embed.ts
new file mode 100644
index 000000000..2c8c23c05
--- /dev/null
+++ b/projects/social_platform/src/app/utils/video-url-embed.ts
@@ -0,0 +1,119 @@
+/** @format */
+
+const VIDEO_FILE_EXTENSION_REGEX = /\.(mp4|webm|ogg|mov|m4v)(?:$|[?#])/i;
+const HTTP_PROTOCOL_REGEX = /^https?:$/;
+const PROVIDER_ID_REGEX = /^[A-Za-z0-9_-]{6,}$/;
+const BLOCKED_FOR_IFRAME_HOSTS = new Set(["disk.yandex.ru", "yadi.sk"]);
+
+const trimProviderPrefix = (hostname: string): string =>
+ hostname.toLowerCase().replace(/^www\./, "");
+
+const normalizeToUrl = (value: string): URL | null => {
+ try {
+ return new URL(value);
+ } catch {
+ return null;
+ }
+};
+
+const extractYoutubeId = (url: URL, host: string): string | null => {
+ if (host === "youtu.be") {
+ const id = url.pathname.split("/").filter(Boolean)[0] ?? "";
+ return PROVIDER_ID_REGEX.test(id) ? id : null;
+ }
+
+ if (host !== "youtube.com" && host !== "m.youtube.com") {
+ return null;
+ }
+
+ const byQuery = url.searchParams.get("v");
+ if (byQuery && PROVIDER_ID_REGEX.test(byQuery)) {
+ return byQuery;
+ }
+
+ const pathSegments = url.pathname.split("/").filter(Boolean);
+ if (!pathSegments.length) {
+ return null;
+ }
+
+ const [prefix, maybeId] = pathSegments;
+
+ if (prefix === "embed" || prefix === "shorts" || prefix === "live") {
+ return maybeId && PROVIDER_ID_REGEX.test(maybeId) ? maybeId : null;
+ }
+
+ return null;
+};
+
+const resolveRutubeEmbedUrl = (url: URL): string | null => {
+ const host = trimProviderPrefix(url.hostname);
+ if (host !== "rutube.ru") {
+ return null;
+ }
+
+ const path = url.pathname;
+ const embedMatch = path.match(/^\/play\/embed\/([A-Za-z0-9_-]+)\/?$/i);
+ if (embedMatch?.[1]) {
+ return `https://rutube.ru/play/embed/${embedMatch[1]}`;
+ }
+
+ const videoMatch = path.match(/^\/video\/([A-Za-z0-9_-]+)\/?$/i);
+ if (videoMatch?.[1]) {
+ return `https://rutube.ru/play/embed/${videoMatch[1]}`;
+ }
+
+ return null;
+};
+
+const resolveGoogleDriveEmbedUrl = (url: URL): string | null => {
+ const host = trimProviderPrefix(url.hostname);
+ if (host !== "drive.google.com") {
+ return null;
+ }
+
+ const fileIdFromPath = url.pathname.match(/\/file\/d\/([^/]+)/)?.[1];
+ if (fileIdFromPath) {
+ return `https://drive.google.com/file/d/${fileIdFromPath}/preview`;
+ }
+
+ const fileIdFromQuery = url.searchParams.get("id");
+ if (fileIdFromQuery && (url.pathname === "/uc" || url.pathname === "/open")) {
+ return `https://drive.google.com/file/d/${fileIdFromQuery}/preview`;
+ }
+
+ return null;
+};
+
+const resolveYoutubeEmbedUrl = (url: URL): string | null => {
+ const host = trimProviderPrefix(url.hostname);
+ const id = extractYoutubeId(url, host);
+ if (!id) {
+ return null;
+ }
+
+ return `https://www.youtube.com/embed/${id}`;
+};
+
+export const resolveVideoUrlForIframe = (value: string | null | undefined): string | null => {
+ const raw = value?.trim();
+ if (!raw) {
+ return null;
+ }
+
+ const parsed = normalizeToUrl(raw);
+ if (!parsed || !HTTP_PROTOCOL_REGEX.test(parsed.protocol)) {
+ return null;
+ }
+
+ const host = trimProviderPrefix(parsed.hostname);
+ if (BLOCKED_FOR_IFRAME_HOSTS.has(host)) {
+ return null;
+ }
+
+ return (
+ resolveRutubeEmbedUrl(parsed) ||
+ resolveGoogleDriveEmbedUrl(parsed) ||
+ resolveYoutubeEmbedUrl(parsed) ||
+ (VIDEO_FILE_EXTENSION_REGEX.test(parsed.pathname) ? raw : null)
+ );
+};
diff --git a/projects/social_platform/src/assets/icons/svg/rocket.svg b/projects/social_platform/src/assets/icons/svg/rocket.svg
new file mode 100644
index 000000000..75422e0ec
--- /dev/null
+++ b/projects/social_platform/src/assets/icons/svg/rocket.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/social_platform/src/assets/icons/svg/triangle.svg b/projects/social_platform/src/assets/icons/svg/triangle.svg
new file mode 100644
index 000000000..c98333c02
--- /dev/null
+++ b/projects/social_platform/src/assets/icons/svg/triangle.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg b/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg
index 57e81601c..b4a4ac247 100644
--- a/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg
+++ b/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/projects/social_platform/src/assets/images/courses/character.svg b/projects/social_platform/src/assets/images/courses/character.svg
new file mode 100644
index 000000000..64f0ea9dd
--- /dev/null
+++ b/projects/social_platform/src/assets/images/courses/character.svg
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/social_platform/src/assets/images/courses/complete.svg b/projects/social_platform/src/assets/images/courses/complete.svg
new file mode 100644
index 000000000..e97c7693a
--- /dev/null
+++ b/projects/social_platform/src/assets/images/courses/complete.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/social_platform/src/styles.scss b/projects/social_platform/src/styles.scss
index d61acb4fa..bef2f6d11 100644
--- a/projects/social_platform/src/styles.scss
+++ b/projects/social_platform/src/styles.scss
@@ -17,6 +17,7 @@
@import "styles/components/key-skills.scss";
@import "styles/components/nav.scss";
@import "styles/components/contact-link.scss";
+@import "app/office/courses/lesson/shared/image-preview/image-preview.scss";
// PAGES
diff --git a/projects/social_platform/src/styles/_colors.scss b/projects/social_platform/src/styles/_colors.scss
index 9deab347f..0d7f3cc02 100644
--- a/projects/social_platform/src/styles/_colors.scss
+++ b/projects/social_platform/src/styles/_colors.scss
@@ -29,6 +29,8 @@
// FUNCTIONAL
--green: #88c9a1;
--green-dark: #297373;
+ --green-light: #e3f0e8;
+ --lime: #d6ff54;
--red: #d48a9e;
--red-dark: #{color.adjust(#d48a9e, $blackness: 10%)};
}
diff --git a/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.ts b/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.ts
index 59f09f357..9aaf0f41b 100644
--- a/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.ts
+++ b/projects/ui/src/lib/components/layout/profile-control-panel/profile-control-panel.component.ts
@@ -8,6 +8,7 @@ import type { Invite } from "@office/models/invite.model";
import { RouterLink } from "@angular/router";
import type { User } from "../../../models/user.model";
import { EmptyManageCardComponent } from "../empty-manage-card/empty-manage-card.component";
+import { UserData } from "projects/skills/src/models/profile.model";
/**
* Компонент панели управления профилем
@@ -47,7 +48,7 @@ import { EmptyManageCardComponent } from "../empty-manage-card/empty-manage-card
})
export class ProfileControlPanelComponent {
/** Данные текущего пользователя */
- @Input({ required: true }) user!: User | null;
+ @Input({ required: true }) user!: User | UserData | null;
/** Массив приглашений пользователя */
@Input({ required: true }) invites!: Invite[];
diff --git a/projects/ui/src/lib/components/layout/profile-info/profile-info.component.ts b/projects/ui/src/lib/components/layout/profile-info/profile-info.component.ts
index 50ad1fc23..016f59943 100644
--- a/projects/ui/src/lib/components/layout/profile-info/profile-info.component.ts
+++ b/projects/ui/src/lib/components/layout/profile-info/profile-info.component.ts
@@ -5,6 +5,7 @@ import { Router, RouterLink } from "@angular/router";
import { DayjsPipe } from "projects/core";
import { AvatarComponent, IconComponent } from "@uilib";
import type { User } from "../../../models/user.model";
+import { UserData } from "projects/skills/src/models/profile.model";
/**
* Компонент отображения информации о профиле пользователя
@@ -25,7 +26,7 @@ import type { User } from "../../../models/user.model";
templateUrl: "./profile-info.component.html",
styleUrl: "./profile-info.component.scss",
standalone: true,
- imports: [RouterLink, AvatarComponent, IconComponent, DayjsPipe],
+ imports: [RouterLink, AvatarComponent, IconComponent],
})
export class ProfileInfoComponent implements OnInit {
constructor(readonly router: Router) {}
@@ -33,7 +34,7 @@ export class ProfileInfoComponent implements OnInit {
ngOnInit(): void {}
/** Данные пользователя для отображения */
- @Input({ required: true }) user!: User;
+ @Input({ required: true }) user!: User | UserData;
/** Событие выхода из системы */
@Output() logout = new EventEmitter();
diff --git a/projects/ui/src/lib/components/layout/sidebar/sidebar.component.ts b/projects/ui/src/lib/components/layout/sidebar/sidebar.component.ts
index 6a5392e0d..726238b71 100644
--- a/projects/ui/src/lib/components/layout/sidebar/sidebar.component.ts
+++ b/projects/ui/src/lib/components/layout/sidebar/sidebar.component.ts
@@ -10,13 +10,8 @@ import {
type OnInit,
} from "@angular/core";
import { RouterLink, RouterLinkActive } from "@angular/router";
-import {
- IconComponent,
- InviteManageCardComponent,
- ProfileControlPanelComponent,
- ProfileInfoComponent,
-} from "@uilib";
-import { AsyncPipe, CommonModule } from "@angular/common";
+import { IconComponent } from "@uilib";
+import { CommonModule } from "@angular/common";
import { ClickOutsideModule } from "ng-click-outside";
/**