Skip to content

Commit d3eda37

Browse files
committed
feat: add articles masonry grid, new article cards and external link support
- Add autotagmate and voice-to-text articles with vc.ru covers and i18n - Switch articles layout to CSS columns masonry grid with heading - Add external link icon and target=_blank for external article URLs - Sort articles by dateISO descending, add dateISO field - Update marketer link to internal /articles/berloga - Remove line-clamp from article card title and description - Add placeholder cards for layout testing
1 parent c947091 commit d3eda37

10 files changed

Lines changed: 242 additions & 18 deletions

File tree

15 KB
Loading
258 KB
Loading

public/images/vc-icon.svg

Lines changed: 1 addition & 0 deletions
Loading

public/js/app.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ const translations = {
120120
title: 'Роман Пуртов. Agentic Engineer\u00A0&\u00A0маркетолог\u00A0&\u00A0дизайнер.',
121121
metaDescription: 'Agentic Engineer\u00A0&\u00A0маркетолог\u00A0&\u00A0UX-UI дизайнер из\u00A0тайги (Томск, МСК+4). Делаю сайты и\u00A0пишу к\u00A0ним код\u00A0🌲',
122122
name: 'Роман Пуртов',
123-
mainDescription1: `<a href="https://github.com/baslie" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">Agentic Engineer</a>\u00A0&\u00A0<a href="https://telegra.ph/Kak-zapuskalas-Berloga-i-kak-zarabatyvalis-milliony-04-12" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">маркетолог</a>\u00A0&\u00A0UX-UI дизайнер из\u00A0тайги (Томск,\u00A0МСК+4).`,
123+
mainDescription1: `<a href="https://github.com/baslie" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">Agentic Engineer</a>\u00A0&\u00A0<a href="/articles/berloga" class="inline-link">маркетолог</a>\u00A0&\u00A0UX-UI дизайнер из\u00A0тайги (Томск,\u00A0МСК+4).`,
124124
mainDescription2: `${timeStringRu} делаю <a href="https://experts.tilda.cc/roman-purtow" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">сайты</a><br>и\u00A0пишу к\u00A0ним <a href="https://github.com/baslie/code-snippets" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">код</a>\u00A0🌲`,
125125
portfolio_title: 'Портфолио',
126126
portfolio_description: 'Сайты на Тильде',
@@ -149,13 +149,14 @@ const translations = {
149149
vc_description: 'Личный блог на vc.ru',
150150
tomsk_walk_title: 'Томская Прогулка',
151151
tomsk_walk_description: 'Гуляем с\u00A0друзьями и\u00A0не\u00A0только',
152-
article_back: 'На главную'
152+
article_back: 'На главную',
153+
articles_heading: 'Статьи и кейсы'
153154
},
154155
en: {
155156
title: 'Roman Purtov. Agentic Engineer\u00A0&\u00A0Marketer\u00A0&\u00A0Designer.',
156157
metaDescription: 'Agentic Engineer\u00A0&\u00A0Marketer\u00A0&\u00A0UX-UI Designer from Siberian taiga (Tomsk, UTC+7). Building websites and writing code\u00A0🌲',
157158
name: 'Roman Purtov',
158-
mainDescription1: `<a href="https://github.com/baslie" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">Agentic Engineer</a>\u00A0&\u00A0<a href="https://telegra.ph/Kak-zapuskalas-Berloga-i-kak-zarabatyvalis-milliony-04-12" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">Marketer</a>\u00A0&\u00A0UX-UI Designer from Siberian taiga (Tomsk,\u00A0UTC+7).`,
159+
mainDescription1: `<a href="https://github.com/baslie" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">Agentic Engineer</a>\u00A0&\u00A0<a href="/articles/berloga" class="inline-link">Marketer</a>\u00A0&\u00A0UX-UI Designer from Siberian taiga (Tomsk,\u00A0UTC+7).`,
159160
mainDescription2: `Building <a href="https://experts.tilda.cc/roman-purtow" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">websites</a> and writing <a href="https://github.com/baslie/code-snippets" target="_blank" rel="nofollow noopener noreferrer" class="inline-link">code</a> for them for ${timeStringEn}\u00A0🌲`,
160161
portfolio_title: 'Portfolio',
161162
portfolio_description: 'Tilda websites',
@@ -184,7 +185,8 @@ const translations = {
184185
vc_description: 'Personal blog on vc.ru',
185186
tomsk_walk_title: 'Tomsk Walk',
186187
tomsk_walk_description: 'Walking with friends and\u00A0more',
187-
article_back: 'Back to home'
188+
article_back: 'Back to home',
189+
articles_heading: 'Articles & Case Studies'
188190
}
189191
};
190192

public/js/i18n-articles.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
11
window.articleTranslations = {
2+
autotagmate: {
3+
ru: {
4+
title: 'Расширение для Chrome для структурирования промптов',
5+
description: 'Опубликовал AutoTagMate — бесплатное расширение для Chrome для автозакрытия тегов. Удобно использовать в том же ChatGPT, Claude AI и проч., чтобы выделять части промпта отдельными «тегами».',
6+
date: '2 марта 2025',
7+
readingTime: '2 мин'
8+
},
9+
en: {
10+
title: 'Chrome Extension for Structuring Prompts',
11+
description: 'Published AutoTagMate — a free Chrome extension for auto-closing tags. Handy for ChatGPT, Claude AI, etc., to wrap parts of your prompt in separate "tags".',
12+
date: 'March 2, 2025',
13+
readingTime: '2 min'
14+
}
15+
},
16+
'voice-to-text': {
17+
ru: {
18+
title: '— Говорите, говорите, я\u00A0вас внематочно слухаю!',
19+
description: 'В коммуникации удобство собеседника важно — факт 100%-й. Поэтому, если мои голосовые в Телеге длятся больше минуты, то перед отправкой собеседнику прогоняю их через нейросеть.',
20+
date: '2 марта 2025',
21+
readingTime: '2 мин'
22+
},
23+
en: {
24+
title: 'Speak, Speak, I\u2019m Listening!',
25+
description: 'Convenience matters in communication — 100% fact. So when my voice messages on Telegram go over a minute, I run them through AI before sending. Here\u2019s how.',
26+
date: 'March 2, 2025',
27+
readingTime: '2 min'
28+
}
29+
},
230
berloga: {
331
ru: {
432
title: 'Как запускалась «Берлога» и\u00A0как зарабатывались миллионы?',

src/components/ArticleCard.astro

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ interface Props {
88
const { article } = Astro.props;
99
---
1010

11-
<a href={article.href} class="article-card">
11+
<a
12+
href={article.href}
13+
class="article-card"
14+
{...(article.href.startsWith('http') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
15+
>
1216
<img
1317
src={article.image}
1418
alt={article.title}
@@ -24,5 +28,10 @@ const { article } = Astro.props;
2428
<span>&middot;</span>
2529
<span data-i18n={`article_${article.id}_date`}>{article.date}</span>
2630
</div>
31+
{article.href.startsWith('http') && (
32+
<svg class="article-card-external" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
33+
<path fill-rule="evenodd" d="M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z" clip-rule="evenodd" />
34+
</svg>
35+
)}
2736
</div>
2837
</a>

src/data/articles.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,131 @@ export interface ArticleData {
55
href: string;
66
image: string;
77
date: string;
8+
dateISO: string;
89
readingTime: string;
910
sourceIcon: string;
1011
sourceName: string;
1112
}
1213

1314
export const articles: ArticleData[] = [
15+
{
16+
id: 'autotagmate',
17+
title: 'Расширение для Chrome для структурирования промптов',
18+
description: 'Опубликовал AutoTagMate — бесплатное расширение для Chrome для автозакрытия тегов. Удобно использовать в том же ChatGPT, Claude AI и проч., чтобы выделять части промпта отдельными «тегами».',
19+
href: 'https://vc.ru/chatgpt/1841565-rasshirenie-dlya-chrome-dlya-strukturirovaniya-promptov',
20+
image: '/images/articles/autotagmate/cover.png',
21+
date: '2 марта 2025',
22+
dateISO: '2025-03-02',
23+
readingTime: '2 мин',
24+
sourceIcon: '/images/vc-icon.svg',
25+
sourceName: 'vc.ru',
26+
},
27+
{
28+
id: 'voice-to-text',
29+
title: '— Говорите, говорите, я вас внематочно слухаю!',
30+
description: 'В коммуникации удобство собеседника важно — факт 100%-й. Поэтому, если мои голосовые в Телеге длятся больше минуты, то перед отправкой собеседнику прогоняю их через нейросеть. Вот как я это делаю.',
31+
href: 'https://vc.ru/life/1822431-govorite-govorite-ya-vas-vnematochno-sluhayu',
32+
image: '/images/articles/voice-to-text/cover.jpg',
33+
date: '2 марта 2025',
34+
dateISO: '2025-03-02',
35+
readingTime: '2 мин',
36+
sourceIcon: '/images/vc-icon.svg',
37+
sourceName: 'vc.ru',
38+
},
1439
{
1540
id: 'berloga',
1641
title: 'Как запускалась «Берлога» и как зарабатывались миллионы?',
1742
description: 'Речь пойдёт о проекте, идея которого закралась мне в голову несколько лет назад. Сейчас это уже компания с 30 людьми в штате, оборотом 7-12 млн в месяц, выстроенным клиентским сервисом и довольно неплохим ассортиментом премиальных эко-продуктов.',
1843
href: '/articles/berloga',
1944
image: '/images/articles/berloga/berloga-store.png',
2045
date: '12 апреля 2022',
46+
dateISO: '2022-04-12',
2147
readingTime: '3 мин',
2248
sourceIcon: '/images/favicon.svg',
2349
sourceName: 'roman-purtow.ru',
2450
},
51+
{
52+
id: 'placeholder-1',
53+
title: 'Тестовая статья номер один',
54+
description: 'Это placeholder-карточка для проверки лейаута на мобильных устройствах. Будет удалена после тестирования.',
55+
href: '#',
56+
image: '/images/articles/berloga/berloga-store.png',
57+
date: '1 января 2025',
58+
dateISO: '2025-01-01',
59+
readingTime: '1 мин',
60+
sourceIcon: '/images/favicon.svg',
61+
sourceName: 'placeholder',
62+
},
63+
{
64+
id: 'placeholder-2',
65+
title: 'Вторая тестовая карточка',
66+
description: 'Краткое описание для второй placeholder-карточки.',
67+
href: '#',
68+
image: '/images/articles/berloga/berloga-store.png',
69+
date: '15 января 2025',
70+
dateISO: '2025-01-15',
71+
readingTime: '3 мин',
72+
sourceIcon: '/images/favicon.svg',
73+
sourceName: 'placeholder',
74+
},
75+
{
76+
id: 'placeholder-3',
77+
title: 'Третья карточка с длинным заголовком для проверки переноса текста',
78+
description: 'Описание средней длины, чтобы проверить как карточка выглядит с разным количеством текста в описании.',
79+
href: '#',
80+
image: '/images/articles/berloga/berloga-store.png',
81+
date: '1 февраля 2025',
82+
dateISO: '2025-02-01',
83+
readingTime: '5 мин',
84+
sourceIcon: '/images/favicon.svg',
85+
sourceName: 'placeholder',
86+
},
87+
{
88+
id: 'placeholder-4',
89+
title: 'Четвёртая карточка',
90+
description: 'Короткое описание.',
91+
href: '#',
92+
image: '/images/articles/berloga/berloga-store.png',
93+
date: '10 февраля 2025',
94+
dateISO: '2025-02-10',
95+
readingTime: '2 мин',
96+
sourceIcon: '/images/favicon.svg',
97+
sourceName: 'placeholder',
98+
},
99+
{
100+
id: 'placeholder-5',
101+
title: 'Пятая тестовая статья',
102+
description: 'Ещё одна placeholder-карточка для тестирования сетки на разных экранах.',
103+
href: '#',
104+
image: '/images/articles/berloga/berloga-store.png',
105+
date: '20 февраля 2025',
106+
dateISO: '2025-02-20',
107+
readingTime: '4 мин',
108+
sourceIcon: '/images/favicon.svg',
109+
sourceName: 'placeholder',
110+
},
111+
{
112+
id: 'placeholder-6',
113+
title: 'Шестая карточка',
114+
description: 'Проверяем поведение лейаута при большом количестве карточек в списке статей.',
115+
href: '#',
116+
image: '/images/articles/berloga/berloga-store.png',
117+
date: '25 февраля 2025',
118+
dateISO: '2025-02-25',
119+
readingTime: '6 мин',
120+
sourceIcon: '/images/favicon.svg',
121+
sourceName: 'placeholder',
122+
},
123+
{
124+
id: 'placeholder-7',
125+
title: 'Седьмая и последняя тестовая карточка',
126+
description: 'Финальная placeholder-карточка. После проверки все семь будут удалены.',
127+
href: '#',
128+
image: '/images/articles/berloga/berloga-store.png',
129+
date: '28 февраля 2025',
130+
dateISO: '2025-02-28',
131+
readingTime: '1 мин',
132+
sourceIcon: '/images/favicon.svg',
133+
sourceName: 'placeholder',
134+
},
25135
];

src/pages/articles/berloga.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ import Article from '../../layouts/Article.astro';
204204
color: var(--color-text-light);
205205
}
206206

207+
.article-body blockquote p:last-child {
208+
margin-bottom: 0;
209+
}
210+
207211
.article-body ol {
208212
list-style: none;
209213
counter-reset: article-list;

src/pages/index.astro

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import CategoryBadge from '../components/CategoryBadge.astro';
77
import { pairedCards, cards } from '../data/cards';
88
import ArticleCard from '../components/ArticleCard.astro';
99
import { articles } from '../data/articles';
10+
11+
const sortedArticles = [...articles].sort((a, b) => b.dateISO.localeCompare(a.dateISO));
1012
---
1113

1214
<Base
@@ -45,11 +47,11 @@ import { articles } from '../data/articles';
4547

4648
<!-- Articles -->
4749
<section class="articles-section mt-3 md:mt-4 xl:mt-5 2xl:mt-6 lg:flex lg:justify-center lg:px-12 xl:px-16 2xl:px-20">
48-
<div class="articles-grid grid grid-cols-1 gap-3 px-3 pb-3
49-
md:grid-cols-2 md:gap-4 md:px-4 md:pb-4
50-
lg:grid-cols-3 lg:p-0 lg:w-full lg:max-w-[90rem]
51-
xl:gap-5 2xl:gap-6">
52-
{articles.map(article => <ArticleCard article={article} />)}
50+
<div class="px-3 pb-3 md:px-4 md:pb-4 lg:p-0 lg:w-full lg:max-w-[90rem]">
51+
<h2 class="articles-heading" data-i18n="articles_heading">Статьи и кейсы</h2>
52+
<div class="articles-grid">
53+
{sortedArticles.map(article => <ArticleCard article={article} />)}
54+
</div>
5355
</div>
5456
</section>
5557
</Base>

src/styles/input.css

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,67 @@ html.lenis {
844844
}
845845
}
846846

847+
/* Articles heading */
848+
.articles-heading {
849+
font-size: 1.5rem;
850+
font-weight: 700;
851+
color: var(--color-text-light);
852+
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
853+
margin-bottom: 0.75rem;
854+
}
855+
856+
@media (min-width: 768px) {
857+
.articles-heading {
858+
font-size: 1.75rem;
859+
margin-bottom: 1rem;
860+
}
861+
}
862+
863+
/* Articles masonry grid */
864+
.articles-grid {
865+
columns: 1;
866+
column-gap: 0.75rem;
867+
}
868+
869+
.articles-grid > * {
870+
break-inside: avoid;
871+
margin-bottom: 0.75rem;
872+
}
873+
874+
@media (min-width: 768px) {
875+
.articles-grid {
876+
columns: 2;
877+
column-gap: 1rem;
878+
}
879+
.articles-grid > * {
880+
margin-bottom: 1rem;
881+
}
882+
}
883+
884+
@media (min-width: 1024px) {
885+
.articles-grid {
886+
columns: 3;
887+
}
888+
}
889+
890+
@media (min-width: 1280px) {
891+
.articles-grid {
892+
column-gap: 1.25rem;
893+
}
894+
.articles-grid > * {
895+
margin-bottom: 1.25rem;
896+
}
897+
}
898+
899+
@media (min-width: 1536px) {
900+
.articles-grid {
901+
column-gap: 1.5rem;
902+
}
903+
.articles-grid > * {
904+
margin-bottom: 1.5rem;
905+
}
906+
}
907+
847908
.article-card {
848909
position: relative;
849910
border-radius: var(--radius);
@@ -881,17 +942,28 @@ html.lenis {
881942
}
882943

883944
.article-card-body {
945+
position: relative;
884946
padding: 1.25rem;
885947
}
886948

949+
.article-card-external {
950+
position: absolute;
951+
bottom: 1.25rem;
952+
right: 1.25rem;
953+
width: 14px;
954+
height: 14px;
955+
color: var(--color-text-muted);
956+
opacity: 0.5;
957+
}
958+
959+
.dark .article-card-external {
960+
color: rgba(255, 255, 255, 0.4);
961+
}
962+
887963
.article-card-title {
888964
font-weight: 600;
889965
font-size: 1.0625rem;
890966
color: var(--color-text);
891-
display: -webkit-box;
892-
-webkit-line-clamp: 2;
893-
-webkit-box-orient: vertical;
894-
overflow: hidden;
895967
margin-bottom: 0.5rem;
896968
}
897969

@@ -902,10 +974,6 @@ html.lenis {
902974
.article-card-desc {
903975
font-size: 0.875rem;
904976
color: var(--color-text-muted);
905-
display: -webkit-box;
906-
-webkit-line-clamp: 3;
907-
-webkit-box-orient: vertical;
908-
overflow: hidden;
909977
margin-bottom: 0.75rem;
910978
line-height: 1.5;
911979
}

0 commit comments

Comments
 (0)