Skip to content

Commit 655b334

Browse files
Kosthiclaude
andcommitted
feat: enhance visual polish and blog experience
- Add CSS variables for typography, easing, and durations - Add pulsing glow effect behind hero logo - Implement staggered text reveal with blur animation - Add shimmer animation to CTA button and floating hero panel - Enhance cards with inner glow, gradient borders, layered shadows - Improve prose typography with 70ch max-width and styled elements - Add syntax highlighting color variables for code blocks - Enhance PostCard with hero images, reading time, animated arrows - Improve TableOfContents with scroll indicator and active states - Create RelatedPosts component with relevance scoring algorithm - Add prefers-reduced-motion support throughout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9c26b81 commit 655b334

File tree

5 files changed

+1013
-16
lines changed

5 files changed

+1013
-16
lines changed

src/components/blog/PostCard.astro

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,40 @@ const formattedDate = post.data.pubDate.toLocaleDateString(
1919
);
2020
2121
const categoryLabel = t(`blog.categories.${post.data.category}`);
22+
23+
// Calculate reading time (roughly 200 words per minute for CJK, 250 for English)
24+
const content = post.body || '';
25+
const wordCount = content.length;
26+
const wordsPerMinute = lang === 'en' ? 250 : 200;
27+
const readingTime = Math.max(1, Math.ceil(wordCount / wordsPerMinute));
28+
const readingTimeLabel = lang === 'zh' ? `${readingTime} 分钟` : lang === 'ja' ? `${readingTime} 分` : `${readingTime} min`;
29+
30+
// Check for hero image
31+
const heroImage = post.data.heroImage;
2232
---
2333

24-
<article class="post-card">
34+
<article class={`post-card ${heroImage ? 'has-hero' : ''}`}>
2535
<a href={`/${lang}/blog/${slug}/`} class="post-card-link">
36+
{heroImage && (
37+
<div class="post-card-image">
38+
<img src={heroImage} alt={post.data.title} loading="lazy" />
39+
<div class="post-card-image-overlay"></div>
40+
</div>
41+
)}
42+
2643
<div class="post-card-content">
2744
<div class="post-card-meta">
2845
<span class="post-card-category">{categoryLabel}</span>
46+
<span class="post-card-divider">·</span>
2947
<span class="post-card-date">{formattedDate}</span>
48+
<span class="post-card-divider">·</span>
49+
<span class="post-card-reading-time">
50+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
51+
<circle cx="12" cy="12" r="10"></circle>
52+
<polyline points="12 6 12 12 16 14"></polyline>
53+
</svg>
54+
{readingTimeLabel}
55+
</span>
3056
</div>
3157

3258
<h2 class="post-card-title">{post.data.title}</h2>
@@ -41,7 +67,12 @@ const categoryLabel = t(`blog.categories.${post.data.category}`);
4167
</div>
4268
)}
4369

44-
<span class="post-card-read-more">{t('blog.readMore')} →</span>
70+
<span class="post-card-read-more">
71+
{t('blog.readMore')}
72+
<svg class="read-more-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
73+
<path d="M5 12h14M12 5l7 7-7 7"/>
74+
</svg>
75+
</span>
4576
</div>
4677
</a>
4778
</article>
@@ -51,28 +82,89 @@ const categoryLabel = t(`blog.categories.${post.data.category}`);
5182
background: rgba(10, 14, 32, 0.62);
5283
border: 1px solid rgba(240, 246, 255, 0.12);
5384
border-radius: 18px;
54-
transition: all 0.3s ease;
85+
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
5586
overflow: hidden;
87+
position: relative;
88+
}
89+
90+
.post-card::before {
91+
content: "";
92+
position: absolute;
93+
inset: -1px;
94+
background: linear-gradient(
95+
135deg,
96+
rgba(34, 211, 238, 0.4),
97+
rgba(168, 85, 247, 0.3),
98+
rgba(96, 165, 250, 0.3)
99+
);
100+
border-radius: inherit;
101+
z-index: -1;
102+
opacity: 0;
103+
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
56104
}
57105

