From 44bab2e96c03c09f2d798663c2020f6de7f37c48 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Mon, 26 Oct 2026 23:11:37 +0300 Subject: [PATCH] fix(programs): role actions and shared solution UI --- .../get-published-projects.use-case.ts | 22 +++ .../program/ports/program.repository.port.ts | 5 + .../src/app/domain/program/program.model.ts | 1 + .../adapters/program/program-http.adapter.ts | 4 + .../repository/program/program.repository.ts | 4 + .../program/detail/list/projects.resolver.ts | 4 + .../program/detail/main/main.component.html | 7 +- .../program/detail/main/main.roles.spec.ts | 10 +- .../public-program-solutions.component.ts | 5 +- .../published-projects.component.ts | 132 ++++++++++++++++++ .../published-solutions.component.html | 83 +++++------ .../published-solutions.component.scss | 93 ++++++------ .../published-solutions.component.spec.ts | 11 +- .../published-solutions.component.ts | 20 ++- .../solution-evaluation.component.html | 24 +--- .../solution-evaluation.component.scss | 22 --- .../solution-evaluation.component.ts | 7 +- .../solution/solution-team.component.scss | 17 +-- .../program/solution/solution.component.html | 18 +-- .../program/solution/solution.component.scss | 52 ++----- .../solution/solution.component.spec.ts | 8 +- .../program/solution/solution.component.ts | 14 +- .../ui/primitives/search/search.component.ts | 5 +- .../app/ui/routes/program/detail.routes.ts | 10 +- .../published-solutions.routes.spec.ts | 6 + .../program/published-solutions.routes.ts | 4 + .../detail/detail-cleanup.component.spec.ts | 59 +++++++- .../ui/widgets/detail/detail.component.html | 45 +++++- .../ui/widgets/detail/detail.component.scss | 11 ++ .../app/ui/widgets/detail/detail.component.ts | 2 + .../detail/services/detail-info.service.ts | 4 + .../program/detail-program-info.service.ts | 6 +- .../solution-fields.component.scss | 6 +- .../src/styles/_solution-ui.scss | 19 +++ .../primitives/tabs/tabs.component.scss | 39 ++++++ .../primitives/tabs/tabs.component.ts | 3 +- 36 files changed, 569 insertions(+), 213 deletions(-) create mode 100644 projects/social_platform/src/app/api/program/use-cases/get-published-projects.use-case.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/published-solutions/published-projects.component.ts create mode 100644 projects/social_platform/src/styles/_solution-ui.scss diff --git a/projects/social_platform/src/app/api/program/use-cases/get-published-projects.use-case.ts b/projects/social_platform/src/app/api/program/use-cases/get-published-projects.use-case.ts new file mode 100644 index 000000000..6cc24c130 --- /dev/null +++ b/projects/social_platform/src/app/api/program/use-cases/get-published-projects.use-case.ts @@ -0,0 +1,22 @@ +/** @format */ +import { inject, Injectable } from "@angular/core"; +import { HttpParams } from "@angular/common/http"; +import { catchError, map, of } from "rxjs"; +import { ProgramRepositoryPort } from "@domain/program/ports/program.repository.port"; +import { fail, ok } from "@domain/shared/result.type"; + +@Injectable({ providedIn: "root" }) +export class GetPublishedProgramProjectsUseCase { + private readonly repository = inject(ProgramRepositoryPort); + execute(programId: number, search: string, offset: number) { + return this.repository + .getPublishedProjects( + programId, + new HttpParams().set("search", search).set("limit", 20).set("offset", offset), + ) + .pipe( + map(value => ok(value)), + catchError(() => of(fail({ kind: "unavailable" as const }))), + ); + } +} diff --git a/projects/social_platform/src/app/domain/program/ports/program.repository.port.ts b/projects/social_platform/src/app/domain/program/ports/program.repository.port.ts index 299058289..7f01e3de0 100644 --- a/projects/social_platform/src/app/domain/program/ports/program.repository.port.ts +++ b/projects/social_platform/src/app/domain/program/ports/program.repository.port.ts @@ -126,6 +126,11 @@ export abstract class ProgramRepositoryPort { params?: HttpParams, ): Observable>; + abstract getPublishedProjects( + programId: number, + params?: HttpParams, + ): Observable>; + abstract getAllMembers( programId: number, skip: number, diff --git a/projects/social_platform/src/app/domain/program/program.model.ts b/projects/social_platform/src/app/domain/program/program.model.ts index 7c0b25469..e82b97497 100644 --- a/projects/social_platform/src/app/domain/program/program.model.ts +++ b/projects/social_platform/src/app/domain/program/program.model.ts @@ -43,6 +43,7 @@ export class Program { currentApplication!: ProgramCurrentApplication | null; welcomeAcknowledgedAt!: string | null; publishProjectsAfterFinish!: boolean; + publishedProjectsAvailable?: boolean; courseId!: number | null; courses!: { id: number; title: string; isAvailable: boolean }[]; diff --git a/projects/social_platform/src/app/infrastructure/adapters/program/program-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/program/program-http.adapter.ts index 42eae39df..830a737d2 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/program/program-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/program/program-http.adapter.ts @@ -129,6 +129,10 @@ export class ProgramHttpAdapter { return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/`); } + getPublishedProjects(programId: number, params?: HttpParams): Observable> { + return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/published-projects/`, params); + } + getManagerOverview(programId: number): Observable { return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/manager-overview/`); } diff --git a/projects/social_platform/src/app/infrastructure/repository/program/program.repository.ts b/projects/social_platform/src/app/infrastructure/repository/program/program.repository.ts index 43e36e085..643fec38d 100644 --- a/projects/social_platform/src/app/infrastructure/repository/program/program.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/program/program.repository.ts @@ -203,6 +203,10 @@ export class ProgramRepository implements ProgramRepositoryPort { return this.programAdapter.getAllProjects(programId, params); } + getPublishedProjects(programId: number, params?: HttpParams): Observable> { + return this.programAdapter.getPublishedProjects(programId, params); + } + getAllMembers(programId: number, skip: number, take: number): Observable> { return this.programAdapter.getAllMembers(programId, skip, take).pipe( map(result => ({ diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/projects.resolver.ts b/projects/social_platform/src/app/ui/pages/program/detail/list/projects.resolver.ts index de5e35832..8a4ed3115 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/projects.resolver.ts +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/projects.resolver.ts @@ -8,11 +8,15 @@ import { catchError, EMPTY, map, of } from "rxjs"; import { Project } from "@domain/project/project.model"; import { GetAllProjectsUseCase } from "@api/program/use-cases/get-all-projects.use-case"; import { CreateProgramFiltersUseCase } from "@api/program/use-cases/create-program-filters.use-case"; +import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/program-detail-main-ui-info.service"; /** Предзагружает проекты программы с фильтрацией. */ export const ProgramProjectsResolver: ResolveFn> = ( route: ActivatedRouteSnapshot, ) => { + const program = inject(ProgramDetailMainUIInfoService).program(); + if (program && !program.isUserManager) + return of({ count: 0, results: [], next: "", previous: "" }); const getAllProjectsUseCase = inject(GetAllProjectsUseCase); const createProgramFiltersUseCase = inject(CreateProgramFiltersUseCase); const programId = route.parent?.params["programId"]; diff --git a/projects/social_platform/src/app/ui/pages/program/detail/main/main.component.html b/projects/social_platform/src/app/ui/pages/program/detail/main/main.component.html index c6f790408..278f51956 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/main/main.component.html +++ b/projects/social_platform/src/app/ui/pages/program/detail/main/main.component.html @@ -4,7 +4,12 @@
- @if (!program()!.isUserMember && !program()!.isUserManager && !program()!.isUserExpert) { + @if ( + !program()!.isUserMember && + !program()!.isUserManager && + !program()!.isUserExpert && + program()!.advertisementImageAddress + ) {
{ expect(mine).not.toHaveBeenCalled(); }); it("preserves advertisement and no internal content for an unrelated user", () => { - const f = render({}); + const f = render({ advertisementImageAddress: "/advertisement.webp" }); expect(f.nativeElement.querySelector(".program__advertisement")).toBeTruthy(); expect( f.nativeElement.querySelector("app-program-role-widget,app-news-card,app-news-form"), ).toBeNull(); expect(fetch).not.toHaveBeenCalled(); }); + it("does not render a broken advertisement when the program has no image", () => { + const f = render({}); + expect(f.nativeElement.querySelector(".program__advertisement img")).toBeNull(); + expect( + f.nativeElement.querySelector("app-program-role-widget,app-news-card,app-news-form"), + ).toBeNull(); + expect(fetch).not.toHaveBeenCalled(); + }); }); diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/public-program-solutions.component.ts b/projects/social_platform/src/app/ui/pages/program/published-solutions/public-program-solutions.component.ts index 66ce51f1e..638c84906 100644 --- a/projects/social_platform/src/app/ui/pages/program/published-solutions/public-program-solutions.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/public-program-solutions.component.ts @@ -17,7 +17,7 @@ import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/p
`, styles: ` .public-program { - max-width: 1200px; + max-width: 864px; margin: 24px auto; padding: 0 16px; font-family: Mont, sans-serif; @@ -28,7 +28,8 @@ import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/p margin-bottom: 24px; } .public-program__title { - font-size: 24px; + font-size: 18px; + font-weight: 400; overflow-wrap: anywhere; } @media (max-width: 999px) { diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-projects.component.ts b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-projects.component.ts new file mode 100644 index 000000000..506b0a0c8 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-projects.component.ts @@ -0,0 +1,132 @@ +/** @format */ +import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { debounceTime, distinctUntilChanged, switchMap, tap } from "rxjs"; +import { GetPublishedProgramProjectsUseCase } from "@api/program/use-cases/get-published-projects.use-case"; +import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/program-detail-main-ui-info.service"; +import { ApiPagination } from "@domain/other/api-pagination.model"; +import { Project } from "@domain/project/project.model"; +import { InfoCardComponent } from "@ui/widgets/info-card/info-card.component"; +import { SearchComponent } from "@ui/primitives/search/search.component"; +import { PaginationComponent, StateComponent } from "@uilib"; +import { ProgramListComponent } from "../detail/list/list.component"; + +@Component({ + selector: "app-published-program-projects", + imports: [ + ReactiveFormsModule, + SearchComponent, + InfoCardComponent, + PaginationComponent, + StateComponent, + ], + template: `
+ + @if (loading()) { + + } @else if (error()) { + + } @else if (data(); as page) { +
    + @for (project of page.results; track project.id) { +
  • + } @empty { +
  • + } +
+ @if (page.count) { + + {{ offset() + 1 }}–{{ offset() + page.results.length }} из {{ page.count }} + + } + } +
`, + styles: ` + :host { + display: block; + min-width: 0; + } + .public-projects { + padding-block: 12px 40px; + } + .public-projects__list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr)); + gap: 30px 20px; + margin-block: 30px; + list-style: none; + padding: 0; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PublishedProgramProjectsComponent { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly read = inject(GetPublishedProgramProjectsUseCase); + private readonly program = inject(ProgramDetailMainUIInfoService).program; + readonly search = new FormControl("", { nonNullable: true }); + readonly offset = signal(0); + readonly data = signal | null>(null); + readonly loading = signal(true); + readonly error = signal(false); + constructor() { + this.search.valueChanges + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed()) + .subscribe( + search => + void this.router.navigate([], { + relativeTo: this.route, + queryParams: { search: search.trim() || null, offset: null }, + }), + ); + this.route.queryParamMap + .pipe( + tap(query => { + this.search.setValue(query.get("search") ?? "", { emitEvent: false }); + const offset = Number(query.get("offset")); + this.offset.set(Number.isSafeInteger(offset) && offset >= 0 ? offset : 0); + this.loading.set(true); + this.error.set(false); + }), + switchMap(() => this.read.execute(this.program()!.id, this.search.value, this.offset())), + takeUntilDestroyed(), + ) + .subscribe(result => { + this.loading.set(false); + if (result.ok) this.data.set(result.value); + else { + this.data.set(null); + this.error.set(true); + } + }); + } + changePage(direction: number): void { + void this.router.navigate([], { + relativeTo: this.route, + queryParams: { offset: Math.max(0, this.offset() + 20 * direction) || null }, + queryParamsHandling: "merge", + }); + } +} + +@Component({ + selector: "app-program-projects-page", + imports: [ProgramListComponent, PublishedProgramProjectsComponent], + template: `@if (program()?.isUserManager) { + + } @else { + + }`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProgramProjectsPageComponent { + readonly program = inject(ProgramDetailMainUIInfoService).program; +} diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html index 4a4d097a1..9c7972463 100644 --- a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.html @@ -5,18 +5,20 @@ >К решениям программы } -

{{ solution()?.application?.name || "Решения программы" }}

+

+ {{ solution()?.application?.name || "Решения программы" }} +

@if (solutionId() === null) { - + } @if (loading()) { -

Загружаем опубликованные решения…

+ } @else if (error(); as problem) {

{{ @@ -28,34 +30,31 @@

{{ solution()?.application?.name || "Решения п } @else if (solution(); as item) {

Опубликовано {{ item.publishedAt | date: "dd.MM.yyyy" }}

- + @if (teamTab()) { -

Команда · {{ item.team.acceptedCount }}

+

Команда · {{ item.team.acceptedCount }}

    @for (member of item.team.members; track member.user.id) {
  • - {{ member.user.firstName }} {{ member.user.lastName }}{{ member.role || member.specialization || "Участник" - }}{{ member.isCaptain ? " · Капитан" : "" }} + +
    + {{ member.user.firstName }} {{ member.user.lastName }}{{ member.role || member.specialization || "Участник" + }}{{ + member.isCaptain && + (member.role || member.specialization || "").trim().toLowerCase() !== "капитан" + ? " · Капитан" + : "" + }} +
  • }
@@ -85,16 +84,22 @@

Команда · {{ item.team.acceptedCount }}

@for (card of list.results; track card.application.solutionId) {
  • {{ card.application.name }} -

    {{ card.captain.firstName }} {{ card.captain.lastName }}

    - В команде: {{ card.acceptedCount }} · - {{ card.publishedAt | date: "dd.MM.yyyy" }}

    {{ card.application.name }}

    +
    + +

    {{ card.captain.firstName }} {{ card.captain.lastName }}

    +
    + В команде: {{ card.acceptedCount }} · + {{ card.publishedAt | date: "dd.MM.yyyy" }}
  • } diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss index 926569334..83d9eb942 100644 --- a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.scss @@ -1,15 +1,8 @@ /** @format */ +@use "styles/typography"; @use "styles/ui-foundation"; - -.ui-field { - @include ui-foundation.control; - - font-size: 12px; -} - -.ui-button.ui-button { - @include ui-foundation.button; -} +@use "styles/solution-ui"; +@include solution-ui.controls; :host { display: block; @@ -17,25 +10,28 @@ } .published { - padding: 24px; - font-family: Mont, sans-serif; - background: var(--white); - border-radius: 15px; + @include typography.body-12; + + min-width: 0; + padding-block: 12px 40px; h2 { - margin: 16px 0; - font-size: 20px; - line-height: 28px; + @include typography.heading-1; + + margin-bottom: 20px; overflow-wrap: anywhere; } h3 { - font-size: 16px; + @include typography.heading-2; + + overflow-wrap: anywhere; } - a { + > a { + display: inline-block; + margin-bottom: 20px; color: var(--accent); - overflow-wrap: anywhere; } &__muted, @@ -43,45 +39,58 @@ color: var(--grey-for-text); } - &__tabs { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin: 20px 0; - } - &__search { - display: grid; - gap: 8px; - margin: 20px 0; + display: block; + margin-bottom: 20px; } &__list, &__team { - display: grid; - gap: 16px; padding: 0; list-style: none; } &__list { - grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr)); + gap: 20px; + margin-block: 20px; } - &__list li { - padding: 20px; - border: 1px solid var(--medium-grey-for-outline); - border-radius: 15px; + &__card { + @include ui-foundation.card; + + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; + height: 100%; + color: var(--black); + text-decoration: none; + + h3 { + color: var(--accent); + } } + &__captain, &__team li { - display: grid; - gap: 4px; - padding: 16px 0; + display: flex; + gap: 12px; + align-items: center; + } + + &__team li { + padding-block: 16px; border-bottom: 1px solid var(--medium-grey-for-outline); } - @media (max-width: 600px) { - padding: 16px; + &__team small { + display: block; + margin-top: 4px; + } + + @media (max-width: 999px) { + padding-inline: 10px; } } diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts index 60fb6d80d..bb3972426 100644 --- a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.spec.ts @@ -73,7 +73,7 @@ describe("Публичные решения внутри программы", () expect(values).toContain("0"); expect(values).toContain("Нет"); expect(harness.routeNativeElement!.querySelector('a[target="_blank"]')).not.toBeNull(); - const tabs = Array.from(harness.routeNativeElement!.querySelectorAll("nav a")); + const tabs = Array.from(harness.routeNativeElement!.querySelectorAll("app-tabs button")); expect(tabs.map(a => a.textContent?.trim())).toEqual(["Решение", "Команда"]); await harness.navigateByUrl( "/office/program/14/published-solutions/501?q=Прототип&offset=20&tab=team", @@ -128,13 +128,16 @@ describe("Публичные решения внутри программы", () "/office/program/14/published-solutions?q=Текст", PublishedSolutionsComponent, ); - const input = - harness.routeNativeElement!.querySelector('input[type="search"]'); + await new Promise(resolve => setTimeout(resolve, 0)); + harness.detectChanges(); + const input = harness.routeNativeElement!.querySelector( + ".published__search input", + ); expect(input?.value).toBe("Текст"); loading.next(ok(list)); loading.complete(); harness.detectChanges(); - expect(harness.routeNativeElement!.querySelector('input[type="search"]')).toBe(input); + expect(harness.routeNativeElement!.querySelector(".published__search input")).toBe(input); component.reload(); expect(getList.execute).toHaveBeenCalledTimes(2); }); diff --git a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts index 9620ff39c..944e6507d 100644 --- a/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/published-solutions/published-solutions.component.ts @@ -25,7 +25,15 @@ import { SolutionPublishedSolutionPage, } from "@domain/solution/solution-contract.model"; import { validSolutionId } from "@domain/solution/ports/solution.repository.port"; -import { ButtonDirective, FieldDirective, PaginationComponent } from "@uilib"; +import { + ButtonDirective, + CardDirective, + PaginationComponent, + TabsComponent, + StateComponent, +} from "@uilib"; +import { SearchComponent } from "@ui/primitives/search/search.component"; +import { AvatarComponent } from "@ui/primitives/avatar/avatar.component"; import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fields.component"; @Component({ @@ -35,7 +43,11 @@ import { SolutionFieldsComponent } from "@ui/widgets/solution-fields/solution-fi RouterLink, ReactiveFormsModule, ButtonDirective, - FieldDirective, + CardDirective, + TabsComponent, + StateComponent, + SearchComponent, + AvatarComponent, PaginationComponent, SolutionFieldsComponent, ], @@ -61,6 +73,10 @@ export class PublishedSolutionsComponent { readonly offset = signal(0); readonly teamTab = signal(false); readonly limit = 20; + readonly sectionTabs = [ + { id: "materials", linkText: "Решение", iconName: "box" }, + { id: "team", linkText: "Команда", iconName: "team" }, + ]; constructor() { this.search.valueChanges diff --git a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html index dda751b58..fee79951f 100644 --- a/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html +++ b/projects/social_platform/src/app/ui/pages/program/solution-evaluation/solution-evaluation.component.html @@ -27,23 +27,13 @@

    {{ data.solution.name }}

    } - +

    (isProjectsPage() || isProjectsRatingPage() || isAnalyticsPage() || + isPublishedSolutionsPage() || isMembersPage()) && appWidth < 1000 ) { @@ -159,6 +161,25 @@

    {{ info().name }}

    зарегистрироваться + } @else { + + зарегистрироваться + + Организатор еще не открыл регистрацию. + + } } @if ( @@ -271,7 +292,7 @@

    {{ info().name }}

    } } - @if (info().solutionStorageMode === "solution") { + @if (!isUserManager() && info().solutionStorageMode === "solution") { {{ info().name }} >решения - } @else if (!isUserManager() && isUserExpert() && appWidth >= 1000) { - + } @else if ( + !isUserManager() && (isUserExpert() || info().publishedProjectsAvailable) + ) { + проекты-участники } - @if (!isUserManager() && !isUserExpert() && appWidth >= 1000) { + @if ( + !isUserManager() && + !isUserExpert() && + appWidth >= 1000 && + info().solutionStorageMode !== "solution" && + !info().publishedProjectsAvailable + ) { a, + > .info__primary-action { + min-width: 0; + } + + app-button { + max-width: 100%; + } +} + .info__actions.info__actions--manager { display: grid; grid-template-columns: minmax(0, 1fr) 207px minmax(0, 1fr); diff --git a/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts b/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts index b46dc0cda..ed9eaec34 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/detail.component.ts @@ -100,6 +100,8 @@ export class DeatilComponent implements OnInit, OnDestroy { protected readonly isMembersPage = this.detailProgramInfoService.isMembersPage; protected readonly isProjectsRatingPage = this.detailProgramInfoService.isProjectsRatingPage; protected readonly isAnalyticsPage = this.detailProgramInfoService.isAnalyticsPage; + protected readonly isPublishedSolutionsPage = + this.detailProgramInfoService.isPublishedSolutionsPage; protected readonly isTeamPage = this.detailProjectInfoService.isTeamPage; protected readonly isVacanciesPage = this.detailProjectInfoService.isVacanciesPage; diff --git a/projects/social_platform/src/app/ui/widgets/detail/services/detail-info.service.ts b/projects/social_platform/src/app/ui/widgets/detail/services/detail-info.service.ts index 06eee02ad..e125971f6 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/services/detail-info.service.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/services/detail-info.service.ts @@ -151,6 +151,10 @@ export class DetailInfoService { */ private updatePageStates(url?: string): void { const currentUrl = url || this.router.url; + this.detailProgramInfoService.applyUpdateStage( + "published-solutions", + currentUrl.includes("/published-solutions"), + ); this.detailProgramInfoService.applyUpdateStage( "projects", diff --git a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts index c9f7bb201..6eaff5e5c 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts @@ -45,6 +45,7 @@ export class DetailProgramInfoService { readonly isMembersPage = signal(false); readonly isProjectsRatingPage = signal(false); readonly isAnalyticsPage = signal(false); + readonly isPublishedSolutionsPage = signal(false); readonly additionalFields = signal([]); readonly isAssignProjectToProgramModalOpen = signal(false); readonly isProgramEndedModalOpen = signal(false); @@ -211,10 +212,13 @@ export class DetailProgramInfoService { } applyUpdateStage( - stage: "projects" | "projects-rating" | "members" | "analytics", + stage: "projects" | "projects-rating" | "members" | "analytics" | "published-solutions", isStage: boolean, ): void { switch (stage) { + case "published-solutions": + this.isPublishedSolutionsPage.set(isStage); + break; case "projects": this.isProjectsPage.set(isStage); break; diff --git a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss index 74eb5ec18..d69732df1 100644 --- a/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss +++ b/projects/social_platform/src/app/ui/widgets/solution-fields/solution-fields.component.scss @@ -1,6 +1,7 @@ /** @format */ @use "styles/ui-foundation"; +@use "styles/typography"; :host { display: block; @@ -22,8 +23,7 @@ fieldset { } label { - font-size: 14px; - line-height: 1.5; + @include typography.body-12; } .ui-field { @@ -35,7 +35,7 @@ label { textarea.ui-field { min-height: 120px; resize: vertical; - border-radius: 12px; + border-radius: var(--rounded-xl); } .file-upload, diff --git a/projects/social_platform/src/styles/_solution-ui.scss b/projects/social_platform/src/styles/_solution-ui.scss new file mode 100644 index 000000000..882fda2a9 --- /dev/null +++ b/projects/social_platform/src/styles/_solution-ui.scss @@ -0,0 +1,19 @@ +/** @format */ +@use "ui-foundation"; +@use "typography"; +@use "program-management-controls"; + +// Solution consumers share the existing product controls and desktop density. +@mixin controls { + .ui-field { + @include ui-foundation.control; + @include typography.body-12; + + width: 100%; + } + + .ui-button.ui-button { + @include ui-foundation.button; + @include program-management-controls.compact-action; + } +} diff --git a/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss b/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss index c4f78169a..ddc649f0d 100644 --- a/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss +++ b/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss @@ -1,4 +1,43 @@ /** @format */ +@use "styles/typography"; + +// Opt-in desktop form navigation, matching the existing project editor. +@media (min-width: 1000px) { + .tabs--steps { + display: flex; + gap: 4px; + align-items: center; + width: 100%; + padding: 2px 10px; + margin-block: 22px; + background: var(--medium-grey-for-outline); + border-radius: var(--rounded-xxl); + + .tabs__item { + @include typography.body-12; + + display: inline-flex; + gap: 5px; + align-items: center; + padding: 0 8px; + color: var(--dark-grey); + cursor: pointer; + background: transparent; + border: 0; + border-radius: var(--rounded-xxl); + + &--active { + color: var(--black); + background: var(--white); + } + + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + } + } + } +} @media screen and (max-width: 999px) { :host { diff --git a/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts b/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts index 8a09e8757..b72d90db3 100644 --- a/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts +++ b/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts @@ -18,7 +18,7 @@ export interface TabItem { selector: "app-tabs", imports: [RouterLink, RouterLinkActive, IconComponent], template: ` -