From b8b1fcee375d9ac97edcf268a6671649d2f4cd50 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Tue, 22 Sep 2026 00:18:03 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=BD?= =?UTF-8?q?=D0=B0=20PROD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/prod-project-card-port.md | 79 ++ docs/prod-project-cards/eight-states.png | Bin 0 -> 68869 bytes docs/prod-project-cards/geometry.json | 958 ++++++++++++++++++ docs/prod-project-cards/my-mobile-long.png | Bin 0 -> 38149 bytes .../program/detail/list/list.component.html | 10 +- .../detail/list/list.component.spec.ts | 48 +- .../pages/projects/bar-new/bar.component.scss | 18 + .../dashboard/dashboard.component.html | 3 +- .../dashboard/dashboard.component.spec.ts | 162 +++ .../projects/dashboard/dashboard.component.ts | 2 + .../dashboardItem.component.html | 9 +- .../dashboardItem.component.scss | 8 +- .../dashboardItem/dashboardItem.component.ts | 34 +- .../pages/projects/list/list.component.html | 20 +- .../pages/projects/list/list.component.scss | 12 +- .../projects/list/list.component.spec.ts | 230 +++-- .../ui/pages/projects/list/list.component.ts | 5 +- .../ui/pages/projects/projects.component.scss | 6 + .../info-card/info-card-list-contract.spec.ts | 136 +++ .../info-card/info-card.component.html | 178 ++-- .../info-card/info-card.component.scss | 290 +++++- .../info-card/info-card.component.spec.ts | 344 ++++++- .../widgets/info-card/info-card.component.ts | 106 +- .../ui/widgets/info-card/info-card.fixture.ts | 159 +++ 24 files changed, 2479 insertions(+), 338 deletions(-) create mode 100644 docs/prod-project-card-port.md create mode 100644 docs/prod-project-cards/eight-states.png create mode 100644 docs/prod-project-cards/geometry.json create mode 100644 docs/prod-project-cards/my-mobile-long.png create mode 100644 projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/widgets/info-card/info-card-list-contract.spec.ts create mode 100644 projects/social_platform/src/app/ui/widgets/info-card/info-card.fixture.ts diff --git a/docs/prod-project-card-port.md b/docs/prod-project-card-port.md new file mode 100644 index 000000000..042b7c860 --- /dev/null +++ b/docs/prod-project-card-port.md @@ -0,0 +1,79 @@ + + +# Выборочный перенос финальных карточек проектов на PROD + +База master: `3890ee6ccb84bf92d215ea32b4316e3f52f1d404`. +Финальный DEV source: `683da4b9b30334bab4bd6c7a1afea3ddc91fe9ad`. + +| DEV PR | Merge SHA | Назначение | +| -------------------------------------------------------------- | ------------------------------------------ | ------------------------------- | +| [#357](https://github.com/PROCOLLAB-github/procollab/pull/357) | `918e30d54b3240078f8dda43c0fbc5761c011ae6` | Карточка и lifecycle | +| [#358](https://github.com/PROCOLLAB-github/procollab/pull/358) | `1b92bf0e8e05f1edd906195770ca8fa46ab8d95a` | Общие поверхности списков | +| [#359](https://github.com/PROCOLLAB-github/procollab/pull/359) | `ec822d13b4f55b0718335ac7f0f9e64d69ed05da` | Роль и доступ | +| [#360](https://github.com/PROCOLLAB-github/procollab/pull/360) | `358e818c7ebd8b7d5916e56167a402e2848177d6` | Единый CTA | +| [#361](https://github.com/PROCOLLAB-github/procollab/pull/361) | `69b37d12000b22fd09b77b5a35d95812bf6fbda5` | Независимость lifecycle от роли | +| [#362](https://github.com/PROCOLLAB-github/procollab/pull/362) | `683da4b9b30334bab4bd6c7a1afea3ddc91fe9ad` | Защита неполного контракта | + +## Состав переноса + +Переносятся конечные изменения InfoCard, Dashboard/DashboardItem, ProjectsList, +адаптивной сетки и project-card контейнера ProgramList. Для этих 20 файлов +сравнены PROD master и DEV-база до #357 (`42cbdd65117cc56beaa6069bcc061192df7733ce`): +различие только в порядке двух SCSS-свойств. Поэтому итоговое содержимое выбранных +файлов совместимо с PROD без переноса остальных DEV-изменений. + +Архивные макеты, промежуточные варианты и DEV merge commits не переносятся. +PROD editor, detail resolver, оценивание, прочие сервисы, React, workflows, +dependencies и test harness остаются на master-реализации. + +## Инварианты + +- Карточка 156 × 180 px, аватар 70 px; название и описание по центру, до двух строк. +- Lifecycle: submitted > draft > program > published, независимо от пользователя. +- Роль берётся из `Project.leader` и `ProfileInfoService.profile()?.id`. +- Редактирование обозначается только лидеру без связи или с явным `isSubmitted === false`. + Неизвестный submission state даёт «только просмотр». +- CTA «Открыть» ведёт в detail; одна ссылка охватывает карточку и CTA. + Подписка — отдельная кнопка вне ссылки; двойной навигации нет. +- Dashboard и полный список используют my/subs/base; all/subscriptions показывают + отрасль и не получают lifecycle/роль собственного проекта. +- Приглашения сохраняют принятие/отклонение и идентичность по inviteId. +- Черновик серый, опубликованный зелёный, программа lilac/accent, сданный gold. + +## Контракт и приёмка + +Нужен backend port DEV #748: `partner_program` содержит `id`, `name`, +`program_link_id`, `program_id`, `is_submitted` той же min-PK связи, что в detail +для лидера/участника. Backend следует развернуть первым после отдельного разрешения. + +Интеграционный тест передаёт сокращённый snake_case HTTP payload через настоящий +CamelcaseInterceptor, ApiService, adapter, repository и use case в InfoCard. +Проверяются сданный проект, неизвестное состояние, восемь сочетаний lifecycle/роли, +контексты Dashboard/List, маршруты, подписки и приглашения. + +Проверки выполняются на PROD-ветке: targeted, полный `npm run test:ci`, format, +lint, scoped Stylelint, `build:prod`, diff-check. В master уже установлен `pool: forks`; +порт не меняет эту настройку и не подавляет ошибки. + +Браузерный smoke использует локальную сборку настоящих компонентов с фикстурами, +без обращений к DEV/PROD и пользовательским данным. Он не заменяет авторизованную +live-проверку после отдельно разрешённого deploy. Здесь создаётся только Draft PR. + +## Локальный visual smoke + +Измерены reference, Dashboard и полные my/subscriptions/all при viewport 1440 × 900, +а также my с длинными строками при 390 × 844 и 320 × 844 (DPR 1). +Во всех случаях карточки 156 × 180 px, аватары 70 × 70 px; горизонтального overflow +и вложенных интерактивных элементов в project link нет. Восьми сочетаниям +lifecycle/роли соответствуют одинаковые подписи в Dashboard и полном списке. + +Tab выделяет следующую ссылку рамкой 2 px; Enter показывает detail-placeholder +проекта 102. Это локальный TestBed-маршрут, не live detail. Console: ошибок нет, +есть предупреждение NG0912 о двух IconComponent в preview. Подписки и приглашения +дополнительно проверены компонентными тестами с настоящими DOM-действиями. + +Замеры: [geometry.json](prod-project-cards/geometry.json). + +![Восемь состояний](prod-project-cards/eight-states.png) + +![Длинные строки, 390 px](prod-project-cards/my-mobile-long.png) diff --git a/docs/prod-project-cards/eight-states.png b/docs/prod-project-cards/eight-states.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca9bd010e5a0cf83e7eb128edd3c6c4d3561f28 GIT binary patch literal 68869 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 Bky-!% literal 0 HcmV?d00001 diff --git a/docs/prod-project-cards/geometry.json b/docs/prod-project-cards/geometry.json new file mode 100644 index 000000000..37ba49442 --- /dev/null +++ b/docs/prod-project-cards/geometry.json @@ -0,0 +1,958 @@ +{ + "reference": { + "cards": [ + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/101", + "nested": 0, + "role": "Лидер", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/102", + "nested": 0, + "role": "Лидер", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/103", + "nested": 0, + "role": "Лидер", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/104", + "nested": 0, + "role": "Лидер", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/107", + "nested": 0, + "role": "Участник", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/108", + "nested": 0, + "role": "Участник", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/105", + "nested": 0, + "role": "Участник", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/106", + "nested": 0, + "role": "Участник", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + } + ], + "overflow": false, + "viewport": { + "dpr": 1, + "height": 900, + "width": 1440 + } + }, + "dashboard": { + "cards": [ + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/101", + "nested": 0, + "role": "Лидер", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/102", + "nested": 0, + "role": "Лидер", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/103", + "nested": 0, + "role": "Лидер", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/104", + "nested": 0, + "role": "Лидер", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/107", + "nested": 0, + "role": "Участник", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/108", + "nested": 0, + "role": "Участник", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/105", + "nested": 0, + "role": "Участник", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/106", + "nested": 0, + "role": "Участник", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/201", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/202", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/203", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/204", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/201", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/202", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/203", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/204", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + } + ], + "overflow": false, + "viewport": { + "dpr": 1, + "height": 900, + "width": 1440 + } + }, + "myDesktop": { + "cards": [ + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/101", + "nested": 0, + "role": "Лидер", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/102", + "nested": 0, + "role": "Лидер", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/103", + "nested": 0, + "role": "Лидер", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/104", + "nested": 0, + "role": "Лидер", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/107", + "nested": 0, + "role": "Участник", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/108", + "nested": 0, + "role": "Участник", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/105", + "nested": 0, + "role": "Участник", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/106", + "nested": 0, + "role": "Участник", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + } + ], + "overflow": false, + "viewport": { + "dpr": 1, + "height": 900, + "width": 1440 + } + }, + "subscriptions": { + "cards": [ + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/201", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/202", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/203", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/204", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + } + ], + "overflow": false, + "viewport": { + "dpr": 1, + "height": 900, + "width": 1440 + } + }, + "all": { + "cards": [ + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/201", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/202", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/203", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + }, + { + "access": null, + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/204", + "nested": 0, + "role": null, + "status": null, + "statusBackground": null, + "width": 156 + } + ], + "overflow": false, + "viewport": { + "dpr": 1, + "height": 900, + "width": 1440 + } + }, + "myMobileLong": { + "cards": [ + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/101", + "nested": 0, + "role": "Лидер", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/102", + "nested": 0, + "role": "Лидер", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/103", + "nested": 0, + "role": "Лидер", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/104", + "nested": 0, + "role": "Лидер", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/107", + "nested": 0, + "role": "Участник", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/108", + "nested": 0, + "role": "Участник", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/105", + "nested": 0, + "role": "Участник", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/106", + "nested": 0, + "role": "Участник", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + } + ], + "overflow": false, + "viewport": { + "dpr": 1, + "height": 844, + "width": 390 + } + }, + "my320": { + "cards": [ + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/101", + "nested": 0, + "role": "Лидер", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/102", + "nested": 0, + "role": "Лидер", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "можно редактировать", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/103", + "nested": 0, + "role": "Лидер", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/104", + "nested": 0, + "role": "Лидер", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/107", + "nested": 0, + "role": "Участник", + "status": "Черновик", + "statusBackground": "rgb(249, 249, 249)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/108", + "nested": 0, + "role": "Участник", + "status": "Опубликован", + "statusBackground": "rgb(227, 240, 232)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/105", + "nested": 0, + "role": "Участник", + "status": "В программе", + "statusBackground": "rgb(232, 224, 250)", + "width": 156 + }, + { + "access": "только просмотр", + "avatar": { + "height": 70, + "width": 70 + }, + "cta": "Открыть", + "height": 180, + "href": "/office/projects/106", + "nested": 0, + "role": "Участник", + "status": "Сдан в программу", + "statusBackground": "rgb(253, 248, 240)", + "width": 156 + } + ], + "overflow": false, + "viewport": { + "dpr": 1, + "height": 844, + "width": 320 + } + }, + "keyboardFocus": { + "href": "/office/projects/102", + "outline": "rgb(123, 99, 179) solid 2px", + "tag": "A" + }, + "keyboardRoute": "http://127.0.0.1:4307/office/projects/my?long", + "console": [ + { + "level": "warn", + "message": "NG0912: Component ID generation collision detected. Components '_IconComponent' and '_IconComponent2' with selector '[appIcon]' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://v20.angular.dev/errors/NG0912", + "timestamp": "2026-09-21T21:13:25.507Z", + "url": "http://127.0.0.1:4307/main.js" + }, + { + "level": "warn", + "message": "NG0912: Component ID generation collision detected. Components '_IconComponent' and '_IconComponent2' with selector '[appIcon]' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://v20.angular.dev/errors/NG0912", + "timestamp": "2026-09-21T21:14:10.706Z", + "url": "http://127.0.0.1:4307/main.js" + }, + { + "level": "warn", + "message": "NG0912: Component ID generation collision detected. Components '_IconComponent' and '_IconComponent2' with selector '[appIcon]' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://v20.angular.dev/errors/NG0912", + "timestamp": "2026-09-21T21:14:11.895Z", + "url": "http://127.0.0.1:4307/main.js" + }, + { + "level": "warn", + "message": "NG0912: Component ID generation collision detected. Components '_IconComponent' and '_IconComponent2' with selector '[appIcon]' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://v20.angular.dev/errors/NG0912", + "timestamp": "2026-09-21T21:14:24.941Z", + "url": "http://127.0.0.1:4307/main.js" + }, + { + "level": "warn", + "message": "NG0912: Component ID generation collision detected. Components '_IconComponent' and '_IconComponent2' with selector '[appIcon]' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://v20.angular.dev/errors/NG0912", + "timestamp": "2026-09-21T21:14:25.54Z", + "url": "http://127.0.0.1:4307/main.js" + }, + { + "level": "warn", + "message": "NG0912: Component ID generation collision detected. Components '_IconComponent' and '_IconComponent2' with selector '[appIcon]' generated the same component ID. To fix this, you can change the selector of one of those components or add an extra host attribute to force a different ID. Find more at https://v20.angular.dev/errors/NG0912", + "timestamp": "2026-09-21T21:14:26.098Z", + "url": "http://127.0.0.1:4307/main.js" + } + ], + "keyboardDetailRendered": "Тестовый маршрут: office/projects/:id / 102 /", + "keyboardScope": "Локальный TestBed preview: маршрут подтверждён отрисованным detail-placeholder; URL browser API остаётся URL входа." +} diff --git a/docs/prod-project-cards/my-mobile-long.png b/docs/prod-project-cards/my-mobile-long.png new file mode 100644 index 0000000000000000000000000000000000000000..b3713b8f41f4e472a16d25e567e7cc2f725c0be1 GIT binary patch literal 38149 zcmd?RbyOYOwl}zOg1c=jXmAMbA-KD{%f>erBqR{r-GaNjySuwP1h=38(&Xfvd(Ip8 z^*6?SU-uu~U+uvf#j^RUIjd^QnyY>;{M-PbNr_2{0U#g%0Em|#;O8Jl5@ZYn!c7_kXwog4dHyi5lCMF#4>(1LiG?1d{Y zjjZS|Kwd~g!NNj9{YLZBhz|W427^TiQ}G-Y+t49%5{pbJs&n&mCHdR;%0HI?NH0Ah z(V@@*f`CIdk#`~@(Eo7((}letE61LCp(VxPOJ1}`k%mk#cG=wO0Av+O0<#+bAmB20 zs*PS9r$B=m+aV}p@}Du}`cmT@Hcf9{{h6@85J#HBOni?!BVJ1)!&8=_`4wfy)la}} zEt+c<87UbVqm)YB#|vxADcRQjbdsV zkCBd?j@lbqLzx%ts%Np{sk{%3U-KVx?S;?(0CVU!bdS5sH~Bp_-7TZ@`}odz_`Nq7 z_1{qbCxZDukwkrIVO)&3ywn{24z$lt0Ct8;y4Ah3BCnNutJz3Py|O-0hoIs{acQp8 zc(8`a;yBUwhL8|q(gMqZLv0;eS~HWhR~E=Nz8>+~2J(S3$x`Anb8ro4P?EK$Hoe-S zL2cZ*rk;wOnV)ZshtEEaLFlSaI~N(Fxuitet=N`=n5X%L^Q=FYRXd%w268H~tmuda zDoadhzs9k*L@`bl;^RbjzPO7c*OHr8P0bpybZ939G^#~!D$N{uiEKz+E+_VWg7CtV z^xKlsB`N-qi2q9CExmiKejbTE^&izRoxV^AvpRboDpHDDodEv^1yG`wF1Rdl^7k< z?@*s&oX?XCZlMfjZZ^$cwwP}tp2ZWdFqk zZ^iXptyx8Bv2F&sRC;5NY=_4Bf;?Dpi*YWhmyQ~DX^Y071=}e`Y%9domr*HVA38P3 z{$D+93#d7_S9{}2+Ne+Vn4c^V9dm{~Js~ADbqWJU5xtjZ^P;3)d&BI-de2ARraurg zEf~|qyZadETWW7lqOMEVVsL?XjZFFz;Hai*NRx-9diEA5tZ4K2mD4M0tv3aBYul#^ zJ}`q0NzW>98IcAsPaa5d17K>NE1ywiyHM%RG7I1g+~bWjswgT7P!;PUnKdyE;m83s zOTYz~UE|P&O9}-wk>fdYnTK*6@j(tL7@jL~-UyAkBHKBDOF!b$g5_UQxF@imMvdEMx}<0z`r(a${d-npOp-3zr8PTGEo=J=&5P`Z2nueT z*_VW7wE|<_>Ty3F%AeDw26t$=t}nP}hxqp90b~ zPUH?21Ml?e%aC|2;WSvSANnF7mk>)qcgqdB;*8un#7k=I;a?^8@RRVtvWA7it{f(BB^XINrSkC1R3!XkUI1IuqxuR6 z(+j$$GZZB>`rt#YZ>;hj&mNSXCA*|+8G)i0mm3>%1NljFma&G@6IyKY(l8+(xO zgp84jO4I#7(tLjoF=v)i1BiUTE<`-KLyAtbLJy6>du4h!D{m0MM7<*u`iPM@0zm6|*#v$^$KIYSFtoRi%UG=J1R_nfbAO2gG`- z?-u7O)^AF-EjK~69B;bIbnPcS(kO)$0~A%n`c#yZ2l{}u<)LV(g+f6w1Ygy<*YT)s*N^IURXJGMuI;-PmbA>A90e;hfDlJ3)hN07_OB;Iga8^f_JojyHRm;K z9B=w!3hOLZKKO|g1bzw(gbLoX4!8_Ae^K?P>M~_!1Kw};ymNA%cVRLAUDd~iT8A>Y z!Q@vIBoT>_&0I5qJm(P?<$`xa%bKUe8C_H7i# z6_Ie32IkhQYbAlp#LQE-}N1C_FnKc=A4_a+a**7dO0-W>k~ z(0V%34qDNyijlYpCAh+ou-%pnkt8%?bxH5=0qg#7S!d@pWSsvRYLPjuSf>H<2-+ zfzNVdk3*%BKnl8-aKs5dloiw`IFA5<-D)x_f-yv4by)hv-QdZ_D#%N@>=tc>4V;m{ z;J;1Sj&1Z&BHJ~prrr!I$irJ0zlkYcZA6`Y%=TInMc6n=alOSnW?@zeFfroSdBl{{ zHCJqx2iGQlm(7}5d%nsn+1$R;tqrK2r5{~OQ9_@GCr0rKDWk<-5hD^$tv+YC7SNo} z3n@zS8b(#=$rhKnX)GO?>T~6s3d?n8#!E?wD|D(78QSb7OCQ+Rvo@Snr4d-Aa29kc z!bpU5nGf0%+AMzLy=TY`0}&8(5KJ~eEEK7UrHe~yk)@jwUNrQY>*iQ_-JNY;)0lOe zry~@b>L?BCOd;yMf>^03Eky&xO{z}J*EWDC(T=-?wedY98(YVX1~;seVntMHM>}h>Z~S$v0%X|C2z;3kMKeE6t;+oj!Z)zvJYqkE`)slG6Vm0(-H+1(pzpp z7NRLunowWO7(vy)XU$ekB@9b_MMugk#%Z_7GhWF5QlP?3MVGlGm0m z++Q|P>YqkZATK&P8ag_}e_s3#Nf@#Um~~yw;urKB5{#}P8GEd*;JBR3yA)&vjtupf zTbNC{;V?|i@rJ2g^Hqvg%{kQ^%)h?AM4WOY#*F9}#=1|d8CEVCz(cuk=va3AUh7GA z6F#HU&Vng;KX!ar&(>$!MAm0ql3G(*ly!ZHt=tXK_=Uy#39Z zzNKp>)1?jUF?hV*xLNi#hiNy8y_}g8;IY(2+R)<+??=JCW*`XT(`e;r+;Ujsq3EMV ztkJNjEPz469J^d3rZ)sR^D3-}29%l<93sm=3S&j-?+wta6`iPxUA_A9EwDD@&lfmi z+~r@7fUd&d)-u+A=N<+A>$T5b@*}yhDh>XjvE+VzqnqUG*?HrfPYV74E?eYnn1R}Cv7@H9@?~zP4DU2_L_RcHb0|TXfCs@&EOc;?Rk-99*=m9 zCB-F*-sj1m00}qP54lX8MG^%DEwNK!Z&?pdh$||B4};ATQ*+<0praf8tr!0|L&Q(K zNY;UZPe&_t&I+_8D&zk|pL~uwg^O1hgP+^wpEoT^JQH&25pk!N@M_{&P*pZ5wP}0^ ze>pp?gq;wK*T*9?QWEs}Y)MdtxxUvx?;w(gzq_iJ-f{$6f#|j>6j5fC!*fqXKNN{I{yxv-<=z z+4Wwrw%|T5(OsP&Ivxx|MxYNm&cd-*O*BX$j+UWp30CbRQR9_#fPjI9;mAdS7{liq z?e8W>9OO_r%-st^y%X1noSr@0?1=cgRl~oR`WwzzN4p&yNTf!EiLmsY0>Bk|nUlGm zHQ(9M3^)L651PajFB8HbM#LXcb``!%s)bQxq+R3T!xPD#bR zwrkwYv`Tu2=O&A z3V+M1NQRe>QO28Cpo$nXjP1UdlmAseYgK8CH z!W|cwiS6-vByRxx=8dS{0GMv?<%~uIEX{cV|H~9?W5Z2{v)?OTM43`h3T}enLUiUJsVVjQ4iSG7+C{WQ9BGqJHP)LgWLlw2IfRUP)KRMQ;rZt?F1fj`+poX`@)(Q_Qf;X-i+dJ_ z8pyWz!F)ptqf2|Q*omlX!j2lwr4IRw5%S725~XE%U&NAL-D)YiMSCNhBpbP>pztq_ zlNGS6@!>gzBF*P{>7ps(-`o!*rBw$0)0Lv_3Lf)Cdkk0HSs$^V!zkEyl=OBc-B#gC$~vQvm<1!*Tqh_Vb#{?+tsU{3rUVRaKCHY+>C8wKZTIr zcm#Z9vc?e&;XGT{GwSK}r7lc>d1&UR4TbLa`_q%k^}>cGjOYAn+HUjkjUaWwfIPG0 z`XwIos?~ZHwQ=qz#tS|%0{VJC!zwvslWK`?o7Bp~Q15VnYMUKX1b{} zD7M*zWmC_eBQY&jMZrV*$5r7X0M@$hdvwvfXqi2+I?$p1eKhU0+lxL-+oar za+D2H15if48pNNyd@JZzg{6Nes%zOFB1H1khdDN=G4uMpDqcbofK+-RXO``Yv&h zHIb6fm z5f5`$;M`v=(N91={L<=(<2+to9z4vw0!ZLx@(>_-qHEw_b?|p~WE*<+8nDt#g(UO{ zy@$NT8!lyrezs*PYTr7aZEpz2U6`6CX9$dGN1)cNlGGvmLxvbvqH)CrZo>gr~6ge)_dmT3A%0J@qSC?~mQQ>+ERNE1M z6lG<%#u;}n6V9?0a* zCku_)tYhDHoUMD$;??5unOxyqfF%W+30BiOGjc02=n;A%ihR`qa!12%y&MJ|-zv2* z-Q5d%!mjmSywBK=VTrNMx`fjcCs0Li^o$}MPe@kXo38NwFfLx(*L-my`WF2`aV79S zv*djD$IabSg{~IY^8av;gs@f6XO%|fnP;&|SX#i)8fC%<5t$?EOvWg^=Wo{4za;qJ zb-KRecJDfd+~A6n9wwfe|FoCdgWjw0RZbj@eGKI5w{X!O1*50BHCyYz!F@;p2Go}K zw9&gkXh5LM4ea&pF)Hl}gO*o?N=(~+zO%pJ-XLXca z{HysBFWD33?2e`!a;vOue1gOhw=@7aNG`us&{>#~mYkz1cH8(xTRxOGp?-HPV6(Fl zO8LAe+83D3fOf#!4_virpnBiIT$E&mFI$%;TsRiv*6s?EuYAH7pHyTsGW*pS=yac} zUU+=YmpKVbp3E{tFd|u=-Bp;ggt2KpXlxHx=?se*pk&D9$)@2+?~{ykV2no(P)DXW z7S=X^13(sV5yN=OHLLRsU+X9K>CDcKa(ju04)lnKz`VMQID5nz!rUcUzayo6iKrCC zF>dfq==^pM!mj$d&AD`y=Q6LJdt|zk9zJ|Cs=a=@ounC_OeVdXYkLbvv^%=jG8b1> zm5ejPGC4E{*lW=6fvGRsVrNcc{DD)R^wvdt1dzNW#-aQZu#7jgdveZ5EsoO8z7mvd z#!_ul93`@+Q*o@}QNQICy9ehp;sVGWt7|7RS;ojh zbMV4SVA+ofb>f;47&JW_>M&)kUo7M^f9;^JuV2H*W21_t9+a3iedFYIb5Yacikt_k z9 z=zxnvH+~&Dw&sF^3(7bwtFzSRlO8VSP4MauRn1!Yr)!RV0Yh3cvcNzee7=HHk(i&`WHd1ctIC#4{6%>EEk;%met6+P9XJPxv?Y8({q#a?(?kOK22{rTXd54xH zbc*$z$5+Jg6!aDnt;kKMFX+{^ow#sGa6dj}e?*lyUp3sRS=(J6ye_OLT&N|Ngq1zn zK3s#_Dm%zG6_Jf6bK?EHS1N}s;rwy9Z@WGG+#AMO8j3WiQUu;$gPW@BByR)znt+}@ z_~qK)$j5qnZ{#VYzj^)%=!8%@g=$qPbcI;+*+fUfVt2&bq;TMk9imOViHzjBfRC9h zGz;yPOkjR))GeuLmH@#F(#o`kO>HkI(|rr6W28XaG=ks4UFrTjxgARtfgak#yQ1v? zqwZtKmdHprM2lnbxt@dfRNgAZcx&(R@h1Q#N8=kfi6ZvYlp>tLq^aDIJG_^I>j|r_ zIjzH{^`v6$74a&iIx(EsVCW|au}eJtjk-r`EayaQwsUwbwmuib+cjxT4~P@lh!G#Y zL8OKl%dF7A5_X9nVLLhy%@^wQ{oiz6IfWabOuHhwVBTS?EKm~8!kWK=;M1DdtgkJ% z1s!yJBP2%&P%uORq)ot+tmO58$!!h0QwCd00|Kpz62{b>iFz5+(1(bh)`jO3#Zxv{nN699kXpnjR2s^5ozoUK88NoY-o zXDwR}DsDm&OKu&6)Vr)wX zs{Np4K}rpKtKJ*174FQEm&`TX0m}AhuxlN1;cbP95hZtiE5==4+x;1LrwG-O0|olB z>57n(*0?mcU@^jX?Vjd)y|%6m3gvhOg*~Z5V^vW;qnQOke<$1YOkI}xiBVFe4)15u z6z!R`_K8&%>tWwb)v(+i+GVUAILpPxIp_t9ccgZERVfm-lck>;bRb>hb{Ar~gRAHr z)ihDa`&VwIDb=zq7YS{mM~!tPE&0Z8l#t33zf>m}@vPmGf~lt_GdTOs z?b8|z8+8$Wbk*0JLGNZrA*5Q1m8XZ93v zO5L3#`<6>K?VjRF&E)$Ba$si}(BM0kP#?h_7t~&Ue!X`TF2o^>bOEA7!aD_FCHWr4 zu?u%74gjci`*P@sfNvPg0`t=j9 z^|3#a*#vM{hS?Y3eJ(_PiaX1eJ!`P7Eo~&PFDG+1JjVq;6>ND^wFYXq#rWnkNoT+Vo zdy3L}a!j*udaawy;3fN_|0})brmq5OKcx5a_1K#!PtPJq!VIIYWzUH!~(l4o+!F|88wqYMQuO zl(^Ex)i6s`xDRzHAz(%AV#=@+6q7L0bJ7bTHd`j-s4vxh5Z#?;T3C&oSh;NPeKEjU zprwN>dtjhV8Ii~GOr^$0h5l_DW4+bbRy7TgWwGbgQ?h=Cwlm#^qSUmk3(N?=jC8F& zV~sX+pW`;nq>2-}4C|FGo0d}xI@z*e?y%BgZ>{(;bsjjeZ#mYC)#6`Zf1vh?d;_!1 zUcAjmh_2Gok|ckdv~9CW*|8?~1BHTy>h(9atBDz9QANZp<{@^LewWPa!W(~JoG(lXHSTS3WnfGz*2I zP1>@(`PY`qa#~~C?9b=Z{?oxhCU~~9WsB@{)0>#aAk6++9ai!!*2-10vLak0{s~S) z#eC(#22jHa&GLLy;PsW=#IPtE^DuCWKB+02vo7;cnX@Hc5_X~YYr{^ z5wpvf38O#5!6nn6$U4*>rrZ+FF9Lfc=$7U z=wC~Wa7JWgsOc}Rxuf+fK1kpF>Q=dTq`n;)(}!2PP~=62BOdEO{c5mZ5_w9GuIQJ2 zw{^ck^%twKg}c;*Z48_y-2h1lnc9emWIc<5AxEB|mw3v6@6h-fR=7KizEYN7W~!y4 z&b-0OEFEVMI-h&$lVMl2se-Y!KaD2|#U^NdiDA?@J;gzwlA=;k@hg9vY~GBi%fx!! zTvh8wi7Xd_CB|A6awezf$juLu^dcc$)C(*l44CmsjEuqz+e>+|R7`tpeH^|lTf z>}7s0i4usWPO!NnHSWEqOxM_`{hop^Hh_N~*NtsFZfm@CXZFW99Fph->bN^Uo$2?7 z`7;OxXNx*9#q=n$%E6r+zf{|K);u5*V0MhY`jNmpzHP)>oyAUu!#qm!8hIbX1 z$>ms2`l_lNzS+g(h-|+x>{ziqTgxjQ;u6&b9qP)=2}qPz87Hi3b`FBHQ#a+(8afi2j zvA1lnbG%oLVqP>t@22>EywoNw@{Sl6eWjp&16Ro#Y73R-zHDXI)0E23Ckyt~*vE&5i^92~j}vgrD#$5F z=AxC}n9?UseUPBwjjP9o#z#dqh^m%WR0JmOu|#ueF`sg%PVXd3xzlWfmlX&YttuT7 z)O>nphXNc*4L>3n+0~^hqUt=LG0pNxile+5*lX-u{RF5&8H;ron3I%Ma(c@tPmb>* zTB4&<`0X;DL|T%a&o)7%f+Ka_<%wbQy2RtH!bfsVitAeB>{yYta$pN?tTgRm|bLM1jD*;?Wl^uAVB-j;ie#$%B9=NAno1Ya+TQx`{>%u z4O(01uE>L1DmJ@L97gMi4X@_r9iO~?gMyNYj6hP8sHpU^%K|Pcmc5;0bK#IvGy}boQ#n#begaCC z0|t*IqKTK^YL3=&0Q*fj>1G^quI*B0m=Avvvi*5q)|PzPPH}Qcx-@Zd9z zO;?yWNHZ|C7lHw(jVKx>l8rncaPM}BY2!BXlrjXbl2cm)rF84Y_JD{u_T)!z(Ch8ETD8aW81YK>XHgl;=2{iP4GLjI#Axqk$5ghJD? zoK&|U08Z}7-BitJ@x4=hFecA&3&mK>B_o!ZiViUieVMM4S(9Cb@Bw{+RQ6HQCm zvmgh~4ci0Jm8*A6X^3 zPDD2|xQ-Y&csxNQN`zLQ*vFP@YV&%#-CE1q2nj-4djcYb{iCl8i^F`%WT=A04?W+d z96p?wV#{){3oB~SWF=pwL9Vt$%NQkdww7Z6rZ$V^wzT_(nmlQv5lUNR$mG;l*G*;h zaOL^r4Vrbfwmr@nc_#w;w!ZghymCnZh-Vyf;%;Q$d7~_IFtq!hsiP{DHt@d*MI6$h zcS#X0wy(#+W56^u?lIxcTH_?XR=^vj+g^H45tqD7oQJ#=R6#sT&y~tIT}NfZxTW>V zc1xV-fEXx==~S7lr8mFyqn2$2D*!1!INx^Ey-Wu-I|s9`^R;@>KP}{UD$SqHRDZol zGa58jlrf#LN{vY|K8@N@K3Frtp7cF>NmEqZr2)m+MWR(zTBBtq=ECwK0Xw`in1i88 z{Vc(6T4!zeCRhk{x@A1Vpll7MCVLBPT$pf_A{P(v?W8kBQ9roj(4ii6pw*jg@UzLQWvZ6{>X z%HF({IC7*M^;3!MxlNYKJ|V^s_JGarP? zBdXXOOJ(Kcw-rP}E%kXrl{}XLm*meVb=5}VkngKf3e7l$$c2-*$R!-et8Qzbf8+>OBD|tzRtSyCn`n2Nhe6`{JnwJtia_<~)%D?&Qj-sGv!v~Dn zeF_iLVGAANhsjbT&K7NAQJ1&+1%z97R^rDmiiFAY{$jJOC*|g|L8;}rl*Z=76-D@T zzT$^-*L7Eh8%4>=2YAA{Q5xex(#o#hy8RQ?uU0WKeQ)nMgq6GDf^qu|YUHML<`W?0 zvaVcR(`K1EootpCPxoV=b@o&|E~ zI;eonyI8J*mmDtv2=CM$ZNHMZ@9beF(;{1Xy06dmY=y|B#w`Rs=(CaV<-vn4CboE) zx7Hy~sGzvln?KCjdIhui8$H=lHx>4NLR@_0*WoNp5V{(A8KKJDDeSM7=%g}O^KpqY z;IRG^Av@=yx7A3;eaOnpR z@3@G%{WM2bR(Z@*91@FY#)SI|(XxY(YZ-Nvl9uh^DjoBC-4`?zPLte~Ek01vV<*cA z)Ban@5=Q}QF<@`JXioP4?DXH#QpKZTK9*@bwNb1D518Hp-0ak5HOv_3jg4RXHMK;> z->mlSfVDY(iz*M6sx7|uLOF13rZ!oJBbkqh=VmR_45&kYD@DZop}CgmWO4b-PIDA) zoNg~>KQ?h-emP*v8iX|E9Kr1)3l(5k7581^bFs1iO+f9^O@F5aa2aH$+4|D`)OSD5 z+^JjNmMG*c4ul#J)u_*@01fnd0P$-D<`xyTciSraV#_*5!1}XQn_F$--+I_SxY3pG!vC^f%WU-U~XrJ zY4625wY5Ss)2JIN4l7`&@dv8Gb1DtI%8_w}N5P_Q(!iHw^!TKXgdhfU?OpTd0x`)3LYM^stl=EN~6LxH#re%423v# z4n+w8$6wrir9-T3mw_6^$Glih*VBr__!wgH{#)Vdb&eC+Yh^8v`1~`$&++Ni3@Q|36L*4|6bQ}K&jD~V3J7|LymvO zz@qi7w8X{+qU}1gi{6j9zLilrzqEmc&6aQ;pSXLU%-lRKAPZPJ)VeM246=VI$r#Cu zC}bOKyq7FCaPoD;B-Dm`5f%q>yX?`nKDXSHS`pDCQEa}VHsW>I+SV>=A4254Z>E8# zOAYTe&tu%UVRhE5)fKjkjAby_PD3NvjoowpTp3y4A(f{r$k=%8ga@9J82L0$ogoMZ zA%GxmR^?o!M>;Pk0>xrO_LlzeC6(;;uk{znUkfq+Sq_iT)vp*v#{Kb`at7Ju}iRtHCw=XYh^g&m>cFw*lsJrXn#GT zG33s-bt0VvjC%p+k!f>F^b^4K@RZP3tMh`zDmmiqC<4 zO8xBnNZoLD-#>rs=6HLz8BPVu5{66^FC-7&ew@^DxVb#)1~Hx#3M9CN`Uwzv#@s$N zdL}({dKvRv@bDCYq6>{T*_pl2w%~}EE+DYSw1sl}-qUMDzlZ4b%P$DCB>I(`!|Pi< zZ+w06CJUln9h%C8J?DPl3OxSV>+VNldm!)Xt5e=*hwC52giVOjo|DJ#CXhRD_Zn~i zqW33kd586@i1LN=-3g1vtKV(*-hOpEZBa$krNI#d6PvH67Lys;=WvQ!h@YCwO7n>& zIrfN4{#^4B-+$4|eAW;%UN`v{d#usLcSSJ%99vL$#^uTiO&)h6xG z^pa+2SMZMS{r>#^anC;H{6M>b!A}6<{-2=q@_%!1{1>ln&T{k{*}{eX#cBCJC6l(@ zzW)a^GHU8>E7~2ixX2^a{2>Ysc^l$3NJv;o@|#ykHodsdSZWq$Pl=96Hu?L#uf_im z%(nb1Mk*?dP2=X^5|(V=cMl2w3-(FuU?cl*3>-iKOXnKvflPqEIDB!-;*I9ogt_AS zxP5fP$L)mUe_wh-6~Fc%`s05!p|qU`VuaGeUkrDm{(apW{}4utF@sY=ps>ooh0jv3 zD-hTJ)84}^lZlU+JR)UK=VS{UTlhrFlUn#`yb+_Tu_~*)D#l-)UZckUz||`%S659D z56f6?8;Z6T+EICYU&$3U%iX?a>u?ctZq6(qTzKAW-_ER- z>gz6R?B3w6KN4sE7>~@gzc5kya6{eS9~f=i{}Jj>d|ck?zLO9#Z+ReXD0- z6~Y;JNVAcaZ$sfA_bt#z5@u=MeNK1_*62It8T8%f#8&7ZorYjVqb9SYGC?=V#I%FM zN+f0;LsrWoS{ROdVF^&qwU?B;#yL+=VJ~Ax6fpfw^adR4Pk^cM1r&?>t6Lu=zr=n7 zGTyJad}HZ&A?-?3|E8B)1 z)yD(zLnh=~Fn^i$o358k~CPYyro z!NUJ0Q|hhbj}1>_7DjJZW-5%%2X$=%+inp**)+se5bnRTxt5NI-=Y}9ZUw&FcLo8Zd*ppzesqG8Dp?a-uQhvaDGEC?uqV3-G3rf; zd&gjRm)<${?@8F#S(|k2qk=o;Iy;*KijxOC|Lg4;t5YefsG|GOiHW{iM7H$&TdwlI z?}rxy=?^B9v$6l1Xg@OcA4RO}`4==Xk=lHGv$+g9Ts&=O?A=c?&*nL>+D^cqMrM^A zvEc-_>g66YiwwLrQDo!b0L*<}!1e26k1y-duIKJMaitAf!usVzk%#S2$M5JN{v&f5 zd-flRIREXgf*12+9>;bI9Zj{?j@kovwfF!8P-`45ePe|=j)A2;49WU2&@B3i+~Fpj+&oXfk2|0s1h`K=se z>*iw2`rKw+m}m7>c)F;_AjfZNR+f;ZJ|t~pLZGU|!r}OB>j)NmlnPu+Wm(`MOm|?z zO5+*K@Q3^ytxfeHl@S^yubQ0$s2E`PLhCg3&Lb0@VjV5KaNhk@i|#4^lkZIPT>wgD zW_GmgmCj}0l7Qn-6ItR>GcUgi+oi~04w6&Z)Tp*^1miV(S5==Sj2Q{NCxyj4uvzP( z1^v22jU-TQ>-*$rKra!m&ScRCZAW>VfO|a~j>d0$K?n19hW>rQfm$~`a|2IyP#&eA zqPZZUf7@AsjWIBgs86@l2jQq=4LQjpd_WLF$2_fETdNTT%!cbUFJ0esd*WsFmQ3`^ zNmidaT$5z*5iGE6Uuqr2g=#FQ-(D+XSiVZ%0$SFx;-_X#obSnyWl7@8>O-G~`nQ{$ zxJzyz$G^83mR_=rDyw+9q#@VXDh4DfBzJkSMmOCNt@sC_yY@@(SfJa=5g6pZ+t@3 zlwV9N9PgcVh1A{v9BT{qt*d1kb{AQz;8Ss*`Th~wVfx+49W|5Mm+~M5kFLuJcuioG>xilyrD7O7(pAnk><1 z10vf}1}1?>+T4sdoS@HucQI>iIOe>=ac3L89X@R=rN&N`nWaRZG{Jk0gZKH=y~^qd ztTvubGv{qy*i1a|z!R^nAK^zRv{xBviHBEWq$_YNmHB84RJ8t_a#O2czzQBi|Mme8 z2D)7Om6hU8F^B>B1AIwkf@G(ZDEeu)qbyg9jrx?gT9)WvZfY^i+kgb zZG(@saV);=6Lw0Q4>jgv?H@VI~Weq`>g*yHK{@cJiR{G#h%gVkv*rh?R(F+ z*?s&xj5wiYV2hoUF0RNJtktJ+$N|y)Suc0`l+w>|l|<#M=A2Um$a0A*iD_|#vC&<6 zSnecdWoqvfEF1je6xA_}yqj)H^<_h}G0BMg9nZ-)Kc@5~cwJ$_gq|V@^npS5uilDmxUWxX`p_WBR z*2ju2Jqf5*XE;$^U~fDj==I3#T`yrN8CQdt1z38U zgEN3%NEC5@$(6kQ;rxo_f=;MX#3cz}*)6WQWRp0Sq8D<&C zkGJ@`pXbXKL9MUV3}^6JankYaZSX#Ku2tJ~xSuw2eJpZHPG%Cfd~h`t)u|O}EY5ng z-_Yy|Hs`B*rCXKo@d&%>W2EYxt@S9rD!2H7Y2qUQZwE#CO-mX6c44Tj zoJ832@(4YnQs=iyz9(4BL{JzNt|DD-vMU$cDJbwfg0<00u-1jF997*e=On#^^N>7O z;ZUZ~U)on~kUl!7m!6czU1(zvDDE=>XEkJ?s!?WGcBInKOyZvqmP5tj0^w0P%6E%e z2ztW*t}6V+$hw!=b30IdbHvqGo-n;yAiKr)ST8EcArzC+VAidU3@9z7!x0gb!052T z33I8clhsgU6)`HHD`+mD!$s3n_n~XsfRv-9M!RYndtEV-+=AK-DtM!AZc5cmN6MN_ z#!^$`u!+cH7fQ90?+(##K^NC4$ZubU3T-e6g|^RsTUxF#PWrSbVX3-Q1v0jRT%r=v(!W z%TjUjXvd~&>a@$_bKT0c%6f~1E37`y(y}G+yy;zecLG)G!uejq%NBPs4N*F%mx5g; zq$^x@%=Q@u9Iw=I0SogoVUX}of1PO_BSi++h|7= zlzW7p1M6$`sFFE~pXyd?S=B7%A4GR#zyd~^RuM2^S;=4J9+GyPlZd`sQm4Z)2@qkJ zK61jxSEG+OSE{n7L1#hE@h!Wgb5Y7Uk8($UAS>vjeH~(U+oJrbgJ3RN+}BoZebmhT z-FSr=U%8OK!T0}*v$u?Et7*4~L$Ok|pJ;TK|5o zOtJ~8tbWGRD*W*W=P3gN^`<5}e~6i>(hhQgbc0Uhbjr4X*l+rgM0$VTgOT+LeA~oo9 z92jG1tK9CTMK{#jp>4au~wh_g@+^i|9J~ed^8=L z*tYQtc?XV%*`>^nUwjAd-A*5d2pF)eb=MD`&a9YGK~^Dw6+>Qvx#GcO24E59#Hk2{CQRMXcj~Kbd8+I3+A(77&%H}#&9g&6I zMPYvsNDOKrSlUirxN+9W{Aixy5xv{uzn;vF(0q{OWhP3)1&5*x#bLfJVf{a2=B$Z^ z8~EZe+70KYaIBUx<5Mo)VwS_ntGY~{k8Len6q zHmW(#odr_X{GdZ)sH4D0#Gn1S%^gokLrSr6a@56_8ePp|<+Y3_YtK1fZV1Wc` zHuHZ*W8$|w_H*LNMOY3WcCr;Vwr9B+?}~!{x2(H*BAl_7u!@%wKO;wUJNK-}LwU2} zRb_Y&h)y?KP71$hSe;?bnuf*3^{kWO__KKg%-ZFivLr&2?Da##ijsSKgm3%Ty&1>~#Mea(wTWBn@u{sJN6OWm6sGV8*Yce}quMRQcNu!{U z-^vr}c&C=NXG+Y%u}BVRRu4NpFVoXb5>^g;c{EPMxb8PsJim5l)8LMSB>o*(Nb`sz z$^k)wwRzOrA2K%`=^JHcrof{i9;?qkvGO|E+qTe!B_L7R_N60-czsPVNqT3?593k5Q#kPG{rs>5TDb@oFbzRf^D-q3!1^2q5(wXGDfdd!^2|y+ zOARftWMa}c#X$zAxTFCoRj!L!@lmqUtYrg~CFw(^rd?Qv)cgte!XD2Gg0w8B?qt=# z8iDrZLif)~mfwwicqu{bf*;ZuwjspAvQ9tFw~X>WqoG1gv+dKDZQkD_fuGN(CBZZp zj+VBynNx}(tl6xd0wL^=q-G(QRFA2i+CAAWPAMqC!y}~n2Lzb%+B4@40fzn^{F8Vk zc{YNrYKu$HU`&L8Ck+*ew{T|~Mx)=+WBsli_h5kf)% zyk$ZI#_(-zq^_h{*V#E|Gds=p5`6=;Pvd}M;Nw`$He-m98`!LqmVRp*65wn} zJ~(b3AxC1;q)ubSq%AXE)(W3AZnWI5VzXzPQJ>x_+%HMiQq|n(n5jcHvrJ;y-wrC9 z6g(Mg-`?jxZScvfeWDfocEz5TF^7Ry3)uueS<|4P6M`K$uTYvR z0N0m?LmxKFztC0?f!iBg~M%`=CC(Pt+(SIu>_jAX|r7-1z72>EBbVsx}gG zn=GZY$9gl=g_o?Ms1nInRDEWULZ^mO!j_}rE5~C@yZuR@o)hCA@cdtZ$mOn`+##-H zURC&KtkkSXlYTL#MQPrewarxXCCxXIEQ`GSGTZ9fe2-^Imys-5R29d1)M#jS#Q$2z zsNDY{7y9_O9K;yfP*tkeP^rR)4|m?1EC_dBvO6)mj$#%Co)q}-4u6EJZ6rE2IXAAS zZ*zBV;EVCMV^sv9%}`gr1EZl*hdsXkov~%hEcVUudc368 zkSu|;n{^u&JgJ$t)Da5;MN5N50ZJMiN%PlWkYp#h=YD0DD~+A^r6ckEUK8$2Yw)N7 z(EA)raxc|-RWNUv&8SO!<3@{i6LGO=gwfo=#N=1#(q`i{rs;$`GYXZADb{IUl~|s6 zSd;89g*-BsRoxLAkQX3pXxVHE1h`jJ8z(5M9m+fXbTq+JcS~w3L5UrAB66J^BmKMLnOESG&B{rg5c1_@{q#Iu%1Z z1^pFugeBCCMV<1|tkxcqhvF}lwYK#;?mg7l{mNr2AnaGCj8*bIz{S$te3KH=e8y}E z37z?|egp0+ER%FMqz_rY^F1+Sk7-uFPNG?LeXMchfA^a6{aX{9 zX_s1ffm*#ICKXR2UiC8}FSfbXH{NVm8xA>k3VF|hF6TF1(JwrqxjGD}@YXK#x@t^% z+F#G>4Bm-LD0PYzBSA3)i3mqNSk4}2#|y62RKH=EK<66wpF^z^nJZ{=REb#mgrPYz z1&efw3K3mqg1DWiJ2l{Ag=K0M(`S}g+$?UeM`7_>+mNExWeyfweV_oc(!5|O%8RzR z-V6Rse{3oE@bMSGU^a(yyLkS|ZSpRYUr4*}$vY$3hnElA8X3e}R_kjw;i~CUx+$ogc@meO?Oset_d#&e zuf@L|$qoZ~<09(W9E!QI8JovSG?rgKR=iL9%=yjWL2VT)M1Bv8edri(U(ZheKy+R9g< zSPDtMHxBaj3zRYB8U$lR`tv6y#=mH}X9-vs=qC?^*~@@bdm4p%tf*68tVhJVBM(ac z0^GND$0Z5Alhml@Qf=gVQNU9xTx02B*_iS}4#$g4@8uCPvuTtxFdSOx&uZKG?a6cw zy^5TQK1)eXKF?+R+upkO(3X?+m1r|~)ZFFd9b>lTMIh=+ef(Y#yF9x(1ebApd4^Iu zuwer`jv<+*C{;z59G|u+71)Jv8Oxy^pSrwI*3qHepQ~Y~Pcm^fa1Cg4J8F!=3Y1hJ z-cJtnBp`Ar+JGQul6T=1C^Y%^RxN#8-1w9#R=p=j3mPp(TJM78_qx7RNtB-p?B${H z?0Wly1gK-X*^t$FNUbGIy?U<9mzq^QGCe7lP@W{0KDO~;)pq0K)CR+vaoMH%=aC&L z=OlYl004WT9(J*|)@_zkM8Awk{3RQ#WI!Df(XF%tAuXMM}|*9P((IF|Jo- zlZ+inWf&&6p2*lS+i!u#yZEZc&bz5NBk5xM5W$@+|7sD@Lw44TZM^ZZZiplTVajt=Z2#8U?7X9b{ck<(ymc)-$INSU!kErBa8tQyvqV9;OT?U40_(6l z4%qkR40`Tg!18LWniUf}-LcVn4z{H1J87PwiC*;0a7CKFQT9k@2i*F- zA}L_2ncu#dp(rU~ZjIrbch97_;-i(ZGKL&uE@DIHl$2vRf2>A`<(I;1rQhyXYnr;d zMtamK86IkUZji&tx1Jx)!p#R2D5HN9?qBN`^};nUs2<(uCUc4zWF^31AXMNA4IB;; z(A9mgy4g-;nBh*9lJBH5R~H;!JOvgm?5nG3kw$3_y_?aPl(&34fAb~Y=vx~rdL zy(O!&zDz3x7v1Jvj}Ff#P*0+8y&GEt|D);Fix%6bQq(~LX8 zj?25mGIYgXBk%g4Uj}EYjrP$9*0~m(M_pQOMHmgn3NcJ}%r0$kasoU$#5sh$?Yn_V zx8s>;!(=CsNzuOmapMJUD@I)bb~>MN4pfe+Xmvtr9YeVM{PV?$jc8rM=T}s|*GRrK zF~J7WM=ba3656}cGxV_F)tH60g$Z0J{{dI%<_v<`MTMv?Yla?hOs#4zt5;s$h)at>x_e#{@X$s^dJoy?4%S;l;TT`vHx%+{pw27c!HB>kkw)JoZ7F(k!Jys%g~DRiVWUGF#U_+8=8k6J zhug_F03gJ}h4u4~67rlvXI^!(H4&%k0mUz9(xBtXw!p+ICZ?`CBb3+nBAixNMdU!T zBSjOrGV9pcj23rXL>?)TeP5%wYkp9Sg?P?n4M=!a9VdSg0uE9T1!{IGv)YA$v%ZzV z>MryH3D`sw*uowsPdICpBKv^i;!<^Nnby3bDlmLcwhPZa7oy+jA6iwDX%{DahblK_ z5*L4=JW}Xy60Z^wsCXCtJli7$XqC+k)I@h&`Z;CtsH2m?GOK{bSohonCC=?c+a5$22PSi>69_{4o#DKXX0hRO1?`K#x`RV~C-1U+W{* zTZ?0cYr!*a_rp|=*{5l*L-O+9ReL?^0Yq~drX;8!#ZTWMUz z?sJN>!du&D`88zzO-~C;D*QEfX;S&XQ?mHTeJiU0#^qbXZt1+-QUmkqshWF(mYE%< zg&Y>GmwS&Mr#{A$LL#IBIc%0ucf2qzO)Y|sr)*Ir34V<=GP09n|JgjAhcy${goh#q zP>||eeMp%{`*E%5*%q!bX)OA&XsFBai66KaU{IdSDFD?>133BB<4}KWQxCAOo$ao@ z_>Qd)KSSut2llj36cT>&bOIQg3oH0KdGeWfL3h#;KMa8KrqZ^!jNP#7k~(t{qq2hh zqT>8K$X0Bex-5||$j^KsrERuuT3Myn>-kvij`}uITWYbkwtKP`E1u}5Mc5FEK`Jh1 z7o>*^OA|6XtGQ=5FF<@P;}Td$#1HUurnj}os4r8e51XDaj%ob<`AmMBZyL4MaPc71 zRX^@l$PyECZ_UxL#;)Dz`h$UKZhoSAKfMS?Y%|&RBkAb?>6`v&<-s@DE)?tUJ$x*H ztBJcUO~+MGtr_ANwU(jCE$D?@HQsE;Hgc}xiZ^hkpdr$cG!;9tyB8Ioi79!f;qw{q zgq=GUps6htujW)?QK1e>76ig_mee;H`YtEFhizW;T zcK)DM`smCWol+$sxeS;K;rmLM20T{5Fv$or#-0!=QSu{!_?^jRuG$E@0t#>R>p>)z zU2JUVdf{b|oZ-h=2}-!D;;Ui=PEiBrTjc15R1!uU=Cv1& zCH+?Hj{~0Puv^#+JO8j(L1U0NFBN;$!#iqi#=wSlG$BI%6Q*0=O4Tcv9o`Ag1=%Az z@Y9<{u}RG| zgd*F~ItO+inLX_}!`FE3rIu9=IB7+kjZl;ASfp#3%zzA#g9Fk7!blho9TiY3X#1O&KrL-n!x~ld)7AA&}9cw#VllqI*?6QM1lGU(` z?y``U)KE5d(&nWBG-KYbP;!dIq(tKH4iA|nD{UVJl?;?HiFf)~&s-L~dU=VvnXpn( zk{T+V>+lOSD311MpDh|!4VmPB0(0GyZ43KoU7_v*|M%I|M6-e@d*74Rp|E! z3$Kb^?}vwsaPCFL$acb!4$L?8R}_?U@3_K{X^ELl*nP0~7877zX1cs8Ho=F<+u@ci z$Ly{pFB}^SI(DGk;yK`O69+u% z&N+J6-A<`f6p&RQoQ4feW0RiJQ~BQMR4@JPc5oEB4*P06M?9-XGU;XN!WwWEMrb+n zt|v~WYEiO&N%dYmHf!@O`B%Mp$wN_#2+!mURkcXJ?nYQuqrCZvB@ui5Qm^Y&%|LP{ zU1Ws?6=LJ8rCOZz&mE)$YCB2E^%vrwAM9H-e98gC2F*9U7x7-_0fZqTFD1tD&_{oJ zgZxq124FB?%?#~D+RP6%$?57e$lN~*dW8~r3@pd2pI!Mlh~SwbK3FEFABpM(hf%+k zSqPVp%{*f}V)pYN=S0Tf52uZ)a3I zyH``-1CvhV+LYQ*ZedLA`}uax7>;^fKO<+)y|31oOLRuQx^a@eN};DF838J7?>r|O zxhYW0mNA_j>tJ5q7{SdaW@#nriN{2+dX_U~c?nZFyk~B~?@pT%ln1e*A+(-$5I0|O zYEG{2^`Zp4_F7@Ly-1BS+X>0K_#Ic(uV><}k-brSw#*Q`PsE3lOQC}rATTvtNBB^l zRj8>wz~wuJG54Z1KU)1_)PHF2&pZ*E7`XhCfn?VG>~Cs(MhSRR688zRv- zbbF43}7x2W}0LBCTrbmut@Pl^7OdDZ(_ zkId_Ru|YYaa6N#fO+)p7k4VM-JEI-?Uf!T?3+sKWmGV?Otpz zXj)${E99LFen6RyZBpL%ee*R@-k zO?hfe;&=(?g$$~X?U{uRBc&xz8@N(1+^ds%#%mA=Z)A7T#K{gyL={KWTcTZIqu5=t z`gU^3?xmV|WW5w1VK-RwN=x7xGeL$_6cxOR>2F;e8_`TY$CjT^9@Np2*F(z}tg4o( zQ=GKG1h`GukKCiv#}A~K=ipWxICJC}TIY`SQB7Q<3;hI^J9|88av=(BRoQ-NJc`~v zZv{8BU-AfaWGB*UD|t&ZbuJ6{;9DdZ!7q8~ILs~>SS%iFY#;fp96~Wi?oQ4awXA-4 z@;058AkL-#c@P7#OQo#W3%;f(BcwLevU>tODz})0c3Sm`+YGN>c!Pn_6nyID(}Hw> zlq{?5@oTJV8k(^GN(A_aV1*|N6?kW6&TTt0FShkg7>$;wTRaJUM9$FhQX} zVZ{;jYa=L5m4@@ik_s~A7@A)-Z0dEb_#hcI!E}VNx{VU5!SYCpGVDjSzW^|=Xsm8x z@lKX2Pl({&qtu>~?}kAL&aNsE4$89>}d= zi`R8k=|9YHgp2mG=_+H;E)A!|OXOoNoR$W5>A7X36ufR9#du<2DKx+5S=ffF!YCO; z@|iIh=y?*C@VRQF-^8WoqD>CjGfPCQ!bckmVWa);{iXhu>T$jO7x&-=7lLZ=T>%X_~3^b3HO|MRO-@adOz(8?5M%5jp8SEuwr!fDndCGOhGF3Yw;{Iz?=n@Il4 zN827H&HT89UFJ?s!zfTIN5@Kw?YHsmN0TI}TOtBc$B{Qa7(Zzh9$tOK1*&L`M zy9mw?I2@I4w>%mD^RX&9jNfkl+faQ93GVT`m5Du{RjX5`_dX(G)w3aa$pv54OjnB1 zAbl^uC)2<9dd9==e3HkmbU>kvQL^xlvsH<|`YcM2%hp!Vi5PW*foz5GjGgA9~)S+vUiG%rTY|3&Sk^j-4{vN-1W~~w{EmR)4 zE%!9fOCz0(tTu0-5Wk=M>>FC&!Z7ju~PPKp^(j&_qER zchfS6u7H23gB%#DDs`n7>uGxYidqUQaXkKy`1 zY|A$jKSmE*=|n6|9Xw^;kp{^-=ox@lL`Nd3XT z_jgH}U_u#Bfk)s6M=0+9Z``acXL%Q2X3wZM-kxCQCN@3w&&&v%5M2cjm3bFe{_e0% zgY}EGWTY8663S-^bf>TDPQj0Px1PX`Z-|lzx)f#6r zNat1X*~MB%T@t>G#L|kvDIsHz&}@DxSEm!BpYr#Rjbu`1=Np^U4I4KaZOF7F{rTDc zKi@EGS|K$Me2D%pNA0$0;}pjm3vD>&1Gpqikns7-^RvC1RA`jy!3?HR=V#xPXGF_@ zVVe|WG?O#jxuVuS12n8Qdoy-1O?4^T=W>>pen&7PZeu-L3y2`Atrg^@e{Gp3nK5&c4P^&fNqO=u0ixrQEG5Oj z`dZxQly@wp327w|3CAhvzW zm%{ZfB{j6K=$A1Ol~Iljn6qppL6yyV6_%FdYlf%I6z+S}0vEN_l>i#_cH6eb3(Nel z!eMZPm!@rySCsWUSuZ;1lxmSV0b$)xRceTBE$K@hmY4lcxximFY7jQ?s&cf{krQuM zdq8?44DBfU%)&vJ1>4@SAATCJM|~`>Qh9m%^)%!} zCa3_u5B}~k4yq5Q?Wz%s6VO}ZPTLWII38l|*_`SKS#uSw^M2_1o-Z`$sU2pLT6^}n z@m29K_$}uljm5f2DoXV7g3IFZz4}?05-;2H_{r{CCvUuHSZD1rOr7$LLa<78Y8c@g zOw`nAOjL(vQOQAG{nXO&FwdIAqU4c2!$Q+DX;$y&A3y>E4%KmjBvj4z*1ddbWj?sy ziVf-l@Vp955;`7h9*vkCW44xeXXY1#rBXk~vZf^?B_y)Q4(;aooK7zxnfK4h`0pOR zf7I_l7jrsGqbuu{LVtVCWLYw! zzt&!79q^^M+;MC>(oeb-+psAfyjW8aur1Qp9iP|qnZFyh#W(R+zOjps_yx#p)6mn= zzORt(J!^i+u~t#uU96=fKRln&mHEmgAxCnOK`*Yvw9k=|C#rU$p=0roKXAt&x10;i z@S(j@+cjnqb8$ML{_Q&H48_rb+ipBg2gxUCsoNJ>B{`1Pc`nyVO&-ylt@!v64L}^q ziF}2!cyq2sH@X(6sy>gm&Yk*(92i@bEiqMdJ9MFQc6HS&yjMtlKv0LEgG+l~vxu%Z zKR;37sQ~Y7@`U|W;1*?$i(OY``jLX4g31rh{psV-p0#qC)j)K^f7bN)m#*UMXJ(D= zXHcx6lM-9vSL>Aa6Rbl@?JLyLwp=N)-Ku@kQxS~pF{M%t$lQYZ@xgBR7n107zPXUp z>o;TSs^NrPTiw>llu9zMA`9F&wmp(wX-tESzJt}Khx(YZEIl^eD=o-NI(?+mZ7VgV zjkxQvE9a$fEy-TDJiIvTU2E__$w5iBDoNd&#r+)V8@GJ0cg2=DspjGgmmG5c7okyaVBIw zWpw#&s^b_p6d_#DunzjjJ**j}IO=4Zlc{cW$t%pnl)f%*hd}XQi)GZqC=S9?l*<2B zI_AFv3{cOn`79dV?612rC6A38`m^C~u2@<4Fx;v3`IG{rQ zpxiMp76vtzx{>#At!TXZ_#iY)FpTYG%t*;}Cs#al0S3wOYE9VThVM+9i*yGns7YN3 z5l(VTB}t5Ty3Tmh`h2(|3r*$J{U)v^8+Rt4$a?j2Ag&x8+DrVSZq?(s8966nIQY9) z>cRW&%B+w{C5+HgzEg1#8>ydi6+&Dj9(aROv)g*#ia4jIYrvm4VNthzz?mNSy5&Kd zv(NTNCHdZO!_A7M4~qK!Ni-Y8`P4zRVJ%Qz>O-8F+(gZWrix||(!TE6*c5dOi&PJJg+-f1U3#<_DY-#aWkghvreK2p8#H=0hWt)t z_|7wo6i(ZD@-F)y1=ED7`(ec?AssJYgI>ma6=mJ09qEgX`^Z-sPatgVQzCP_bWt3q zg7uk$KSmqrkMM^Fh6KO)!9Sl$+20E1ruzG}|0Yo2sRo=Sm5f!D7D_?;0k<%GjgROR zqgT*HA4j9U2jGNRW+C_ED1pjjr+?lIO!McuL~1V_;LRIvMb-lym2nwbnH7jOUYSmr z_-PW)VoE0W)SsHQJNxmxo0a#f`U87*8SlP5csgZZQlut{Yb#yrhEP;Gr1}@2GDfi7 zC2FkvqxhZ%_OMoYz|-Q+t+XedjB~XKqZUqO!h>ZCUVNMAwj)Odm?bXMN>J|~?gL8=33VZ#D3&MaYkC!;YF&GA`Y0^+fEjJjYu3>aU(nAn6ii zaAFzIz86=}dmd}ybGTLK_kz(Q_~Y@~d?Pe9%&u4Q7k~kAMi8{oR96h;cT1NvaWTpPpDH237NOkJ|Pkd(*oeBg-@I7gN5Np0(Ue z-d|}3EgV%3YnTvEJ9HWqbg_Jj%&v^AsDwMtd$_rjGq)sX!{-@NFXZJR?{A`yg8f3> zy*!qJpVWc{9{m8)DX;XW9kX5fi@}%X`0Cf)H89H9r{3qA^zS$j5lSaiu@0v?G-9zQ zTqwYR(nzaSU&G@xOuzdi_J9YVn$lWvx$R3Q?yVQI2)_+a10}^Ytq)6hA)yQR$CDof zt&)SfLc&LWzb1df^hJNU^CIX9!PEb;S_$#3oa=lPz->0L$exSM!@0+}U2kuBg{b7W zcF0WZU;}5demo~6J*haxFa2Wa>de3!5Dr9Guv?{TlG8^-E^7)aReW&T`+OErlD1P; zXtE*)=E@XGd_*eCUv(KMMXAH_Q`FR+#+vWd+N@3M%v{rWNfksA;#K>@2h5(vGmJ0a z7@cJku^5_=n@uoOzA*59zEZ1}_CwvCcR1^|Cq)u=?^(SilTi_V-|RBkwI-f54w*)f zZ0e>n{_O0gO|_lFvzU=nVM1EufHkrYaU}g7rk}0hts9|tgnTXWA;YdE$sy;_$1;;Y z2Ingm(UHG#N8D^<`>%a4qTweVeuGn2`Hb{@V@R{yt~WP|RlmAAli1L|s9aR7DMu{J zc{if>EaE-7w_Y5SA1AmoGBC%%veNd4{VG(x*H6P3Uphz`ER@|aiN+NhslhlcjBxjO z0%{bVgy5?hn;;xM8Z6TrYrc}b)s@S>r@q96Y=s695t0AI&<}yK2Un{$@URmz7gN$p zzWG!w?emDo4#0Nl8J_+d4Hn1B)Y$)rasK}p*xdbnD)}_4hh)T~uin9PRQQRA|{!mS;#&IAy>v_lqnJga? zm_R0#i%#VL;S=1rT9in3+R}3xh6TZm3n$DAD^3)D&n>mcK{n)tCN-U8R@{O`#xM%{`f0_p!peEs(_EQ--9&J_-bg0!yEJ8H=rA&8(lXoD3@T;w)sQAR2H*Ydb4BhnJIm{2| z(4wm+B0(Xe{`PajHVZGcZ62&HWM-BE;d`z>6^>`ZpV_vwM^`@0PI_GC-B+#7BNq-0 zPD9#XtkwHc#^((a%izh>@^*L_bD!q-(f+?q)u7tzPV8bgVZ%Gf@0A%{GrtG;s+qIm7muqxHp9g^4Mkec zF6WlQQ1;!LEo9!om&b5)5DvV1v`Ck2lHpQcH&wJxJ9>xD6Qv%FPH=H;sce`~#?^IP zKAR0mpG#F7pE(r%%(DuILXV!u36}~RPbbE`q7o4Kq87G%8-l1y@~1Von*wnCL02F) zGvO@8_D;I*NGGt&T1%aibQ|U`R7JvpD&sY9mZ3bZl_CRB?zjkL|t) z4^bzV+lnZezmycK{!My2(q%n%|LpPj>HS`4s#UOsnT2VL);RW#P6$L$SLaAYHuJ!; zlc6xzns#9jn3M1=3aMFmw8*|n`VE%C9CxZf=BiFE2%VxsFd?{bNYG&>m1!#wjvX(R z1g`hJm@84JxGeDec~y~)Fsph|6<`63+l znY-)ug#n66nr_1=@cK5fi&K}NBl}CSir{3DkW?bHVZJ`o>YPCt+;JsEWlgiZ%C@=y zEfCL2KL~9&9+yI4#SftMBVFvU0UWDD?1c)+Rs_orr^kRKbk^!$MkdY$8YjE3!9X?~ zK^#A4B=dqZLf0!!-fF>`NwJ$nruU`(6%Dc;Gc$l!}iYvfcv zA*y}dSCl@JCN^w|BNEN~eL`7?uN%HMNb1V-2I0k>5q0v?#kFhh6L%ieJYU*qQZ?!I z3TnA^DMp)dlea^cH{O1g3OZB{5T}AjR~}m44)^O2vPG|If*v%UX1<`TX|MPB_BOA2H}r~W&cht_2k9hobZ}w2ue*8W)G7SIg};!_eb+?U(ukUgJa z+KFE-ne0M(SXit!A++wcpc-P|70gsgzmrQus-f4RRNwWDG~d$MVp_-PxSb-+az_R6 z09vh9^8C9QyVzB}-sX}sr2xs|k(*CaMaS8*d_?WY%#Y1FyZhUWYkDDU?7VD`U1;t_ z{Sz%Sdz5PE+qIhRByZRTiF!|;kS0y6gC*!E$hMQosgNtb;G^k?k%ec~Nc>IIUqTzw_;#7^G|2?7hZ5tu>apcF=K%U~bYEwRTGF zm0mT7tX(@S69d*vsZj)G1bgB*W$2AHLBh*&Z^HsciZAjR30S#;Uu(ZlWN)7SIlNX| zMYQAL^)}cX>zf+gq{<}hwz9YHD#Z!yscl&+Z1?<)WVPutX@#2g*~RIFeN3QEvV(|N z<%H@~H(5KHz~g>wbYF;XR^^;kaJu04k_oR6OM0l!@A!q7-9g7jY?gH&Zf6T4CPYgM=Al~0NR7Um zY8mHJqwF1!Z5A#0>3%j=V}*qMbdkp}P`pp6mRaTDMo6PTXe5Gq>q(SnA2l{%$6l8u zD>1u-Z@HJ+>o$aCf!@qQ27dnS6c5u{kDRY5dA?6BwC)+QxNNdasHKU>dN8iv*|;sU zc@}|?tzl=`x9;A0)ZOoKBt_J*%RI9)l%+2U4WzZCu@$Jo-n!@Dn4~VCgH9+_8UaW~ z^(>Pn<-4W`zRDl1vE+Y;LH!}EjG#b$ZDiK}=+(ZSM~BWH6qH)eVhEc|^OXT@rB)r= z>f#a_47l3R5;0|;vpFf+(!sD}+LR*SI{?mE#`C%?H%gw5u+Kk3?R3>WUCy^v#c{MY zGjiag*+Xfbm#>Jp*MB~oWsQ)Y$rAGoqkSe94!YBvjkhDxQr~KpFq-JvmNIp=W<2W+ zV{>k@U58yo!u&Qy{e`j#>y%@CwXYlB&m3LJf4sbK4y3OW_>w#JKT9m|$nck)E-*!YXj;?IR242e;IfdZ>$?oHWyNbM|Yb z2`fo=7dIR*T9yV5ylu{e6tH9S?romr!rKvE)tY1riw1#lUf#1j2m2Q`984Rfc`^LI z0FPeZ*dWiJJYQ^WpDix_ux##E)rygynFr(UhlROx$v8vf1azia$kbk$aNzUE@U_e= zSK)kJDq)vwZ+qT%zNnpqP?b0MHlcb>sH#-Q$km!i&SJ1*Gi*Nd?%o9@%zkQm=1^Bn z6qKQuPzUgJjY3dK!yYa_AD-nK9z1C<>-iq`h6wTzFx;gvOSm}$g`tH1UqM6|FG7 zPg|SQ;OdoAlf%!=i5!=iFi@ETvV<<>;vxj`Tj}~(?9Fn)yvJ#8v=nO1*OAb~+Z3*& zSSEy~NWYV4Qd-aNmo@AB0$409x*`_Idrv4U56gPxT0)g-H4kWt>($CUvMbbnxk|CU z>Pe|~>*LcZx?x*k67Bbs@Ijr{j=LKd4wVH2y@1xk!`)H!vJixRl#-jKf53Y3`6&1c zKg)XC6Mi*e`K9+ZjtZ^kqdbx)H!PmCjO(8ve57w3U)^*|O*ho+9|(0qZ}{u0s7@|4 z^7=u_zNWEd=zff01!dxh`Wspm2@e{YR~^lRm;?2PoO1YkpKKE%gOnEdMu?UldLK&W zs`H#V3npI;+zOb-T(Z?0IyjK(eXwopb=+=2g>ypnCMt2h^Ku3Uk!Tg360Mg;vv!?( zzoZkerj2-O;>#J!V~a6Qg!IAg6$)mQ9O#~sG$*&~beD6Nd7}%%RokiTk*ES-AKksq zEb%*I$ih|RG7|8PXru}!zs_n{ah0Q6481!?Hoi(oYgZ_UVdqsp@^)mq9d+Uqd2h6F z*Vq=8dxm4VMU-M0=(TN*kKN9S?4td_O8-ZXeIwH}{=TuF5*3lfe&4!lID<~ah*0*W zMnx(HNkaIlI*PUX4Jx@>1%b%Tz~cmk}0`eh6I|b32`cuvLrDhJ&SEY7hS&m zb07J&1LPFpiLTi9DsKyEQCigj0w=O773E#FHShpggg4@q}w!|c?oFtF5ijT{9j zEB2KCJ7GXbVn&IdHLZ#5Ea|!rf!v2X6F|2hF2Jp0HO8Gat(bz>&Te%D8hgV`UlJQ5 zy2`hfTJQ3;`z0IiCEnY;euxY^;i4zY@n(hfX^z^volt1i5w-(c!&2WB(~<_27jcb` z?S|c@3ljoJAy5;s#h#?*Z!D~gN5&0*^B$rYxAe+>`ABY4lcGZ^*__l(K(w(XtdaGt z<-QfIhsh*KWu?d+eCkpu%7Pf#=2~-+4$I?|NTi{>5Xe1;Y_qgs08F@&Zvpl{atD&Q zWG|d8qPBLsSep^3;mxI*5CK~>G=Q$QWDA(Mr`c6d0c0jeYE^pnN1LFi1rFFOS0WRZ z(nA)vfUbL&kC_j_n-%R{8M~PF6YUyvdaka50=DQkNL5vB*Ak!PztJI8@YLBn{*j5Z zW=E<0$t>|n^43CpE~dY*mmKxX){WrBTA2yp>Pb-Wi6(lq5i7H5;pQ>Batb(z+@ z5{mlDzxZ+L1bK57FFC38YbY7^pmdR^pbMLjOGTw6vX3Rz%Ap43V|kVcLUZ5tg9R`6 z!afD2FZSX>aRlW2-p(VF!ObK6TnG6C-9GCM`L{Y>6I zR_f67&Yp0c_C9lJGwUE3cG$Z~AfGqB^P}fB-$;9SvrQWQzW|W|ZvOhNM*jdOqThqYnoCzBD`SKP>2q#(Z&p!{Ze3$y3}r-(@EFp2PLhAh zq-ChvRSd0|>1DScQM$^urRPTG_)Td)TKM{xil`$2x?g+l-kr%qS%wMZmO%SC#jXO} zg_VUq117RNiqByCY}Z|uBV4_9i6 z#?)o8JdKh{j}K8Uvuropfb8I|8G$vOXH3S^u&~@--mtEPY@J$?OEXlY2X6k#EKd=d zmLlld!gu(<{{WQeom9(%xz&3{%miiakhHSq?H1gArI%Lte9ow&idTRf;d5>Z;a;0B z#SETQcOpsUM{*!8GQfTasLr_M7B5Q-vP)SpiSlp+JoqcKR{&;`Mrg&z+ @for (listItem of searchedList(); track listItem.id) {
  • - @if (listType() === "projects" || listType() === "members") { + @if (listType() === "projects") { + + } @else if (listType() === "members") { { let component: ProgramListComponent; let fixture: ComponentFixture; + const projects = signal([]); beforeEach(async () => { + projects.set([]); const programDetailListInfoServiceSpy = { initializeSearchForm: vi.fn(), initializationListData: vi.fn(), @@ -30,7 +39,7 @@ describe("ProgramListComponent", () => { const programDetailListUIInfoServiceSpy = { searchForm: fb.group({ search: [""] }), listType: signal("projects"), - searchedList: signal([]), + searchedList: projects, profileProjSubsIds: signal([]), routerLink: vi.fn(), applySetAvailableFilters: vi.fn(), @@ -70,7 +79,18 @@ describe("ProgramListComponent", () => { await TestBed.configureTestingModule({ imports: [ProgramListComponent], - providers: [provideRouter([])], + providers: [ + provideRouter([]), + { provide: IndustryRepositoryPort, useValue: { getOne: () => ({ name: "Образование" }) } }, + { + provide: AddProjectSubscriptionUseCase, + useValue: { execute: vi.fn(() => of(ok(undefined))) }, + }, + { + provide: DeleteProjectSubscriptionUseCase, + useValue: { execute: vi.fn(() => of(ok(undefined))) }, + }, + ], }) .overrideComponent(ProgramListComponent, { remove: { @@ -112,4 +132,26 @@ describe("ProgramListComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + it("проект программы открывается одной ссылкой без вложенной кнопки подписки", () => { + projects.set([projectCardFixture()]); + fixture.detectChanges(); + const card = fixture.nativeElement.querySelector("app-info-card") as HTMLElement; + const link = card.querySelector(".card__project-link")!; + expect(card.closest("a")).toBeNull(); + expect(card.querySelector("a a, a button, a [tabindex]")).toBeNull(); + expect(link.getAttribute("href")).toBe("/office/projects/101"); + expect(link.querySelector(".card__project-action")?.textContent?.trim()).toBe("Открыть"); + expect(card.querySelector(".card__subscription-action")?.closest("a")).toBeNull(); + const router = TestBed.inject(Router); + const navigate = vi.spyOn(router, "navigateByUrl").mockResolvedValue(true); + try { + link.querySelector(".card__project-action")!.click(); + expect(navigate).toHaveBeenCalledOnce(); + expect(router.serializeUrl(navigate.mock.calls[0][0] as UrlTree)).toBe( + "/office/projects/101", + ); + } finally { + navigate.mockRestore(); + } + }); }); diff --git a/projects/social_platform/src/app/ui/pages/projects/bar-new/bar.component.scss b/projects/social_platform/src/app/ui/pages/projects/bar-new/bar.component.scss index fb4ab018f..cc0d02ee1 100644 --- a/projects/social_platform/src/app/ui/pages/projects/bar-new/bar.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/bar-new/bar.component.scss @@ -51,4 +51,22 @@ } } } + + @media (max-width: 999px) { + // Перенос вкладок не даёт навигации растягивать сетку фиксированных карточек. + &__tabs { + flex-wrap: wrap; + gap: 4px; + } + + &__link { + flex: 1 1 140px; + min-width: 0; + } + + &__item { + justify-content: center; + padding: 5px 8px; + } + } } diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.html b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.html index 1e4d077d4..f0dc49b88 100644 --- a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.html +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.html @@ -7,7 +7,8 @@ [title]="dashboardItem.title" [iconName]="dashboardItem.iconName" [arrayItems]="dashboardItem.arrayItems" - [profileProjSubsIds]="dashboardItem.sectionName === 'all' ? profileProjSubsIds() : []" + [profileProjSubsIds]="profileProjSubsIds()" + [loggedUserId]="profile()?.id" (addProjectClick)="onAddProject()" > } diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts new file mode 100644 index 000000000..5e8882dbf --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts @@ -0,0 +1,162 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideRouter, Router, RouterLink } from "@angular/router"; +import { of } from "rxjs"; +import { DashboardProjectsComponent } from "./dashboard.component"; +import { DashboardItemComponent } from "./dashboardItem/dashboardItem.component"; +import { InfoCardComponent } from "@ui/widgets/info-card/info-card.component"; +import { + myProjectCardFixtures, + publicProjectCardFixtures, +} from "@ui/widgets/info-card/info-card.fixture"; +import { ProjectsDashboardInfoService } from "@api/project/facades/dashboard/projects-dashboard-info.service"; +import { ProjectsDashboardUIInfoService } from "@api/project/facades/dashboard/ui/projects-dashboard-ui-info.service"; +import { ProgramDetailListUIInfoService } from "@api/program/facades/detail/ui/program-detail-list-ui-info.service"; +import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { AddProjectSubscriptionUseCase } from "@api/project/use-cases/add-project-subscription.use-case"; +import { DeleteProjectSubscriptionUseCase } from "@api/project/use-cases/delete-project-subscription.use-case"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; +import { ok } from "@domain/shared/result.type"; +import { DashboardItem } from "@utils/dashboardItemBuilder"; + +/** Дашборд использует те же карточки и контексты, что и полный список. */ +describe("ProjectsDashboard: контексты карточек", () => { + const items = signal([]); + const dashboard = { initializationDashboardItems: vi.fn(), addProject: vi.fn() }; + beforeEach(async () => { + vi.clearAllMocks(); + items.set([ + { + sectionName: "my", + title: "мои проекты", + iconName: "main", + arrayItems: myProjectCardFixtures.map(x => x.project), + }, + { + sectionName: "subscriptions", + title: "мои подписки", + iconName: "favourities", + arrayItems: publicProjectCardFixtures, + }, + { + sectionName: "all", + title: "витрина проектов", + iconName: "folders", + arrayItems: publicProjectCardFixtures, + }, + ]); + await TestBed.configureTestingModule({ + imports: [DashboardProjectsComponent, DashboardItemComponent], + providers: [ + { provide: ProfileInfoService, useValue: { profile: signal({ id: 7 }) } }, + provideRouter([]), + { + provide: IndustryRepositoryPort, + useValue: { getOne: (id: number) => ({ id, name: "Отрасль " + id }) }, + }, + { + provide: AddProjectSubscriptionUseCase, + useValue: { execute: vi.fn(() => of(ok(undefined))) }, + }, + { + provide: DeleteProjectSubscriptionUseCase, + useValue: { execute: vi.fn(() => of(ok(undefined))) }, + }, + ], + }) + .overrideComponent(DashboardProjectsComponent, { + set: { + providers: [ + { provide: ProjectsDashboardInfoService, useValue: dashboard }, + { provide: ProjectsDashboardUIInfoService, useValue: { dashboardItems: items } }, + { + provide: ProgramDetailListUIInfoService, + useValue: { profileProjSubsIds: signal(publicProjectCardFixtures.map(x => x.id)) }, + }, + ], + }, + }) + .compileComponents(); + }); + + it("передаёт my/subs/base, ID и подписки; показывает нужный контекст каждого раздела", () => { + const fixture = TestBed.createComponent(DashboardProjectsComponent); + fixture.detectChanges(); + const sections = fixture.debugElement.queryAll(By.directive(DashboardItemComponent)); + expect(sections).toHaveLength(3); + for (const [index, section] of sections.entries()) { + const appearance = ["my", "subs", "base"][index]; + const cards = section.queryAll(By.directive(InfoCardComponent)); + expect(cards).toHaveLength(index === 0 ? myProjectCardFixtures.length : 4); + cards.forEach((el, i) => { + const c = el.componentInstance as InfoCardComponent; + expect(c.appereance()).toBe(appearance); + expect(c.profileId()).toBe(c.info().id); + expect(c.loggedUserId()).toBe(7); + expect(el.nativeElement.closest("a")).toBeNull(); + expect(el.nativeElement.querySelector("a a, a button, a [tabindex]")).toBeNull(); + expect(el.nativeElement.querySelector(".card__project-action").textContent.trim()).toBe( + "Открыть", + ); + const actionLink = el.query(By.directive(RouterLink)).injector.get(RouterLink); + expect(TestBed.inject(Router).serializeUrl(actionLink.urlTree!)).toBe( + "/office/projects/" + c.info().id, + ); + expect(actionLink.urlTree!.queryParams).toEqual({}); + if (index === 0) { + expect(el.nativeElement.querySelector(".card__status").textContent.trim()).toBe( + myProjectCardFixtures[i].label, + ); + expect(el.nativeElement.querySelector(".card__context--industry")).toBeNull(); + expect(el.nativeElement.querySelector(".card__role").textContent).toBe( + myProjectCardFixtures[i].role, + ); + expect(el.nativeElement.querySelector(".card__access-label").textContent).toBe( + myProjectCardFixtures[i].access, + ); + } else { + expect(c.isSubscribed).toBe(true); + expect(el.nativeElement.querySelector(".card__status")).toBeNull(); + expect(!!el.nativeElement.querySelector(".card__context--industry")).toBe(i !== 3); + expect( + el.nativeElement.querySelector(".card__project-action-label").textContent.trim(), + ).toBe("Открыть"); + } + }); + const sectionLink = section.query(By.css(".dashboard__link")).injector.get(RouterLink); + expect(TestBed.inject(Router).serializeUrl(sectionLink.urlTree!)).toBe( + "/office/projects/" + ["my", "subscriptions", "all"][index], + ); + } + expect(dashboard.initializationDashboardItems).toHaveBeenCalledOnce(); + }); + + it("определяет контекст по разделу, даже если декоративная иконка другая", () => { + const fixture = TestBed.createComponent(DashboardItemComponent); + fixture.componentRef.setInput("title", "Раздел"); + fixture.componentRef.setInput("iconName", "folders"); + fixture.componentRef.setInput("sectionName", "my"); + fixture.componentRef.setInput("arrayItems", [myProjectCardFixtures[0].project]); + fixture.detectChanges(); + const card = () => + fixture.debugElement.query(By.directive(InfoCardComponent)) + .componentInstance as InfoCardComponent; + expect(card().appereance()).toBe("my"); + fixture.componentRef.setInput("sectionName", "subscriptions"); + fixture.detectChanges(); + expect(card().appereance()).toBe("subs"); + }); + + it("пустой раздел сохраняет действие создания", () => { + items.set([{ sectionName: "my", title: "мои проекты", iconName: "main", arrayItems: [] }]); + const fixture = TestBed.createComponent(DashboardProjectsComponent); + fixture.detectChanges(); + const card = fixture.debugElement.query(By.directive(InfoCardComponent)); + expect(card.componentInstance.appereance()).toBe("empty"); + card.nativeElement.querySelector(".card__body").click(); + expect(dashboard.addProject).toHaveBeenCalledOnce(); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.ts b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.ts index aa6f8960c..d8e5bee09 100644 --- a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.ts +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.ts @@ -6,6 +6,7 @@ import { DashboardItemComponent } from "./dashboardItem/dashboardItem.component" import { ProjectsDashboardInfoService } from "@api/project/facades/dashboard/projects-dashboard-info.service"; import { ProjectsDashboardUIInfoService } from "@api/project/facades/dashboard/ui/projects-dashboard-ui-info.service"; import { ProgramDetailListUIInfoService } from "@api/program/facades/detail/ui/program-detail-list-ui-info.service"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; /** Дашборд проектов пользователя. */ @Component({ @@ -26,6 +27,7 @@ export class DashboardProjectsComponent implements OnInit { private readonly programDetailListUIInfoService = inject(ProgramDetailListUIInfoService); protected readonly dashboardItems = this.projectsDashboardUIInfoService.dashboardItems; + protected readonly profile = inject(ProfileInfoService).profile; protected readonly profileProjSubsIds = this.programDetailListUIInfoService.profileProjSubsIds; ngOnInit(): void { diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.html b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.html index 276f76950..cb2419cdc 100644 --- a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.html +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.html @@ -9,13 +9,16 @@ @if (arrayItems().length) { } @else { diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss index b2b495b4e..1ffb5ee0c 100644 --- a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss @@ -18,14 +18,12 @@ &__list { display: grid; flex-grow: 1; - grid-template-columns: 1fr; + grid-template-columns: repeat(auto-fill, 156px); + row-gap: 50px; column-gap: 20px; align-items: flex-start; + justify-content: center; margin-top: 50px; - - @include responsive.apply-desktop { - grid-template-columns: repeat(4, 2fr); - } } &__link { diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.ts b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.ts index 4d4505499..f6c92e586 100644 --- a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.ts +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.ts @@ -1,16 +1,7 @@ /** @format */ import { CommonModule } from "@angular/common"; -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - input, - Input, - OnInit, - output, - Output, -} from "@angular/core"; +import { ChangeDetectionStrategy, Component, computed, input, output } from "@angular/core"; import { IconComponent } from "@uilib"; import { RouterLink } from "@angular/router"; import { InfoCardComponent } from "@ui/widgets/info-card/info-card.component"; @@ -25,30 +16,19 @@ import { AppRoutes } from "@api/paths/app-routes"; imports: [CommonModule, IconComponent, RouterLink, InfoCardComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DashboardItemComponent implements OnInit { +export class DashboardItemComponent { readonly title = input.required(); readonly arrayItems = input.required(); readonly iconName = input.required(); readonly sectionName = input.required(); readonly profileProjSubsIds = input(); + readonly loggedUserId = input(); readonly addProjectClick = output(); - appereance: "base" | "subs" | "my" = "base"; + /** Раздел задаёт контекст независимо от декоративной иконки и обновляется вместе с input. */ + readonly appereance = computed(() => + this.sectionName() === "my" ? "my" : this.sectionName() === "subscriptions" ? "subs" : "base", + ); protected readonly AppRoutes = AppRoutes; - - ngOnInit(): void { - switch (this.iconName()) { - case "favourities": - this.appereance = "subs"; - break; - - case "main": - this.appereance = "my"; - break; - - default: - break; - } - } } diff --git a/projects/social_platform/src/app/ui/pages/projects/list/list.component.html b/projects/social_platform/src/app/ui/pages/projects/list/list.component.html index c50769bf4..2f1067d1d 100644 --- a/projects/social_platform/src/app/ui/pages/projects/list/list.component.html +++ b/projects/social_platform/src/app/ui/pages/projects/list/list.component.html @@ -8,19 +8,29 @@ } diff --git a/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss b/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss index 85d685865..0301fbf68 100644 --- a/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss @@ -29,14 +29,16 @@ &__list { display: grid; flex-grow: 1; - grid-template-columns: 1fr; + + // Число колонок зависит от доступного места рядом с сайдбаром, а не от ширины окна. + grid-template-columns: repeat(auto-fill, 156px); row-gap: 50px; column-gap: 20px; align-items: flex-start; - - @include responsive.apply-desktop { - grid-template-columns: repeat(4, 2fr); - } + justify-content: center; + width: 100%; + min-width: 0; + padding-top: 35px; } } diff --git a/projects/social_platform/src/app/ui/pages/projects/list/list.component.spec.ts b/projects/social_platform/src/app/ui/pages/projects/list/list.component.spec.ts index b9df54f06..f81eff230 100644 --- a/projects/social_platform/src/app/ui/pages/projects/list/list.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/projects/list/list.component.spec.ts @@ -1,117 +1,173 @@ /** @format */ -import { ComponentFixture, TestBed } from "@angular/core/testing"; - -import { ProjectsListComponent } from "./list.component"; -import { of } from "rxjs"; -import { AuthRepository } from "@infrastructure/repository/auth/auth.repository"; -import { HttpClientTestingModule } from "@angular/common/http/testing"; -import { provideRouter } from "@angular/router"; -import { AuthRepositoryPort } from "@domain/auth/ports/auth.repository.port"; import { signal } from "@angular/core"; -import { initial } from "@domain/shared/async-state"; +import { TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideRouter, Router, UrlTree } from "@angular/router"; +import { RouterTestingHarness } from "@angular/router/testing"; +import { of } from "rxjs"; +import { ProjectsListComponent } from "./list.component"; +import { InfoCardComponent } from "@ui/widgets/info-card/info-card.component"; +import { myProjectCardFixtures, projectCardFixture } from "@ui/widgets/info-card/info-card.fixture"; import { ProjectsListInfoService } from "@api/project/facades/list/projects-list-info.service"; import { ProjectsInfoService } from "@api/project/facades/projects-info.service"; -import { ProgramDetailListInfoService } from "@api/program/facades/detail/program-detail-list-info.service"; import { ProgramDetailListUIInfoService } from "@api/program/facades/detail/ui/program-detail-list-ui-info.service"; import { OfficeInfoService } from "@api/office/facades/office-info.service"; -import { OfficeUIInfoService } from "@api/office/facades/ui/office-ui-info.service"; import { SwipeService } from "@api/swipe/swipe.service"; +import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { AddProjectSubscriptionUseCase } from "@api/project/use-cases/add-project-subscription.use-case"; +import { DeleteProjectSubscriptionUseCase } from "@api/project/use-cases/delete-project-subscription.use-case"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; +import { ok } from "@domain/shared/result.type"; -describe("ProjectsListComponent", () => { - let component: ProjectsListComponent; - let fixture: ComponentFixture; +/** Проверяем реальную разметку списка и идентичность карточек при обновлении массива. */ +describe("ProjectsListComponent: контекст и track", () => { + const first = projectCardFixture({ id: 101, inviteId: 0 }); + const second = projectCardFixture({ id: 102, inviteId: 0 }); + const projects = signal([first, second]); + const office = { onAcceptInvite: vi.fn(), onRejectInvite: vi.fn() }; + let harness: RouterTestingHarness; beforeEach(async () => { - const authSpy = { - profile: of({}), - }; - const authPortSpy = { - login: of({} as any), - logout: of(undefined), - fetchProfile: of({} as any), - fetchUserRoles: of([]), - fetchChangeableRoles: of([]), - fetchLeaderProjects: of({} as any), - }; - - const projectsListInfoServiceSpy = { - initializationProjectsList: vi.fn(), - initScroll: vi.fn(), - destroy: vi.fn(), - projects: signal([]), - }; - - const projectsInfoServiceSpy = { - isAll: signal(false), - isMy: signal(false), - isSubs: signal(false), - isInvites: signal(false), - }; - - const programDetailListUIInfoServiceSpy = { - profileProjSubsIds: signal([]), - }; - - const officeInfoServiceSpy = { onAcceptInvite: vi.fn(), onRejectInvite: vi.fn() }; - - const officeUIInfoServiceSpy = {}; - - const swipeServiceSpy = { - isFilterOpen: signal(false), - onSwipeStart: vi.fn(), - onSwipeMove: vi.fn(), - onSwipeEnd: vi.fn(), - closeFilter: vi.fn(), - }; - - const programDetailListInfoServiceSpy = { init: vi.fn(), destroy: vi.fn() }; - + vi.clearAllMocks(); + projects.set([first, second]); + const routeIs = (section: string) => TestBed.inject(Router).url.endsWith("/" + section); await TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, ProjectsListComponent], + imports: [ProjectsListComponent], providers: [ - provideRouter([]), - { provide: AuthRepository, useValue: authSpy }, - { provide: AuthRepositoryPort, useValue: authPortSpy }, + { provide: ProfileInfoService, useValue: { profile: signal({ id: 7 }) } }, + provideRouter([{ path: "office/projects/:section", component: ProjectsListComponent }]), + { + provide: IndustryRepositoryPort, + useValue: { getOne: () => ({ id: 1, name: "Образование" }) }, + }, + { + provide: AddProjectSubscriptionUseCase, + useValue: { execute: vi.fn(() => of(ok(undefined))) }, + }, + { + provide: DeleteProjectSubscriptionUseCase, + useValue: { execute: vi.fn(() => of(ok(undefined))) }, + }, ], }) .overrideComponent(ProjectsListComponent, { - remove: { - providers: [ - ProjectsListInfoService, - ProjectsInfoService, - ProgramDetailListInfoService, - ProgramDetailListUIInfoService, - OfficeInfoService, - OfficeUIInfoService, - SwipeService, - ], - }, - add: { + set: { providers: [ - { provide: ProjectsListInfoService, useValue: projectsListInfoServiceSpy }, - { provide: ProjectsInfoService, useValue: projectsInfoServiceSpy }, - { provide: ProgramDetailListInfoService, useValue: programDetailListInfoServiceSpy }, + { + provide: ProjectsListInfoService, + useValue: { projects, initializationProjectsList: vi.fn(), initScroll: vi.fn() }, + }, + { + provide: ProjectsInfoService, + useValue: { + isMy: () => routeIs("my"), + isSubs: () => routeIs("subscriptions"), + isAll: () => routeIs("all"), + isInvites: () => routeIs("invites"), + }, + }, { provide: ProgramDetailListUIInfoService, - useValue: programDetailListUIInfoServiceSpy, + useValue: { profileProjSubsIds: signal([101]) }, }, - { provide: OfficeInfoService, useValue: officeInfoServiceSpy }, - { provide: OfficeUIInfoService, useValue: officeUIInfoServiceSpy }, - { provide: SwipeService, useValue: swipeServiceSpy }, + { provide: OfficeInfoService, useValue: office }, + { provide: SwipeService, useValue: { isFilterOpen: signal(false) } }, ], }, }) .compileComponents(); + harness = await RouterTestingHarness.create(); + }); + + const cards = () => harness.routeDebugElement!.queryAll(By.directive(InfoCardComponent)); + + it.each(["my", "subscriptions", "all"])( + "/%s: CTA выполняет один переход в detail", + async route => { + await harness.navigateByUrl("/office/projects/" + route, ProjectsListComponent); + harness.detectChanges(); + const router = TestBed.inject(Router); + const navigate = vi.spyOn(router, "navigateByUrl").mockResolvedValue(true); + try { + cards()[0].nativeElement.querySelector(".card__project-action").click(); + expect(navigate).toHaveBeenCalledOnce(); + expect(router.serializeUrl(navigate.mock.calls[0][0] as UrlTree)).toBe( + "/office/projects/101", + ); + } finally { + navigate.mockRestore(); + } + }, + ); + + it.each([ + ["my", "my"], + ["subscriptions", "subs"], + ["all", "base"], + ] as const)("/%s явно передаёт %s и сохраняет ID действия", async (route, appearance) => { + await harness.navigateByUrl("/office/projects/" + route, ProjectsListComponent); + harness.detectChanges(); + expect(cards()).toHaveLength(2); + for (const [index, element] of cards().entries()) { + const card = element.componentInstance as InfoCardComponent; + expect(card.appereance()).toBe(appearance); + expect(card.type()).toBe("projects"); + expect(card.profileId()).toBe(projects()[index].id); + expect(card.loggedUserId()).toBe(7); + expect(element.nativeElement.closest("a")).toBeNull(); + expect(element.nativeElement.querySelector("a a, a button, a [tabindex]")).toBeNull(); + expect(card.showSubscriptionAction()).toBe(route !== "my"); + } + }); + + it("полный список сохраняет независимые lifecycle/роль/доступ во всех восьми сочетаниях", async () => { + projects.set(myProjectCardFixtures.map(item => item.project)); + await harness.navigateByUrl("/office/projects/my", ProjectsListComponent); + harness.detectChanges(); + expect(cards()).toHaveLength(8); + cards().forEach((element, index) => { + const item = myProjectCardFixtures[index]; + const card = element.nativeElement as HTMLElement; + expect(card.querySelector(".card__status")?.textContent?.trim()).toBe(item.label); + expect(card.querySelector(".card__role")?.textContent).toBe(item.role); + expect(card.querySelector(".card__access-label")?.textContent).toBe(item.access); + expect(card.querySelector(".card__project-action")?.textContent?.trim()).toBe("Открыть"); + expect(card.querySelector(".card__project-link")?.getAttribute("href")).toBe( + "/office/projects/" + item.project.id, + ); + }); }); - beforeEach(() => { - fixture = TestBed.createComponent(ProjectsListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + it("обычные проекты с одинаковым inviteId сохраняют DOM по project.id", async () => { + await harness.navigateByUrl("/office/projects/my", ProjectsListComponent); + harness.detectChanges(); + const before = cards().map(c => c.nativeElement); + projects.set([{ ...second, name: "Обновлённый второй" }, { ...first }]); + harness.detectChanges(); + expect(cards()[0].nativeElement).toBe(before[1]); + expect(cards()[1].nativeElement).toBe(before[0]); + expect(cards()[0].nativeElement.textContent).toContain("Обновлённый второй"); }); - it("should create", () => { - expect(component).toBeTruthy(); + it("приглашения одного проекта используют inviteId и прежние действия", async () => { + const invite1 = { ...first, inviteId: 51 }; + const invite2 = { ...first, inviteId: 52 }; + projects.set([invite1, invite2]); + await harness.navigateByUrl("/office/projects/invites", ProjectsListComponent); + harness.detectChanges(); + const before = cards().map(c => c.nativeElement); + expect(cards().every(c => c.componentInstance.type() === "invite")).toBe(true); + expect(cards().every(c => !c.componentInstance.showSubscriptionAction())).toBe(true); + projects.set([{ ...invite2 }, { ...invite1 }]); + harness.detectChanges(); + expect(cards()[0].nativeElement).toBe(before[1]); + expect(cards()[1].nativeElement).toBe(before[0]); + const buttons = cards()[0].nativeElement.querySelectorAll(".card__invite-actions button"); + buttons[0].click(); + buttons[1].click(); + expect(office.onAcceptInvite).toHaveBeenCalledWith(52); + expect(office.onRejectInvite).toHaveBeenCalledWith(52); + expect(TestBed.inject(Router).url).toBe("/office/projects/invites"); }); }); diff --git a/projects/social_platform/src/app/ui/pages/projects/list/list.component.ts b/projects/social_platform/src/app/ui/pages/projects/list/list.component.ts index fed041abf..ea2aa9be4 100644 --- a/projects/social_platform/src/app/ui/pages/projects/list/list.component.ts +++ b/projects/social_platform/src/app/ui/pages/projects/list/list.component.ts @@ -10,6 +10,7 @@ import { viewChild, } from "@angular/core"; import { RouterLink } from "@angular/router"; +import { NgTemplateOutlet } from "@angular/common"; import { IconComponent } from "@ui/primitives"; import { InfoCardComponent } from "@ui/widgets/info-card/info-card.component"; import { ProjectsListInfoService } from "@api/project/facades/list/projects-list-info.service"; @@ -20,13 +21,14 @@ import { ProgramDetailListUIInfoService } from "@api/program/facades/detail/ui/p import { ProgramDetailListInfoService } from "@api/program/facades/detail/program-detail-list-info.service"; import { OfficeUIInfoService } from "@api/office/facades/ui/office-ui-info.service"; import { AppRoutes } from "@api/paths/app-routes"; +import { ProfileInfoService } from "@api/profile/facades/profile-info.service"; /** Отображает список проектов с поиском, фильтрацией и бесконечной прокруткой. */ @Component({ selector: "app-list", templateUrl: "./list.component.html", styleUrl: "./list.component.scss", - imports: [IconComponent, RouterLink, InfoCardComponent], + imports: [IconComponent, RouterLink, InfoCardComponent, NgTemplateOutlet], providers: [ ProjectsListInfoService, ProjectsInfoService, @@ -51,6 +53,7 @@ export class ProjectsListComponent implements OnInit, AfterViewInit { protected readonly isFilterOpen = this.swipeService.isFilterOpen; protected readonly projects = this.projectsListInfoService.projects; + protected readonly profile = inject(ProfileInfoService).profile; protected readonly profileProjSubsIds = this.programDetailListUIInfoService.profileProjSubsIds; protected readonly isAll = this.projectsInfoService.isAll; diff --git a/projects/social_platform/src/app/ui/pages/projects/projects.component.scss b/projects/social_platform/src/app/ui/pages/projects/projects.component.scss index d60f60ce0..be2a2ea6e 100644 --- a/projects/social_platform/src/app/ui/pages/projects/projects.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/projects.component.scss @@ -5,10 +5,15 @@ &__info { display: flex; + flex-direction: column; gap: 20px; justify-content: space-between; padding-bottom: 100px; + @include responsive.apply-desktop { + flex-direction: row; + } + form { width: 100%; } @@ -20,6 +25,7 @@ flex-grow: 1; gap: 20px; width: 100%; + min-width: 0; } &__create { diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card-list-contract.spec.ts b/projects/social_platform/src/app/ui/widgets/info-card/info-card-list-contract.spec.ts new file mode 100644 index 000000000..dfe6d84e8 --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card-list-contract.spec.ts @@ -0,0 +1,136 @@ +/** @format */ + +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideRouter, Router, RouterLink } from "@angular/router"; +import { firstValueFrom, of } from "rxjs"; +import { API_URL, CamelcaseInterceptor } from "@corelib"; +import { GetMyProjectsUseCase } from "@api/project/use-cases/get-my-projects.use-case"; +import { AddProjectSubscriptionUseCase } from "@api/project/use-cases/add-project-subscription.use-case"; +import { DeleteProjectSubscriptionUseCase } from "@api/project/use-cases/delete-project-subscription.use-case"; +import { AppRoutes } from "@api/paths/app-routes"; +import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { ProjectRepositoryPort } from "@domain/project/ports/project.repository.port"; +import { Project } from "@domain/project/project.model"; +import { ok } from "@domain/shared/result.type"; +import { ProjectRepository } from "@infrastructure/repository/project/project.repository"; +import { InfoCardComponent } from "./info-card.component"; + +describe("Мои проекты: HTTP list → преобразование контракта → карточка", () => { + let fixture: ComponentFixture; + let http: HttpTestingController; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InfoCardComponent], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: API_URL, useValue: "/api" }, + { provide: ProjectRepositoryPort, useClass: ProjectRepository }, + { provide: IndustryRepositoryPort, useValue: { getOne: () => undefined } }, + { provide: AddProjectSubscriptionUseCase, useValue: { execute: () => of(ok(undefined)) } }, + { + provide: DeleteProjectSubscriptionUseCase, + useValue: { execute: () => of(ok(undefined)) }, + }, + provideRouter([]), + ], + }).compileComponents(); + http = TestBed.inject(HttpTestingController); + fixture = TestBed.createComponent(InfoCardComponent); + fixture.componentRef.setInput("appereance", "my"); + fixture.componentRef.setInput("loggedUserId", 7); + }); + + afterEach(() => http.verify()); + + /** + * Передаёт настоящий сокращённый snake_case payload через HTTP/interceptor, + * адаптер, репозиторий и use case. Готовая camelCase-фикстура скрыла бы дефект. + */ + async function loadProject(submitted: boolean | undefined, draft = false): Promise { + const resultPromise = firstValueFrom(TestBed.inject(GetMyProjectsUseCase).execute()); + const request = http.expectOne("/api/auth/users/projects/"); + expect(request.request.method).toBe("GET"); + request.flush({ + count: 1, + next: null, + previous: null, + results: [ + { + id: 101, + name: "Проект программы", + leader: 7, + short_description: "Описание проекта", + image_address: "/assets/images/projects/shared/idea.svg", + industry: 1, + views_count: 0, + draft, + is_company: false, + partner_program: { + id: 12, + name: "Программа", + ...(submitted === undefined + ? {} + : { program_link_id: 120, program_id: 12, is_submitted: submitted }), + }, + }, + ], + }); + const result = await resultPromise; + expect(result.ok).toBe(true); + if (!result.ok) throw new Error("Список проектов не загружен"); + const project = result.value.results[0]; + expect(project).toBeInstanceOf(Project); + expect(project.shortDescription).toBe("Описание проекта"); + expect(project.partnerProgram?.isSubmitted).toBe(submitted); + if (submitted !== undefined) { + expect(project.partnerProgram?.programLinkId).toBe(120); + expect(project.partnerProgram?.programId).toBe(12); + } + fixture.componentRef.setInput("info", project); + fixture.detectChanges(); + } + + const text = (selector: string): string => + fixture.nativeElement.querySelector(selector)?.textContent?.trim(); + + it.each([ + { submitted: true, draft: false, status: "Сдан в программу" }, + { submitted: true, draft: true, status: "Сдан в программу" }, + { submitted: false, draft: false, status: "В программе" }, + { submitted: false, draft: true, status: "Черновик" }, + { submitted: undefined, draft: false, status: "В программе" }, + { submitted: undefined, draft: true, status: "Черновик" }, + ])("is_submitted=$submitted, draft=$draft: lifecycle и доступ независимы", async item => { + await loadProject(item.submitted, item.draft); + for (const userId of [7, 99, undefined]) { + fixture.componentRef.setInput("loggedUserId", userId); + fixture.detectChanges(); + expect(text(".card__status")).toBe(item.status); + expect(text(".card__role")).toBe(userId === 7 ? "Лидер" : "Участник"); + expect(text(".card__access-label")).toBe( + userId === 7 && item.submitted === false ? "можно редактировать" : "только просмотр", + ); + expect(text(".card__project-action")).toBe("Открыть"); + const link = fixture.debugElement.query(By.directive(RouterLink)).injector.get(RouterLink); + expect(TestBed.inject(Router).serializeUrl(link.urlTree!)).toBe( + AppRoutes.projects.detail(101), + ); + expect(link.urlTree!.queryParams).toEqual({}); + } + }); + + it("обновляет доступ после получения состояния и не сохраняет его для неполного ответа", async () => { + for (const submitted of [undefined, false, true, undefined]) { + await loadProject(submitted); + expect(text(".card__access-label")).toBe( + submitted === false ? "можно редактировать" : "только просмотр", + ); + } + }); +}); diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html index b341cbd5a..a873de976 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html @@ -2,6 +2,8 @@
    - @if (shouldShowProjectInfo()) { -
    -
    -

    12

    - -
    -
    -

    12

    - -
    -
    - } @if (shouldShowSubscriptionBadge()) { -
    +
    + } - + @if (isProjectCard()) { + + + + + } @else { + + } -
    -
    -
    - @if (appereance() === "empty") { - - } @else { - - } -
    -
    + +
    +
    - @if (appereance() !== "empty") { - - } + + - +

    {{ info()?.name }}

    @@ -86,7 +113,9 @@
    - @if (type() === "projects" || type() === "invite") { + @if (isProjectCard()) { +

    {{ info().name }}

    + } @else if (type() === "projects" || type() === "invite") { @if (info().name) {

    {{ info().name | truncate: 12 }}

    } @@ -116,7 +145,7 @@
    @if (type() === "invite") { @@ -143,67 +172,12 @@
    - @if (type() === "projects") { -
    - @if (info().partnerProgram || info().draft) { - @if (info().partnerProgram && info().draft) { -
    - -
    - - @if (programProjectHovered) { -
    -

    - проект привязан к программе {{ info().partnerProgram.name }} -

    -
    - } - } - - - проект - - -
    - -
    - - @if (iconHovered) { -
    -

    - @if (info().draft) { - проект пока еще не опубликовали. - } @else { - проект привязан к программе - {{ info().partnerProgram.name }} - } -

    -
    - } - } @else { - - проект - - } -
    + @if (isProjectCard()) { + + + Открыть + + } @else if (type() === "members") { @if (leaderId() === loggedUserId() && loggedUserId() !== info()?.userId) {
    diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss index b34779d7a..9a96740fb 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss @@ -27,63 +27,72 @@ align-items: center; justify-content: space-evenly; - &--vacancies, - &--collaborators { - display: flex; - gap: 4px; - align-items: center; - color: var(--grey-for-text); - } - &--program { display: flex; gap: 5px; align-items: center; justify-content: center; - - &-icon { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 12px; - margin-top: 6px; - border: 0.5px solid var(--medium-grey-for-outline); - border-radius: var(--rounded-lg); - - i { - color: var(--accent); - } - } } + } - &--project-partner { - position: absolute; - top: 63%; - left: 30%; - z-index: 10000; - padding: 3px 5px; - background-color: var(--light-white); - border: 0.5px solid var(--medium-grey-for-outline); - border-radius: var(--rounded-lg); + // Ссылка занимает прежнее место контента; CTA внутри не создаёт второй tab stop. + &__project-link { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + border-radius: inherit; - p { - color: var(--black); - } + &:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 2px; } } - &__badge { - position: absolute; - top: 30px; - left: 0; - z-index: 1; + // Сохраняем геометрию прежнего ButtonComponent для навигационного CTA. + &__project-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 70px; + padding: 2px 10px; + color: var(--white); + background: var(--accent); + border: 0.5px solid transparent; + border-radius: var(--rounded-xxl); + transition: all 0.2s; + + &:hover { + background-color: var(--accent-medium); + box-shadow: -2px 3px 3px rgb(51 51 51 / 20%); + } } - &__subscribe-badge { - display: block; - color: var(--accent); + &__subscription-action { + position: absolute; + top: 6px; + right: 6px; + z-index: 3; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + color: var(--accent-dark); cursor: pointer; + background: var(--light-white); + border: 0; + border-radius: var(--rounded-lg); + + &:hover { + background: var(--accent-light); + } + + &:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 2px; + } } &__photo { @@ -243,6 +252,201 @@ &__skill { width: 100px; } + + // Общая геометрия проектов не затрагивает приглашения, участников и пустые карточки. + &--project { + .card__content { + padding: 27px 12px 0; + } + + .card__head { + width: 100%; + height: 30px; + margin-bottom: 6px; + + > div { + width: 100%; + min-width: 0; + } + } + + .card__name { + /* stylelint-disable-next-line value-no-vendor-prefix -- Префикс нужен для двухстрочного ellipsis. */ + display: -webkit-box; + max-height: 30px; + margin: 0; + font-size: 12px; + font-weight: 600; + line-height: 15px; + overflow-wrap: anywhere; + white-space: normal; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .card__description { + flex: 0 0 26px; + width: 100%; + height: 26px; + min-height: 26px; + + p { + /* stylelint-disable-next-line value-no-vendor-prefix -- Префикс сохраняет двухстрочное сокращение описания. */ + display: -webkit-box; + overflow: hidden; + font-size: 10px; + line-height: 13px; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + } + + .card__project-action-label { + font-size: 9px; + font-weight: 600; + line-height: inherit; + white-space: nowrap; + } + } + + // Нижний блок имеет постоянную высоту: длинный текст не сдвигает роль и CTA. + &--my-project { + .card__body { + padding-bottom: 8px; + } + + .card__content { + padding: 22px 10px 0; + } + + .card__context { + margin-bottom: 3px; + } + + .card__head { + height: 28px; + margin-bottom: 2px; + } + + .card__name { + max-height: 28px; + font-size: 11px; + line-height: 14px; + text-align: center; + } + + .card__description { + flex-basis: 24px; + height: 24px; + min-height: 24px; + color: var(--grey-for-text); + + p { + font-size: 9px; + line-height: 12px; + } + } + + .card__footer { + flex-direction: column; + gap: 4px; + align-items: stretch; + width: 100%; + } + + .card__info, + .card__info--text { + width: 100%; + } + + .card__project-action { + width: 100%; + padding: 4px 24px; + } + + .card__project-action-label { + line-height: 11px; + } + } + + &__access { + flex-shrink: 0; + padding-top: 4px; + text-align: center; + border-top: 1px solid var(--medium-grey-for-outline); + } + + &__role { + font-size: 9px; + font-weight: 600; + line-height: 11px; + color: var(--black); + } + + &__access-label { + font-size: 8px; + line-height: 10px; + color: var(--grey-for-text); + } + + &__context { + display: inline-flex; + flex-shrink: 0; + gap: 5px; + align-items: center; + max-width: 100%; + height: 20px; + padding: 3px 8px; + margin-bottom: 6px; + font-size: 9px; + font-weight: 600; + line-height: 14px; + color: var(--dark-grey); + white-space: nowrap; + background: var(--light-gray); + border-radius: 11px; + + &--industry { + color: var(--accent-dark); + background: var(--accent-light); + + span { + overflow: hidden; + text-overflow: ellipsis; + } + + &:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 2px; + } + } + } + + &__status { + &::before { + flex: 0 0 6px; + width: 6px; + height: 6px; + content: ""; + background: currentcolor; + border-radius: 50%; + } + + &--published { + color: var(--green-dark); + background: var(--green-light); + } + + &--program { + color: var(--accent-dark); + background: var(--accent-light); + } + + &--submitted { + color: var(--gold-dark); + background: var(--gold-light); + } + } } .message-modal { diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts index 0fc33a848..321a713dc 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts @@ -1,50 +1,346 @@ /** @format */ +import { OverlayContainer } from "@angular/cdk/overlay"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideRouter, Router, RouterLink, UrlTree } from "@angular/router"; import { of } from "rxjs"; import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { AddProjectSubscriptionUseCase } from "@api/project/use-cases/add-project-subscription.use-case"; +import { DeleteProjectSubscriptionUseCase } from "@api/project/use-cases/delete-project-subscription.use-case"; +import { AppRoutes } from "@api/paths/app-routes"; +import { ok } from "@domain/shared/result.type"; +import { AvatarComponent } from "@ui/primitives/avatar/avatar.component"; +import { IconComponent } from "@ui/primitives"; import { InfoCardComponent } from "./info-card.component"; -import { provideRouter } from "@angular/router"; -import { ProjectSubscriptionRepositoryPort } from "@domain/project/ports/project-subscription.repository.port"; -import { EventBus } from "@domain/shared/event-bus"; +import { myProjectCardFixtures, projectCardFixture, projectCardProgram } from "./info-card.fixture"; -describe("ProjectCardComponent", () => { - let component: InfoCardComponent; +describe("InfoCardComponent: статусы моих проектов", () => { let fixture: ComponentFixture; + let component: InfoCardComponent; + const addSubscription = { execute: vi.fn(() => of(ok(undefined))) }; + const deleteSubscription = { execute: vi.fn(() => of(ok(undefined))) }; beforeEach(async () => { - const industrySpy = { industries: () => of([]), getOne: () => of({ name: "Test Industry" }) }; - + vi.clearAllMocks(); await TestBed.configureTestingModule({ imports: [InfoCardComponent], providers: [ - { provide: IndustryRepositoryPort, useValue: industrySpy }, { - provide: ProjectSubscriptionRepositoryPort, - useValue: { addSubscription: of({}), deleteSubscription: of({}) }, + provide: IndustryRepositoryPort, + useValue: { industries: () => [], getOne: () => ({ id: 1, name: "Образование" }) }, }, - { provide: EventBus, useValue: { emit: () => {} } }, + { provide: AddProjectSubscriptionUseCase, useValue: addSubscription }, + { provide: DeleteProjectSubscriptionUseCase, useValue: deleteSubscription }, provideRouter([]), ], }).compileComponents(); - }); - - beforeEach(() => { fixture = TestBed.createComponent(InfoCardComponent); component = fixture.componentInstance; - fixture.componentRef.setInput("info", { - id: 1, - name: "Test Project", - description: "", - skills: [], - status: "active", - isCompetitive: false, - }); + fixture.componentRef.setInput("info", projectCardFixture()); fixture.componentRef.setInput("type", "projects"); + fixture.componentRef.setInput("appereance", "my"); + fixture.componentRef.setInput("loggedUserId", 7); + }); + + afterEach(async () => { + // CDK прикрепляет/открепляет portal в следующем такте; завершаем его до teardown. + await new Promise(resolve => setTimeout(resolve, 0)); + vi.restoreAllMocks(); + }); + const card = (): HTMLElement => fixture.nativeElement.querySelector(".card__body"); + + it.each(myProjectCardFixtures)( + "$key / $role: статус, доступ и единственный корректный маршрут", + async item => { + const original = JSON.stringify(item.project); + fixture.componentRef.setInput("info", item.project); + fixture.detectChanges(); + expect(card().querySelectorAll(".card__status")).toHaveLength(1); + expect( + card() + .querySelector(".card__status") + ?.classList.contains("card__status--" + item.key), + ).toBe(true); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe(item.label); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + item.action, + ); + expect(card().querySelector(".card__name")?.textContent).toBe(item.project.name); + expect( + card().querySelector( + ".card__info--vacancies, .card__info--collaborators, .card__info--program-icon, .card__industry, .card__info--project-partner", + ), + ).toBeNull(); + expect(fixture.debugElement.queryAll(By.directive(IconComponent))).toHaveLength(0); + const avatar = fixture.debugElement.query(By.directive(AvatarComponent)).componentInstance; + expect(avatar.size()).toBe(70); + expect(avatar.url()).toBe(item.project.imageAddress); + const router = TestBed.inject(Router); + const link = fixture.debugElement.query(By.directive(RouterLink)).injector.get(RouterLink); + expect(card().querySelector(".card__role")?.textContent).toBe(item.role); + expect(card().querySelector(".card__access-label")?.textContent).toBe(item.access); + const expectedRoute = AppRoutes.projects.detail(item.project.id); + expect(router.serializeUrl(link.urlTree!)).toBe(expectedRoute); + expect(link.urlTree!.queryParams).toEqual({}); + expect(card().querySelectorAll("a")).toHaveLength(1); + expect(card().querySelector("a a, a button, a [tabindex]")).toBeNull(); + const navigate = vi.spyOn(router, "navigateByUrl").mockResolvedValue(true); + card().querySelector(".card__project-action")!.click(); + await fixture.whenStable(); + expect(navigate).toHaveBeenCalledOnce(); + expect(router.serializeUrl(navigate.mock.calls[0][0] as UrlTree)).toBe(expectedRoute); + navigate.mockClear(); + card().querySelector(".card__name")!.click(); + expect(navigate).toHaveBeenCalledOnce(); + expect(router.serializeUrl(navigate.mock.calls[0][0] as UrlTree)).toBe(expectedRoute); + expect(JSON.stringify(item.project)).toBe(original); + expect(addSubscription.execute).not.toHaveBeenCalled(); + expect(deleteSubscription.execute).not.toHaveBeenCalled(); + }, + ); + + it.each([7, 99])("сдача имеет приоритет над draft для пользователя %s", userId => { + fixture.componentRef.setInput("loggedUserId", userId); + fixture.componentRef.setInput( + "info", + projectCardFixture({ draft: true, partnerProgram: projectCardProgram(true) }), + ); fixture.detectChanges(); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe("Сдан в программу"); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + expect(card().querySelector(".card__role")?.textContent).toBe( + userId === 7 ? "Лидер" : "Участник", + ); + expect(card().querySelector(".card__access-label")?.textContent).toBe("только просмотр"); + const link = fixture.debugElement.query(By.directive(RouterLink)).injector.get(RouterLink); + expect(link.urlTree!.queryParams).toEqual({}); + expect(TestBed.inject(Router).serializeUrl(link.urlTree!)).toBe(AppRoutes.projects.detail(101)); }); - it("should create", () => { - expect(component).toBeTruthy(); + it.each(myProjectCardFixtures.slice(0, 4))( + "$key: смена роли или отсутствие профиля не меняет lifecycle и CTA", + item => { + fixture.componentRef.setInput("info", item.project); + for (const userId of [7, 99, undefined]) { + fixture.componentRef.setInput("loggedUserId", userId); + fixture.detectChanges(); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe(item.label); + expect(card().querySelector(".card__role")?.textContent).toBe( + userId === 7 ? "Лидер" : "Участник", + ); + expect(card().querySelector(".card__access-label")?.textContent).toBe( + userId === 7 ? item.access : "только просмотр", + ); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe("Открыть"); + const link = fixture.debugElement.query(By.directive(RouterLink)).injector.get(RouterLink); + expect(TestBed.inject(Router).serializeUrl(link.urlTree!)).toBe( + AppRoutes.projects.detail(item.project.id), + ); + expect(link.urlTree!.queryParams).toEqual({}); + } + }, + ); + + it("отсутствующие ID безопасны, а загрузка/смена пользователя обновляет доступ", () => { + fixture.componentRef.setInput("info", projectCardFixture({ leader: undefined })); + fixture.componentRef.setInput("loggedUserId", undefined); + fixture.detectChanges(); + expect(card().querySelector(".card__role")?.textContent).toBe("Участник"); + expect(card().querySelector(".card__access-label")?.textContent).toBe("только просмотр"); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + fixture.componentRef.setInput("info", projectCardFixture({ leader: 7 })); + fixture.detectChanges(); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + fixture.componentRef.setInput("loggedUserId", 7); + fixture.detectChanges(); + expect(card().querySelector(".card__role")?.textContent).toBe("Лидер"); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + fixture.componentRef.setInput("loggedUserId", undefined); + fixture.detectChanges(); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + }); + + it("canSubmit не подменяет факт сдачи", () => { + fixture.componentRef.setInput( + "info", + projectCardFixture({ + partnerProgram: { ...projectCardProgram(false), canSubmit: false }, + }), + ); + fixture.detectChanges(); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe("В программе"); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + }); + + it("draft имеет приоритет над обычной связью с программой", () => { + fixture.componentRef.setInput( + "info", + projectCardFixture({ draft: true, partnerProgram: projectCardProgram(false) }), + ); + fixture.detectChanges(); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe("Черновик"); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + fixture.detectChanges(); + expect(card().querySelector(".card__info--project-partner")).toBeNull(); + expect(card().textContent).not.toContain("привязан к программе"); + }); + + it("отсутствующая связь не превращает опубликованный проект в программный", () => { + fixture.componentRef.setInput("info", projectCardFixture({ partnerProgram: undefined })); + fixture.detectChanges(); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe("Опубликован"); + }); + + it("обновляет статус при замене данных и убирает его при смене appearance", () => { + fixture.componentRef.setInput("info", myProjectCardFixtures[0].project); + fixture.detectChanges(); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe("Черновик"); + fixture.componentRef.setInput("info", myProjectCardFixtures[3].project); + fixture.detectChanges(); + expect(card().querySelector(".card__status")?.textContent?.trim()).toBe("Сдан в программу"); + fixture.componentRef.setInput("appereance", "subs"); + fixture.detectChanges(); + expect(card().querySelector(".card__status")).toBeNull(); + expect(fixture.nativeElement.querySelector(".card--project")).not.toBeNull(); + }); + + it("сохраняет полное название и описание в DOM для двухстрочного CSS-ограничения", () => { + const project = projectCardFixture({ + name: "ОченьДлинноеНазваниеБезПробелов".repeat(4), + shortDescription: "Очень длинное описание проекта. ".repeat(20), + }); + fixture.componentRef.setInput("info", project); + fixture.detectChanges(); + expect(card().querySelector(".card__name")?.textContent).toBe(project.name); + expect(card().querySelector(".card__description")?.textContent?.trim()).toBe( + project.shortDescription.trim(), + ); + }); + + it.each(["base", "subs"] as const)( + "%s: отрасль, полный текст, Открыть и отсутствие lifecycle/шума", + appearance => { + const project = projectCardFixture({ draft: true, partnerProgram: projectCardProgram(true) }); + fixture.componentRef.setInput("info", project); + fixture.componentRef.setInput("appereance", appearance); + fixture.detectChanges(); + expect(card().querySelector(".card__status")).toBeNull(); + expect(card().querySelector(".card__access")).toBeNull(); + expect(fixture.nativeElement.querySelector(".card--project")).not.toBeNull(); + expect(card().querySelector(".card__name")?.textContent).toBe(project.name); + expect(card().querySelector(".card__context--industry")?.textContent?.trim()).toBe( + "Образование", + ); + expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( + "Открыть", + ); + expect( + card().querySelector( + ".card__info--vacancies, .card__info--collaborators, .card__info--program-icon, .card__industry, .card__info--project-partner", + ), + ).toBeNull(); + const router = TestBed.inject(Router); + const link = fixture.debugElement.query(By.directive(RouterLink)).injector.get(RouterLink); + expect(router.serializeUrl(link.urlTree!)).toBe(AppRoutes.projects.detail(project.id)); + }, + ); + + it.each(["base", "subs"] as const)("%s без отрасли не резервирует плашку", appearance => { + fixture.componentRef.setInput("info", projectCardFixture({ industry: undefined })); + fixture.componentRef.setInput("appereance", appearance); + fixture.detectChanges(); + expect(card().querySelector(".card__context")).toBeNull(); + expect(card().querySelector(".card__name")?.textContent).toBe(projectCardFixture().name); + }); + + it("длинная отрасль доступна целиком в подсказке и с клавиатуры", () => { + const name = "Очень длинное название отрасли проекта"; + vi.spyOn(TestBed.inject(IndustryRepositoryPort), "getOne").mockReturnValue({ id: 1, name }); + fixture.componentRef.setInput("appereance", "subs"); + fixture.detectChanges(); + const context = card().querySelector(".card__context--industry")!; + expect(context.title).toBe(name); + expect(context.textContent?.trim()).toBe(name); + const link = card().querySelector(".card__project-link")!; + expect(link.title).toBe(name); + expect(link.tabIndex).toBe(0); + }); + + it.each(["base", "subs"] as const)( + "%s: отдельное действие сохраняет сценарий отписки", + async appearance => { + fixture.componentRef.setInput("appereance", appearance); + fixture.componentRef.setInput("showSubscriptionAction", true); + fixture.componentRef.setInput("profileId", 101); + fixture.componentRef.setInput("isSubscribed", true); + fixture.detectChanges(); + const action = card().querySelector(".card__subscription-action")!; + expect(action.getAttribute("aria-label")).toBe("Отписаться от проекта"); + expect(action.type).toBe("button"); + expect(action.closest(".card__content, a")).toBeNull(); + const navigate = vi.spyOn(TestBed.inject(Router), "navigateByUrl").mockResolvedValue(true); + action.click(); + fixture.detectChanges(); + expect(component.isUnsubscribeModalOpen).toBe(true); + expect(deleteSubscription.execute).not.toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + await new Promise(resolve => setTimeout(resolve, 0)); + await fixture.whenStable(); + TestBed.inject(OverlayContainer) + .getContainerElement() + .querySelector(".unsubscribe-modal__buttons button")! + .click(); + fixture.detectChanges(); + expect(deleteSubscription.execute).toHaveBeenCalledExactlyOnceWith(101); + expect(component.isSubscribed).toBe(false); + expect(component.isUnsubscribeModalOpen).toBe(false); + }, + ); + + it("подписка отправляет существующий use case с ID проекта", () => { + fixture.componentRef.setInput("appereance", "base"); + fixture.componentRef.setInput("showSubscriptionAction", true); + fixture.componentRef.setInput("profileId", 101); + fixture.detectChanges(); + card().querySelector(".card__subscription-action")!.click(); + fixture.detectChanges(); + expect(addSubscription.execute).toHaveBeenCalledExactlyOnceWith(101); + expect(component.isSubscribed).toBe(true); + }); + + it.each(["invite", "members", "rating"] as const)("не оформляет %s как мой проект", type => { + fixture.componentRef.setInput("showSubscriptionAction", true); + fixture.componentRef.setInput("type", type); + fixture.detectChanges(); + expect(card().querySelector(".card__status")).toBeNull(); + expect(card().querySelector(".card__access")).toBeNull(); + expect(fixture.nativeElement.querySelector(".card--project")).toBeNull(); + }); + + it("сохраняет пустую карточку и действие создания", () => { + fixture.componentRef.setInput("appereance", "empty"); + fixture.detectChanges(); + expect(card().querySelector(".card__status")).toBeNull(); + expect(fixture.nativeElement.querySelector(".card__empty")).not.toBeNull(); + expect(card().textContent).toContain("Создайте первый проект"); + const create = vi.fn(); + component.onCreate.subscribe(create); + card().click(); + expect(create).toHaveBeenCalledOnce(); }); }); diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts index 12a9fedf7..15007d28a 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, + computed, DestroyRef, inject, Input, @@ -23,6 +24,16 @@ import { AddProjectSubscriptionUseCase } from "@api/project/use-cases/add-projec import { DeleteProjectSubscriptionUseCase } from "@api/project/use-cases/delete-project-subscription.use-case"; import { AppRoutes } from "@api/paths/app-routes"; import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { Project } from "@domain/project/project.model"; + +interface MyProjectPresentation { + lifecycle: "submitted" | "draft" | "program" | "published"; + statusLabel: "Черновик" | "Опубликован" | "В программе" | "Сдан в программу"; + role: "leader" | "participant"; + roleLabel: "Лидер" | "Участник"; + accessLabel: "можно редактировать" | "только просмотр"; + canEdit: boolean; +} /** * Компонент карточки информации с разным наполнением, в зависимости от контекста @@ -64,6 +75,65 @@ export class InfoCardComponent { readonly profileId = input(); readonly leaderId = input(); readonly loggedUserId = input(); + readonly showSubscriptionAction = input(false); + + /** Общая геометрия применяется только к заполненным карточкам проектов. */ + protected readonly isProjectCard = computed( + () => this.type() === "projects" && this.appereance() !== "empty", + ); + + /** Отрасль берётся из уже загруженного справочника; пустая плашка не занимает строку. */ + protected readonly projectIndustry = computed(() => { + if (!this.isProjectCard() || this.appereance() === "my") return null; + const industryId = this.info()?.industry; + return industryId == null + ? null + : this.industryRepository.getOne(industryId)?.name?.trim() || null; + }); + + /** + * Lifecycle зависит только от проекта: submitted > draft > program > published. + * Роль определяется отдельно по текущему профилю. Отсутствующие ID не делают + * пользователя лидером; после сдачи даже лидер видит «только просмотр». + * Единственный признак сдачи — isSubmitted: canSubmit описывает возможность + * действия, например открытый срок. Пока состояние программной связи неизвестно, + * не обещаем редактирование: отсутствие isSubmitted не равнозначно false. + * Это представление готовых данных, + * а не изменение guard или серверных прав. CTA от этих значений не зависит. + */ + protected readonly myProjectPresentation = computed(() => { + if (this.type() !== "projects" || this.appereance() !== "my") return null; + const project: Project | undefined = this.info(); + if (!project) return null; + + const isSubmitted = project.partnerProgram?.isSubmitted === true; + const lifecycle = isSubmitted + ? "submitted" + : project.draft === true + ? "draft" + : project.partnerProgram != null + ? "program" + : "published"; + const labels: Record = + { + submitted: "Сдан в программу", + draft: "Черновик", + program: "В программе", + published: "Опубликован", + }; + const userId = this.loggedUserId(); + const isLeader = userId != null && project.leader === userId; + const canEdit = + isLeader && (project.partnerProgram == null || project.partnerProgram.isSubmitted === false); + return { + lifecycle, + statusLabel: labels[lifecycle], + role: isLeader ? "leader" : "participant", + roleLabel: isLeader ? "Лидер" : "Участник", + accessLabel: canEdit ? "можно редактировать" : "только просмотр", + canEdit, + }; + }); readonly onAcceptingInvite = output(); readonly onRejectingInvite = output(); @@ -73,35 +143,20 @@ export class InfoCardComponent { // Состояние компонента isUnsubscribeModalOpen = false; inviteErrorModal = false; - haveBadge = this.calculateHaveBadge(); - - programProjectHovered = false; - iconHovered = false; - draftProjectHovered = false; removeCollaboratorFromProject(userId: number): void { this.onRemoveCollaborator.emit(userId); } /** - * Определяет, нужно ли показывать информацию о проекте - */ - shouldShowProjectInfo(): boolean { - return ( - this.type() === "projects" && this.appereance() !== "subs" && this.appereance() !== "empty" - ); - } - - /** - * Определяет, нужно ли показывать бейдж подписки + * Контейнер явно разрешает действие подписки; URL не определяет контекст карточки. + * Приглашения, участники, пустые и собственные проекты не получают это действие. */ shouldShowSubscriptionBadge(): boolean { return ( - this.appereance() !== "empty" && - this.haveBadge && - this.appereance() === "base" && - this.type() !== "invite" && - this.type() !== "members" + this.showSubscriptionAction() && + this.isProjectCard() && + (this.appereance() === "base" || this.appereance() === "subs") ); } @@ -244,15 +299,4 @@ export class InfoCardComponent { .navigateByUrl(AppRoutes.projects.all()) .then(() => this.logger.debug("Route change from ProjectsComponent")); } - - /** - * Вычисление флага haveBadge - */ - private calculateHaveBadge(): boolean { - return ( - location.href.includes("/subscriptions") || - location.href.includes("/all") || - location.href.includes("/projects") - ); - } } diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.fixture.ts b/projects/social_platform/src/app/ui/widgets/info-card/info-card.fixture.ts new file mode 100644 index 000000000..97fe1564b --- /dev/null +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.fixture.ts @@ -0,0 +1,159 @@ +/** @format */ + +import { PartnerProgramInfo, Project } from "@domain/project/project.model"; + +/** Связь из list-контракта: имя программы для отображения статуса не требуется. */ +export function projectCardProgram(isSubmitted: boolean): PartnerProgramInfo { + return { + id: 12, + programId: 12, + programLinkId: 120, + isSubmitted, + canSubmit: !isSubmitted, + programFields: [], + programFieldValues: [], + }; +} + +/** Полная модель исключает зависимость UI-тестов от случайно пропущенных полей API. */ +export function projectCardFixture(overrides: Partial = {}): Project { + return { + ...Project.default(), + id: 101, + leader: 7, + name: "Кейс для Ростовской области", + shortDescription: "Проект для комфортной жизни и совместной работы студентов.", + imageAddress: "/assets/images/projects/shared/idea.svg", + industry: 1, + ...overrides, + }; +} + +/** Lifecycle и роль проверяются на одних данных в component tests и visual smoke. */ +export const myProjectCardFixtures = [ + { + key: "draft", + label: "Черновик", + action: "Открыть", + role: "Лидер", + access: "можно редактировать", + canEdit: true, + project: projectCardFixture({ + id: 101, + draft: true, + name: "Тестирование пути пользователя", + shortDescription: "Удобный сервис для организации учебного процесса и студенческой жизни.", + }), + }, + { + key: "published", + label: "Опубликован", + action: "Открыть", + role: "Лидер", + access: "можно редактировать", + canEdit: true, + project: projectCardFixture({ + id: 102, + name: "Тест ленты", + shortDescription: "Инициативы для более экологичного и комфортного университета.", + }), + }, + { + key: "program", + label: "В программе", + action: "Открыть", + role: "Лидер", + access: "можно редактировать", + canEdit: true, + project: projectCardFixture({ + id: 103, + name: "TEST VALIDATION", + shortDescription: "Интеллектуальный ассистент для студентов и преподавателей.", + partnerProgram: projectCardProgram(false), + }), + }, + { + key: "submitted", + label: "Сдан в программу", + action: "Открыть", + role: "Лидер", + access: "только просмотр", + canEdit: false, + project: projectCardFixture({ + id: 104, + name: "Анализ результатов исследования", + shortDescription: "Решения для создания инклюзивной и комфортной образовательной среды.", + partnerProgram: projectCardProgram(true), + }), + }, + { + key: "program", + label: "В программе", + action: "Открыть", + role: "Участник", + access: "только просмотр", + canEdit: false, + project: projectCardFixture({ + id: 105, + leader: 8, + name: "TEST VALIDATION & SEVERAL PROCESSES", + shortDescription: "info", + partnerProgram: projectCardProgram(false), + }), + }, + { + key: "submitted", + label: "Сдан в программу", + action: "Открыть", + role: "Участник", + access: "только просмотр", + canEdit: false, + project: projectCardFixture({ + id: 106, + leader: 8, + name: "Командное исследование", + shortDescription: "Сданный проект участника команды.", + partnerProgram: projectCardProgram(true), + }), + }, + { + key: "draft", + label: "Черновик", + action: "Открыть", + role: "Участник", + access: "только просмотр", + canEdit: false, + project: projectCardFixture({ + id: 107, + leader: 8, + draft: true, + partnerProgram: null, + name: "Черновик команды", + shortDescription: "Совместная работа над идеей нового проекта.", + }), + }, + { + key: "published", + label: "Опубликован", + action: "Открыть", + role: "Участник", + access: "только просмотр", + canEdit: false, + project: projectCardFixture({ + id: 108, + leader: 8, + draft: false, + partnerProgram: null, + name: "Открытая инициатива", + shortDescription: "Опубликованный проект участника команды.", + }), + }, +] as const; + +/** Отрасли и их отсутствие проверяются на одних данных в подписках и витрине. */ +export const publicProjectCardFixtures = [ + projectCardFixture({ id: 201, name: "Учимся вместе", industry: 1 }), + projectCardFixture({ id: 202, name: "Открытая лаборатория технологий", industry: 2 }), + projectCardFixture({ id: 203, name: "Сообщество добрых дел", industry: 3 }), + projectCardFixture({ id: 204, name: "Проект без указанной отрасли", industry: undefined }), +];