58106
.post-card:hover {
59-
transform: translateY(-4px);
107+
transform: translateY(-6px);
60108
border-color: rgba(34, 211, 238, 0.3);
61-
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
109+
box-shadow:
110+
0 4px 6px rgba(0, 0, 0, 0.1),
111+
0 10px 20px rgba(0, 0, 0, 0.15),
112+
0 20px 50px rgba(34, 211, 238, 0.1);
113+
}
114+
115+
.post-card:hover::before {
116+
opacity: 1;
62117
}
63118

64119
.post-card-link {
65120
display: block;
66121
text-decoration: none;
122+
}
123+
124+
/* Hero Image Styles */
125+
.post-card-image {
126+
position: relative;
127+
height: 180px;
128+
overflow: hidden;
129+
}
130+
131+
.post-card-image img {
132+
width: 100%;
133+
height: 100%;
134+
object-fit: cover;
135+
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
136+
}
137+
138+
.post-card:hover .post-card-image img {
139+
transform: scale(1.08);
140+
}
141+
142+
.post-card-image-overlay {
143+
position: absolute;
144+
inset: 0;
145+
background: linear-gradient(
146+
to bottom,
147+
transparent 0%,
148+
rgba(10, 14, 32, 0.4) 60%,
149+
rgba(10, 14, 32, 0.9) 100%
150+
);
151+
}
152+
153+
.post-card-content {
67154
padding: 1.5rem;
68155
}
69156

157+
.has-hero .post-card-content {
158+
padding-top: 1rem;
159+
}
160+
70161
.post-card-meta {
71162
display: flex;
72163
align-items: center;
73-
gap: 1rem;
164+
gap: 0.5rem;
74165
margin-bottom: 0.75rem;
75166
font-size: 0.85rem;
167+
flex-wrap: wrap;
76168
}
77169

78170
.post-card-category {
@@ -82,17 +174,32 @@ const categoryLabel = t(`blog.categories.${post.data.category}`);
82174
letter-spacing: 0.05em;
83175
}
84176

177+
.post-card-divider {
178+
color: var(--text-3);
179+
}
180+
85181
.post-card-date {
86182
color: var(--text-3);
87183
}
88184

185+
.post-card-reading-time {
186+
display: inline-flex;
187+
align-items: center;
188+
gap: 0.35rem;
189+
color: var(--text-3);
190+
}
191+
192+
.post-card-reading-time svg {
193+
opacity: 0.7;
194+
}
195+
89196
.post-card-title {
90197
font-size: 1.3rem;
91198
font-weight: 700;
92199
color: var(--text);
93200
margin-bottom: 0.75rem;
94201
line-height: 1.3;
95-
transition: color 0.2s ease;
202+
transition: color 0.3s cubic-bezier(0.16, 1, 0.3, 1);
96203
}
97204

98205
.post-card:hover .post-card-title {
@@ -118,19 +225,34 @@ const categoryLabel = t(`blog.categories.${post.data.category}`);
118225
}
119226

120227
.post-card-tag {
121-
color: var(--text-3);
228+
background: rgba(168, 85, 247, 0.1);
229+
color: var(--c2);
122230
font-size: 0.8rem;
231+
padding: 0.25rem 0.75rem;
232+
border-radius: 999px;
233+
border: 1px solid rgba(168, 85, 247, 0.2);
234+
transition: all 0.3s ease;
235+
}
236+
237+
.post-card:hover .post-card-tag {
238+
background: rgba(168, 85, 247, 0.15);
239+
border-color: rgba(168, 85, 247, 0.3);
123240
}
124241

125242
.post-card-read-more {
243+
display: inline-flex;
244+
align-items: center;
245+
gap: 0.5rem;
126246
color: var(--c1);
127247
font-size: 0.9rem;
128248
font-weight: 600;
129-
transition: transform 0.2s ease;
130-
display: inline-block;
131249
}
132250

133-
.post-card:hover .post-card-read-more {
251+
.read-more-arrow {
252+
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
253+
}
254+
255+
.post-card:hover .read-more-arrow {
134256
transform: translateX(4px);
135257
}
136258
</style>

0 commit comments

Comments
 (0)