Skip to content

Commit cc3903c

Browse files
Добавил ридинг прогресс
1 parent 81d78a9 commit cc3903c

File tree

3 files changed

+155
-34
lines changed

3 files changed

+155
-34
lines changed

src/layouts/BaseLayout.astro

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import Header from './Header.astro'
66
interface Props {
77
title?: string | undefined
88
description?: string | undefined
9+
showReadingProgress?: boolean
910
}
1011
11-
const { title, description } = Astro.props
12+
const { title, description, showReadingProgress = false } = Astro.props
1213
---
1314

1415
<html lang="en">
@@ -20,6 +21,15 @@ const { title, description } = Astro.props
2021
class="flex flex-col overflow-hidden bg-background font-sans antialiased"
2122
style="height: 100dvh"
2223
>
24+
{showReadingProgress && (
25+
<div class="fixed inset-x-0 top-0 z-[60] h-[3px] bg-transparent">
26+
<div
27+
id="reading-progress"
28+
class="h-full w-0 bg-primary transition-[width] duration-150 ease-out"
29+
>
30+
</div>
31+
</div>
32+
)}
2333
<div class="pointer-events-none fixed inset-0 -z-50">
2434
<div
2535
class="bg-grid-pattern absolute inset-0 opacity-[0.03] dark:opacity-[0.04]"
@@ -33,7 +43,7 @@ const { title, description } = Astro.props
3343
</div>
3444

3545
<Header />
36-
<div class="min-h-0 w-full flex-1 overflow-y-auto">
46+
<div class="min-h-0 w-full flex-1 overflow-y-auto" id="content-scroll-container">
3747
<slot />
3848
</div>
3949

src/pages/blog/[slug].astro

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,27 @@ type Props = { entry: CollectionEntry<'blog'> }
1616
const { entry } = Astro.props as Props
1717
const { Content, headings } = await entry.render()
1818
const { title, description, pubDate, tags } = entry.data
19+
20+
const AVERAGE_READING_WPM = 220
21+
const wordCount = entry.body?.split(/\s+/).length ?? 0
22+
const readingTimeMinutes =
23+
wordCount > 0 ? Math.max(1, Math.ceil(wordCount / AVERAGE_READING_WPM)) : 1
24+
25+
const allPosts = await getCollection('blog')
26+
const relatedPosts = allPosts
27+
.filter((post) => post.id !== entry.id)
28+
.filter((post) => {
29+
if (!tags || !post.data.tags) return false
30+
return post.data.tags.some((tag) => tags.includes(tag))
31+
})
32+
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
33+
.slice(0, 3)
1934
---
2035

