Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions docs/feed-card-visual-system/README.md

Large diffs are not rendered by default.

617 changes: 617 additions & 0 deletions docs/feed-card-visual-system/geometry.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/feed-card-visual-system/preview-index.html.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><html lang="ru"><head><meta charset="utf-8"><title>Раздел проектов — локальная проверка</title><base href="/"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body></body></html>
40 changes: 40 additions & 0 deletions docs/feed-card-visual-system/preview-main.ts.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import '@angular/compiler';
import { Component, provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
import { provideRouter, Router, RouterOutlet } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideNoopAnimations } from '@angular/platform-browser/animations';
import { API_URL } from '@corelib';
import { FeedComponent } from '../projects/social_platform/src/app/ui/pages/feed/feed.component';
import { FeedInfoService } from '@api/feed/facades/feed-info.service';
import { FeedUIInfoService } from '@api/feed/facades/ui/feed-ui-info.service';
import { IndustryRepositoryPort } from '@domain/industry/ports/industry.repository.port';
import { FeedNews } from '@domain/news/project-news.model';

const params=new URLSearchParams(location.search);
const long=params.has('long');
const avatar='/assets/images/profile/main.svg';
const date='2026-08-29T12:00:00Z';
const project={id:51,name:long?'ОченьДлинноеНазваниеПроектаБезПробелов'.repeat(6):'Команда цифровых решений',imageAddress:avatar,industry:1,leader:7,shortDescription:long?'Подробное описание нового проекта '.repeat(60):'Помогаем студентам находить команду и запускать полезные инициативы.',viewsCount:0};
const vacancy=(id:number)=>({typeModel:'vacancy',publishedAt:date,content:{id,project,role:long?'ВедущийСпециалистПоТестированиюСложныхСистем'.repeat(6):'Тестировщик (QA)',description:long?'Очень подробное описание вакансии и задач '.repeat(60):'Помогите проверить новую платформу: тестируйте API, находите ошибки и улучшайте опыт пользователей.',requiredSkills:['QA','Postman','API','SQL','Git','Аналитика'].map((name,id)=>({id,name:long?name+'ОченьДлинныйНавыкБезПробелов'.repeat(3):name}))}});
const news=(id:number,people=false,media=false)=>({typeModel:'news',publishedAt:date,content:{...FeedNews.default(),id,name:people?'Анна Смирнова':project.name,imageAddress:avatar,text:long?'ОченьДлинныйЗаголовокБезПробелов'.repeat(8)+'. '+'Большой текст новости '.repeat(100):(people?'Поделилась результатами исследования. ':'Запустили открытую лабораторию. ')+'Приглашаем познакомиться с первыми результатами и обсудить идеи следующего этапа.',contentObject:people?{id:42,email:'fixture@example.test'}:project,files:media?[{link:'/assets/images/profile/main.svg',name:'Иллюстрация',mimeType:'image/svg+xml',size:100},{link:'/assets/images/profile/skills.svg',name:'Вторая иллюстрация',mimeType:'image/svg+xml',size:100},{link:'/assets/images/profile/main.svg',name:'Описание проекта.pdf',mimeType:'application/pdf',size:1024}]:[]}});
const items:any[]=[vacancy(1),news(2),{typeModel:'project',publishedAt:date,content:{...project,id:1}},news(4,true),vacancy(5),news(6,false,true)];
if(params.has('odd')) items.pop();
const crop=params.get('crop');
const cropY=crop==='row2'?647:crop==='filter'?58:207;
const cropStyle=crop?`position:relative;left:-365.828125px;top:-${cropY}px;width:1425px`:'';
const ui=new FeedUIInfoService();
ui.applyInitializationFeedNewsEvent({count:6,next:'',previous:'',results:items,counts:{all:86,project:56,vacancy:3,news:27,partnerprogram:0,education:0}});
@Component({selector:'app-preview',imports:[RouterOutlet],styleUrls:['../projects/social_platform/src/app/ui/pages/office/office.component.scss'],template:`<div [style]="cropStyle"><div class="office"><div class="office__wrapper"><div class="office__body"><div class="office__inner"><div class="office__inner--wrapper"><aside class="office__sidebar" style="padding-top:30px"><img src="/assets/images/shared/logo.svg" alt="PROCOLLAB" style="width:130px"><p style="font-size:10px;color:var(--dark-grey);margin-top:24px">Локальная проверка<br>Тестовые данные</p></aside><div class="office__inner--content"><router-outlet/></div></div></div></div></div></div></div>`})
class Preview { cropStyle=cropStyle; }
TestBed.initTestEnvironment(BrowserTestingModule,platformBrowserTesting());
TestBed.configureTestingModule({imports:[Preview],providers:[provideZonelessChangeDetection(),provideHttpClient(),provideNoopAnimations(),provideRouter([{path:'office/feed',component:FeedComponent},{path:'',redirectTo:'office/feed',pathMatch:'full'}]),{provide:API_URL,useValue:'/fixture-api'},{provide:IndustryRepositoryPort,useValue:{getOne:()=>({id:1,name:long?'ОченьДлинноеНазваниеОтраслиБезПробелов'.repeat(6):'Entertainment Tech'})}}]})
.overrideComponent(FeedComponent,{set:{providers:[{provide:FeedUIInfoService,useValue:ui},{provide:FeedInfoService,useValue:{initializationFeedNews(){},initScroll(){},destroy(){},onLike(id:number){const index=ui.feedItems().findIndex(x=>x.typeModel==='news'&&x.content.id===id);ui.applyLikeNews(index)}}}]}});
TestBed.compileComponents().then(async()=>{
const fixture=TestBed.createComponent(Preview);
document.body.appendChild(fixture.nativeElement);
fixture.autoDetectChanges();
await TestBed.inject(Router).navigateByUrl(location.pathname+location.search);

});
1 change: 1 addition & 0 deletions docs/feed-card-visual-system/preview-tsconfig.json.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"extends":"../tsconfig.json","compilerOptions":{"outDir":"../out-tsc/surface-preview","types":[]},"files":["surface-main.ts"],"include":["../projects/**/*.d.ts"]}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions docs/feed-card-visual-system/serve-preview.py.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit
class Handler(SimpleHTTPRequestHandler):
def do_GET(self):
if urlsplit(self.path).path.startswith(('/office/','/card-reference','/activity-reference')):
self.path='/index.html'
super().do_GET()
ThreadingHTTPServer(('127.0.0.1',int(sys.argv[1]) if len(sys.argv)>1 else 4307),Handler).serve_forever()
53 changes: 53 additions & 0 deletions docs/feed-card-visual-system/style-receipt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"cards": [
{
"badgeHeight": 26,
"ctaFont": "12px",
"ctaHeight": 36,
"padding": "20px",
"radius": "15px",
"titleFont": "18px"
},
{
"badgeHeight": 26,
"ctaFont": "12px",
"ctaHeight": 36,
"padding": "20px",
"radius": "15px",
"titleFont": "18px"
},
{
"badgeHeight": 26,
"ctaFont": "12px",
"ctaHeight": 36,
"padding": "20px",
"radius": "15px",
"titleFont": "18px"
},
{
"badgeHeight": 26,
"ctaFont": "12px",
"ctaHeight": 36,
"padding": "20px",
"radius": "15px",
"titleFont": "18px"
},
{
"badgeHeight": 26,
"ctaFont": "12px",
"ctaHeight": 36,
"padding": "20px",
"radius": "15px",
"titleFont": "18px"
},
{
"badgeHeight": 26,
"ctaFont": "12px",
"ctaHeight": 36,
"padding": "20px",
"radius": "15px",
"titleFont": "18px"
}
],
"nestedInteractive": 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
gap: 20px;
align-items: start;

