Виджеты — переиспользуемые блоки внутри social_platform. По соглашению виджет используется в одном-двух местах в рамках одной/нескольких страниц (в отличие от примитивов, которые повсюду). Папка projects/social_platform/src/app/ui/widgets/ содержит 20 директорий.
Импорт через @ui/widgets/<name>/<name>.component.
| Виджет | Selector | Inputs | Outputs | Где используется |
|---|---|---|---|---|
ChatWindowComponent |
app-chat-window |
messages: ChatMessage[] (req, setter), currentUserId (опц.), placeholder, loadingMore |
submit: ChatMessage, edit: ChatMessage, delete: number, type: void, fetch: void (для пагинации), read: number (id прочитанного) |
pages/chat/... (direct и project chats) |
CookieConsentComponent |
app-cookie-consent |
— | — | app.component.html (root) |
CourseAboutComponent |
app-course-about |
description: string (req) |
— | pages/courses/detail/info/info.component, pages/courses/detail/course-detail.component |
DeatilComponent (sic — опечатка) |
app-detail |
— (всё через route.data + DI фасадов) |
— | widgets/detail/detail.component обёртка для <app-detail> в pages/projects/detail, pages/program/detail, pages/profile/detail. Универсальный header с инфо + табы + кнопки действий. |
FeedFilterComponent |
app-feed-filter |
— (читает query params + дёргает фасады напрямую) | — | pages/feed |
HeaderComponent |
app-header |
invites: Invite[] |
acceptInvite: number, rejectInvite: number |
pages/office/office.component (шапка office shell) |
InfoCardComponent |
app-info-card |
info?: any, type: "invite" | "projects" | "members" | "rating" = "projects", appereance: "my" | "subs" | "base" | "empty" = "base" (sic — опечатка), section: "projects" | "subscriptions" | "other" = "projects", canDelete?, isSubscribed?, profileId?, leaderId?, loggedUserId? |
onAcceptingInvite: number, onRejectingInvite: number, onCreate: void, onRemoveCollaborator: number |
Списки: pages/program/detail/list/list.component, pages/members/..., и др. |
MessageInputComponent |
app-message-input |
placeholder = "", mask = "", replyMessage?: ChatMessage, value (CVA setter) |
appValueChange: { text, files }, submit: void, resize: void, cancel: void (отмена reply) |
widgets/chat-window |
NewsCardComponent |
app-news-card |
feedItem: FeedNews (req), resourceLink: (string | number)[] (req), contentId?: number, isOwner?: boolean |
delete: number, like: number, edited: FeedNews |
widgets/news-form, pages/feed, pages/program/detail/main, pages/projects/detail/info, pages/projects/detail/news-detail |
NewsFormComponent |
app-news-form |
— (внутри своя ReactiveForm) | addNews: { text: string; files: string[] } |
pages/program/detail/main, pages/projects/detail/info |
ProgramLinksComponent |
app-program-links |
title: string (req), icon: string (req), links: { label, url }[] (req) |
— | widgets/detail, pages/program/detail/main |
ProjectDirectionCard |
app-project-direction-card |
direction!: string, icon!: string, about!: string | any[], type!: string, isOwner!: boolean, profileInfoType?: "skills" | "achievements", projectInfoType?: "goals" | "partners" |
— | pages/profile/detail, pages/projects/detail/info |
ProjectNavigationComponent |
app-project-navigation |
navItems: Navigation[] |
stepChange: EditStep |
Step-by-step редакторы: pages/projects/edit, pages/profile/edit |
ProjectVacancyCardComponent |
app-project-vacancy-card |
vacancy: Vacancy (req), type: "vacancies" | "project" = "project" |
— | pages/vacancies/list, pages/projects/detail/vacancies |
ProjectsFilterComponent |
app-projects-filter |
— (через сервис + query params) | — | pages/projects/list, pages/program/detail/list |
SkillsBasketComponent |
app-skills-basket |
error = false |
— (внутри ReactiveForm) | Профильный edit, проектный edit |
SkillsGroupComponent |
app-skills-group |
options: Skill[] (req, setter), selected: Skill[] (req, setter), title: string (req), hasOpenGroups = false, disabled = false |
groupToggled: boolean, optionToggled: Skill |
Профильный edit, проектный edit, kanban task-detail |
SpecializationsGroupComponent |
app-specializations-group |
title: string (req), options: Specialization[] (req), hasOpenGroups = false, disabled = false |
selectOption: Specialization, groupToggled: boolean |
Профильный edit |
VacancyCardComponent |
app-vacancy-card |
vacancy?: Vacancy |
remove: number, edit: number |
pages/vacancies/list, pages/projects/detail/vacancies |
VacancyFilterComponent |
app-vacancy-filter |
searchValue (setter) |
searchValueChange: string |
pages/vacancies/list |
Универсальный заголовок-шапка для трёх типов сущностей: проект, программа, профиль. Решение какой режим включить берётся из route.data.listType ("project" \| "program" \| "profile") и из URL (isProjectsPage, isProjectChatPage, isKanbanBoardPage и т. п. — определяются в DetailInfoService).
Внутри:
DetailInfoService— общий fasade.DetailProfileInfoService/DetailProjectInfoService/DetailProgramInfoService— специфичные для типа.ProjectAdditionalService,TooltipInfoService— вспомогательные.ChatStateService— для индикатора непрочитанных в чате.
Компонент сам ничего не принимает через @Input() — все данные идут через route.data resolver и инжектируемые сервисы. Поэтому виджет тяжёлый (840+ строк HTML и 130+ строк TS) и фактически "smart" вопреки общей конвенции виджет = dumb. Это исторический долг.
Опечатка в имени класса (DeatilComponent) сохраняется — переименование сломало бы все импорты.
Универсальная карточка для отображения сущностей в списках. Тип данных переключается через type ("invite" \| "projects" \| "members" \| "rating"). Поле info: any — ввиду полиморфизма; реальный тип зависит от type.
Опечатка appereance (вместо appearance) сохраняется — менять сломает шаблоны-потребители.
Пара. ChatWindowComponent — собственно окно с сообщениями (виртуальный скролл, типизация, "is typing"). MessageInputComponent — поле ввода с поддержкой replyMessage и upload-файлов (CVA).
Output type: void в ChatWindowComponent имеет коллизию с TypeScript-зарезервированным словом — компилируется, но в IDE может выглядеть странно.
Все три не принимают inputs — каждый дёргает свой *FilterInfoService напрямую и читает/пишет query-параметры через Router + ActivatedRoute. Это противоречит "виджет = dumb"; правильнее было бы переделать на input-driven через декларативные FilterFieldConfig-конфиги (см. domain/other/filter-fields.model.ts), но сейчас работают так.
Пара. NewsFormComponent — форма создания (ReactiveForm с text+files), эмитит addNews. NewsCardComponent — отображение одной новости с редактированием/удалением/лайком.
NewsCardComponent принимает resourceLink: (string | number)[] — массив сегментов URL для router-link навигации к деталям новости. Это путь который собирает страница-родитель (для feed: [], для project-news: ["/office/projects/", projectId, "news"]).
Простые dumb-компоненты. ProgramLinksComponent рисует блок «контакты» / «материалы» программы — с иконкой, заголовком, списком ссылок (через UserLinksPipe). CourseAboutComponent — блок «о курсе» с раскрытием по кнопке.
Триада для UX навыков и специализаций:
SkillsGroupComponent— рендер одной группы навыков (категория с под-навыками), эмитит выбор отдельного навыка черезoptionToggled.SpecializationsGroupComponent— то же, но для специализаций.SkillsBasketComponent— корзина выбранных навыков (с возможностью удаления). Внутри подключён к ReactiveForm (через@Input() errorдля подсветки и DI родительскойFormGroup).
Stepper для редактирования (профиля и проекта). Принимает массив navItems: Navigation[] (тип из @core/lib/models/navigation.model.ts) и эмитит stepChange: EditStep при клике. Готовые наборы шагов — в core/consts/navigation/nav-{profile,project}-items.const.ts.
Шапка office shell. Принимает invites: Invite[] (для бейджа уведомлений) и эмитит acceptInvite / rejectInvite. Использует app-profile-control-panel из @uilib.
GDPR-баннер, добавлен через коммит e3a36e7c. Лежит в widgets (не в primitives), потому что используется ровно один раз — в app.component.html. Управляет localStorage.cookieConsent, при accepted дёргает AnalyticsService.loadAnalytics() (Yandex Metrika + Mail.ru counter — только на app.procollab.ru, см. docs/cross-cutting.md).