2136
<BaseLayout
2237
title={`${title} — Блог | Eldar Muhamethanov`}
2338
description={description}
39+
showReadingProgress={true}
2440
>
2541
<main
2642
class="relative flex min-h-[80vh] flex-auto flex-col"
@@ -44,18 +60,19 @@ const { title, description, pubDate, tags } = entry.data
4460
{title}
4561
</h1>
4662
<div class="mt-2 flex flex-wrap items-center gap-3">
47-
<time
48-
datetime={pubDate.toISOString()}
49-
class="block text-sm text-muted-foreground"
50-
>
51-
{
52-
pubDate.toLocaleDateString('ru-RU', {
53-
day: 'numeric',
54-
month: 'long',
55-
year: 'numeric',
56-
})
57-
}
58-
</time>
63+
<div class="flex items-center gap-2 text-sm text-muted-foreground">
64+
<time datetime={pubDate.toISOString()}>
65+
{
66+
pubDate.toLocaleDateString('ru-RU', {
67+
day: 'numeric',
68+
month: 'long',
69+
year: 'numeric',
70+
})
71+
}
72+
</time>
73+
<span aria-hidden="true">•</span>
74+
<span>{readingTimeMinutes} мин чтения</span>
75+
</div>
5976
{tags && tags.length > 0 && (
6077
<ul class="flex flex-wrap gap-1.5">
6178
{tags.map((tag) => (
@@ -70,20 +87,55 @@ const { title, description, pubDate, tags } = entry.data
7087
</header>
7188
<div
7289
class="prose prose-neutral dark:prose-invert prose-headings:font-bold prose-pre:rounded-xl prose-code:rounded prose-code:px-1 prose-code:py-0.5 prose-code:before:content-none prose-code:after:content-none max-w-none"
90+
data-article-content
7391
>
7492
<Content />
7593
</div>
7694
</BlogArticleLayout>
7795
</div>
7896
</div>
7997
<BottomLayout size="lg">
80-
<div class="border-t border-border pt-8">
81-
<a
82-
href="/blog"
83-
class="text-sm font-medium text-primary hover:underline"
84-
>
85-
← Все статьи
86-
</a>
98+
<div class="border-t border-border pt-8 space-y-8">
99+
{relatedPosts.length > 0 && (
100+
<section aria-labelledby="related-posts-heading">
101+
<h2
102+
id="related-posts-heading"
103+
class="text-sm font-semibold uppercase tracking-wide text-muted-foreground mb-3"
104+
>
105+
Похожие статьи
106+
</h2>
107+
<ul class="grid gap-4 md:grid-cols-3">
108+
{relatedPosts.map((post) => (
109+
<li>
110+
<a
111+
href={`/blog/${post.id.replace(/\.(mdx?|md)$/, '')}`}
112+
class="block rounded-xl border border-border/80 bg-card/40 px-3 py-3 text-sm hover:border-primary/30 hover:bg-card/70 transition-colors"
113+
>
114+
<div class="text-[11px] uppercase tracking-wide text-muted-foreground mb-1">
115+
{post.data.pubDate.toLocaleDateString('ru-RU', {
116+
day: 'numeric',
117+
month: 'short',
118+
year: 'numeric',
119+
})}
120+
</div>
121+
<div class="font-medium text-foreground line-clamp-2">
122+
{post.data.title}
123+
</div>
124+
</a>
125+
</li>
126+
))}
127+
</ul>
128+
</section>
129+
)}
130+
131+
<div>
132+
<a
133+
href="/blog"
134+
class="text-sm font-medium text-primary hover:underline"
135+
>
136+
← Все статьи
137+
</a>
138+
</div>
87139
</div>
88140
</BottomLayout>
89141
</main>
@@ -95,4 +147,52 @@ const { title, description, pubDate, tags } = entry.data
95147
overflow-y: hidden;
96148
max-width: 85vw;
97149
}
150+
151+
#reading-progress {
152+
transform-origin: left center;
153+
border-radius: 0;
154+
}
98155
</style>
156+
157+
<script is:inline>
158+
;(function () {
159+
function attachReadingProgress() {
160+
const progressBar = document.getElementById('reading-progress')
161+
const contentScrollContainer = document.getElementById('content-scroll-container')
162+
163+
if (!progressBar || !contentScrollContainer) {
164+
return
165+
}
166+
167+
if (window.__readingProgressDetach) {
168+
window.__readingProgressDetach()
169+
}
170+
171+
const updateProgress = () => {
172+
const maxScroll =
173+
contentScrollContainer.scrollHeight - contentScrollContainer.clientHeight
174+
175+
if (maxScroll <= 0) {
176+
progressBar.style.width = '0%'
177+
return
178+
}
179+
180+
const current = contentScrollContainer.scrollTop
181+
const progress = Math.min(1, Math.max(0, current / maxScroll))
182+
progressBar.style.width = `${progress * 100}%`
183+
}
184+
185+
contentScrollContainer.addEventListener('scroll', updateProgress, { passive: true })
186+
window.addEventListener('resize', updateProgress)
187+
updateProgress()
188+
189+
window.__readingProgressDetach = () => {
190+
contentScrollContainer.removeEventListener('scroll', updateProgress)
191+
window.removeEventListener('resize', updateProgress)
192+
}
193+
}
194+
195+
attachReadingProgress()
196+
document.addEventListener('astro:after-swap', attachReadingProgress)
197+
})()
198+
</script>

src/pages/blog/index.astro

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ import PageTitle from '../../components/common/PageTitle.astro'
77
88
const BLOG_TITLE = 'Блог'
99
const BLOG_DESCRIPTION = 'Заметки о разработке, инструментах и опыте'
10+
const AVERAGE_READING_WPM = 220
1011
11-
const posts = (await getCollection('blog')).sort(
12-
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()
13-
)
12+
const posts = (await getCollection('blog'))
13+
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
14+
.map((entry) => {
15+
const words = entry.body?.split(/\s+/).length ?? 0
16+
const readingTimeMinutes =
17+
words > 0 ? Math.max(1, Math.ceil(words / AVERAGE_READING_WPM)) : 1
18+
19+
return {
20+
...entry,
21+
readingTimeMinutes,
22+
}
23+
})
1424
---
1525

1626
<BaseLayout title={`${BLOG_TITLE} — Eldar Muhamethanov`} description={BLOG_DESCRIPTION}>
@@ -28,16 +38,17 @@ const posts = (await getCollection('blog')).sort(
2838
href={`/blog/${entry.id.replace(/\.(mdx?|md)$/, '')}`}
2939
class="group block rounded-2xl border border-border/80 bg-card/50 p-6 transition-all duration-300 hover:border-primary/20 hover:shadow-glow-sm"
3040
>
31-
<time
32-
datetime={entry.data.pubDate.toISOString()}
33-
class="text-sm font-medium text-muted-foreground"
34-
>
35-
{entry.data.pubDate.toLocaleDateString('ru-RU', {
36-
day: 'numeric',
37-
month: 'long',
38-
year: 'numeric',
39-
})}
40-
</time>
41+
<div class="flex items-center gap-2 text-sm font-medium text-muted-foreground">
42+
<time datetime={entry.data.pubDate.toISOString()}>
43+
{entry.data.pubDate.toLocaleDateString('ru-RU', {
44+
day: 'numeric',
45+
month: 'long',
46+
year: 'numeric',
47+
})}
48+
</time>
49+
<span aria-hidden="true">•</span>
50+
<span>{entry.readingTimeMinutes} мин чтения</span>
51+
</div>
4152
<h2 class="mt-2 text-xl font-semibold tracking-tight text-foreground group-hover:text-primary transition-colors">
4253
{entry.data.title}
4354
</h2>

0 commit comments

Comments
 (0)