@media (max-width: 800px) {
@media (max-width: 999px) {
grid-template-columns: minmax(0, 1fr);
}

Expand Down
120 changes: 120 additions & 0 deletions projects/social_platform/src/app/ui/pages/feed/feed.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/** @format */

import { TestBed } from "@angular/core/testing";
import { provideRouter } from "@angular/router";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { API_URL } from "@corelib";
import { FeedInfoService } from "@api/feed/facades/feed-info.service";
import { FeedUIInfoService } from "@api/feed/facades/ui/feed-ui-info.service";
import { FeedItem, FeedProject } from "@domain/feed/feed-item.model";
import { FeedNews } from "@domain/news/project-news.model";
import { Vacancy } from "@domain/vacancy/vacancy.model";
import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port";
import { success } from "@domain/shared/async-state";
import { FeedComponent } from "./feed.component";

describe("FeedComponent: порядок и представление карточек #372", () => {
const project: FeedProject = {
id: 1,
name: "Проект",
shortDescription: "Описание",
imageAddress: "",
industry: 1,
leader: 7,
viewsCount: 0,
};
const date = "2026-08-29T12:00:00Z";
const news = {
...FeedNews.default(),
id: 1,
name: "Источник",
text: "Первая строка. Подробности",
files: [],
contentObject: { id: 1, industry: 1 },
};
const initial: FeedItem[] = [
{
typeModel: "vacancy",
publishedAt: date,
content: {
id: 1,
project,
role: "QA",
description: "Задачи",
requiredSkills: [],
} as unknown as Vacancy,
},
{ typeModel: "news", publishedAt: date, content: news },
{ typeModel: "project", publishedAt: date, content: project },
{
typeModel: "news",
publishedAt: date,
content: { ...news, id: 2, contentObject: { id: 42, email: "fixture@example.test" } },
},
{ typeModel: "project", publishedAt: date, content: { ...project, id: 2 } },
{ typeModel: "project", publishedAt: date, content: { ...project, id: 3 } },
];

it("сохраняет шесть начальных карточек, ключи type:id и append в DOM без перестановки", async () => {
const ui = new FeedUIInfoService();
ui.applyInitializationFeedNewsEvent({ count: 12, next: "", previous: "", results: initial });
await TestBed.configureTestingModule({
imports: [FeedComponent, HttpClientTestingModule],
providers: [
provideRouter([]),
{ provide: API_URL, useValue: "" },
{ provide: IndustryRepositoryPort, useValue: { getOne: () => ({ name: "IT" }) } },
],
})
.overrideComponent(FeedComponent, {
set: {
providers: [
{ provide: FeedUIInfoService, useValue: ui },
{
provide: FeedInfoService,
useValue: { initializationFeedNews: vi.fn(), initScroll: vi.fn(), destroy: vi.fn() },
},
],
},
})
.compileComponents();
const fixture = TestBed.createComponent(FeedComponent);
fixture.detectChanges();
const root: HTMLElement = fixture.nativeElement;
const cards = () => Array.from(root.querySelectorAll<HTMLElement>(".page__item"));
const before = cards();
expect(before.map(card => card.dataset["key"])).toEqual([
"vacancy:1",
"news:1",
"project:1",
"news:2",
"project:2",
"project:3",
]);
expect(
before.slice(0, 4).map(card => card.querySelector(".feed-card__type")?.textContent?.trim()),
).toEqual(["Новая вакансия", "Новости проекта", "Новый проект", "Новости людей"]);
expect(
before.every(card => card.querySelector("time")?.getAttribute("datetime") === date),
).toBe(true);
expect(before[3].querySelector(".feed-card__cta")?.getAttribute("href")).toBe(
"/office/profile/42",
);

const next: FeedItem[] = [4, 5, 6, 7, 8, 9].map(id => ({
typeModel: "project",
content: { ...project, id },
publishedAt: date,
}));
ui.feedItems$.set(success([...initial, ...next]));
fixture.detectChanges();
expect(cards()).toHaveLength(12);
expect(cards().slice(0, 6)).toEqual(before);
expect(
cards()
.slice(6)
.map(card => card.dataset["key"]),
).toEqual(["project:4", "project:5", "project:6", "project:7", "project:8", "project:9"]);
expect(ui.perFetchTake()).toBe(6);
});
});
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
<!-- @format -->

<article class="card">
<div class="card__meta">
<span class="card__type">Новый проект</span>
<div class="card__meta feed-card__meta">
<span class="card__type feed-card__type">Новый проект</span>
@if (publishedAt()) {
<time class="card__date" [attr.datetime]="publishedAt()">
<time class="card__date feed-card__date" [attr.datetime]="publishedAt()">
{{ publishedAt() | dayjs: "format" : "DD.MM.YYYY" }}
</time>
}
</div>

<div class="card__heading">
<app-avatar [size]="64" [url]="feedItem().imageAddress"></app-avatar>
<h3 class="card__title">{{ feedItem().name }}</h3>
<div class="feed-card__source">
<app-avatar class="feed-card__avatar" [size]="40" [url]="feedItem().imageAddress"></app-avatar>
<div class="feed-card__source-text">
<span class="feed-card__eyebrow">Проект</span>
<a
class="feed-card__source-name"
[title]="feedItem().name"
[routerLink]="AppRoutes.projects.detail(feedItem().id)"
>{{ feedItem().name }}</a
>
</div>
</div>

@if (feedItem().shortDescription) {
<p class="card__description">{{ feedItem().shortDescription }}</p>
}

@if (industryRepository.getOne(feedItem().industry); as industry) {
<span class="card__industry">{{ industry.name }}</span>
}

<app-button
class="card__cta"
appearance="outline"
color="white"
size="medium"
customTypographyClass="text-body-12"
[routerLink]="AppRoutes.projects.detail(feedItem().id)"
<h3 class="card__title feed-card__title" [title]="feedItem().name">{{ feedItem().name }}</h3>
<p class="card__description feed-card__description">{{ feedItem().shortDescription }}</p>
<div class="feed-card__metadata">
@if (industryRepository.getOne(feedItem().industry); as industry) {
<span
class="card__industry feed-card__chip feed-card__chip--industry"
[title]="industry.name"
>{{ industry.name }}</span
>
}
</div>
<a class="card__cta feed-card__cta" [routerLink]="AppRoutes.projects.detail(feedItem().id)"
>Перейти в проект</a
>
Перейти в проект
</app-button>
</article>
Original file line number Diff line number Diff line change
@@ -1,74 +1,11 @@
.card {
display: flex;
flex-direction: column;
gap: 18px;
min-height: 100%;
padding: 24px;
color: var(--white);
background: var(--accent);
border-radius: var(--rounded-xl);

&__meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
}

&__type {
padding: 7px 12px;
font-size: 12px;
font-weight: 600;
color: var(--accent);
background: var(--white);
border-radius: var(--rounded-xxl);
}

&__date {
font-size: 12px;
}
@use "styles/feed-card";

&__heading {
display: flex;
gap: 16px;
align-items: center;
min-width: 0;
}

&__title {
font-size: 20px;
font-weight: 700;
line-height: 1.3;
overflow-wrap: anywhere;
}

/* stylelint-disable value-no-vendor-prefix */
&__description {
display: -webkit-box;
overflow: hidden;
font-size: 14px;
line-height: 1.5;
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
/* stylelint-enable value-no-vendor-prefix */

&__industry {
align-self: flex-start;
min-height: 32px;
padding: 7px 14px;
font-size: 14px;
font-weight: 600;
line-height: 18px;
color: var(--accent);
background: var(--white);
border-radius: var(--rounded-xxl);
}
.card {
--feed-background: var(--accent);
--feed-text: var(--white);
--feed-secondary: var(--white);
--feed-cta-text: var(--accent-dark);
--feed-cta-background: var(--white);

&__cta {
align-self: flex-start;
margin-top: auto;
}
@include feed-card.layout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ describe("NewProjectComponent", () => {
);
expect(element.querySelector(".card__industry")?.textContent?.trim()).toBe("Инженерия");
expect(element.querySelector(".card__cta")?.textContent).toContain("Перейти в проект");
expect(element.querySelector("a.card__cta")?.getAttribute("href")).toBe("/office/projects/5");
expect(element.querySelector("time")?.getAttribute("datetime")).toBe("2026-09-23T12:00:00Z");
});
});
Loading