@astro-minimax/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/package.json +41 -0
- package/src/assets/icons/IconArchive.svg +1 -0
- package/src/assets/icons/IconArrowLeft.svg +1 -0
- package/src/assets/icons/IconArrowNarrowUp.svg +1 -0
- package/src/assets/icons/IconArrowRight.svg +1 -0
- package/src/assets/icons/IconArticle.svg +1 -0
- package/src/assets/icons/IconBrandX.svg +1 -0
- package/src/assets/icons/IconCalendar.svg +1 -0
- package/src/assets/icons/IconChevronLeft.svg +1 -0
- package/src/assets/icons/IconChevronRight.svg +1 -0
- package/src/assets/icons/IconEdit.svg +1 -0
- package/src/assets/icons/IconFacebook.svg +1 -0
- package/src/assets/icons/IconGitHub.svg +1 -0
- package/src/assets/icons/IconHash.svg +1 -0
- package/src/assets/icons/IconHome.svg +1 -0
- package/src/assets/icons/IconLinkedin.svg +1 -0
- package/src/assets/icons/IconMail.svg +1 -0
- package/src/assets/icons/IconMenuDeep.svg +1 -0
- package/src/assets/icons/IconMoon.svg +1 -0
- package/src/assets/icons/IconPinterest.svg +1 -0
- package/src/assets/icons/IconProject.svg +1 -0
- package/src/assets/icons/IconRss.svg +1 -0
- package/src/assets/icons/IconSearch.svg +1 -0
- package/src/assets/icons/IconSeries.svg +1 -0
- package/src/assets/icons/IconSunHigh.svg +1 -0
- package/src/assets/icons/IconTag.svg +1 -0
- package/src/assets/icons/IconTelegram.svg +1 -0
- package/src/assets/icons/IconUser.svg +1 -0
- package/src/assets/icons/IconWhatsapp.svg +1 -0
- package/src/assets/icons/IconX.svg +1 -0
- package/src/components/ai/AIChatWidget.astro +377 -0
- package/src/components/blog/Comments.astro +527 -0
- package/src/components/blog/Copyright.astro +152 -0
- package/src/components/blog/EditPost.astro +59 -0
- package/src/components/blog/FloatingTOC.astro +260 -0
- package/src/components/blog/InlineTOC.astro +223 -0
- package/src/components/blog/PostActions.astro +306 -0
- package/src/components/blog/RelatedPosts.astro +60 -0
- package/src/components/blog/SeriesNav.astro +176 -0
- package/src/components/blog/ShareLinks.astro +26 -0
- package/src/components/nav/BackButton.astro +37 -0
- package/src/components/nav/BackToTopButton.astro +223 -0
- package/src/components/nav/Breadcrumb.astro +57 -0
- package/src/components/nav/FloatingActions.astro +206 -0
- package/src/components/nav/Footer.astro +107 -0
- package/src/components/nav/Header.astro +252 -0
- package/src/components/nav/Pagination.astro +45 -0
- package/src/components/social/Socials.astro +19 -0
- package/src/components/social/Sponsors.astro +34 -0
- package/src/components/social/Sponsorship.astro +44 -0
- package/src/components/ui/Alert.astro +28 -0
- package/src/components/ui/Card.astro +206 -0
- package/src/components/ui/Collapse.astro +82 -0
- package/src/components/ui/ColorPreview.astro +29 -0
- package/src/components/ui/Datetime.astro +61 -0
- package/src/components/ui/GithubCard.astro +191 -0
- package/src/components/ui/LinkButton.astro +21 -0
- package/src/components/ui/Tag.astro +37 -0
- package/src/components/ui/TagCloud.astro +69 -0
- package/src/components/ui/Timeline.astro +39 -0
- package/src/layouts/AboutLayout.astro +24 -0
- package/src/layouts/Layout.astro +329 -0
- package/src/layouts/Main.astro +42 -0
- package/src/layouts/PostDetails.astro +445 -0
- package/src/plugins/rehype-autolink-headings.ts +46 -0
- package/src/plugins/rehype-external-links.ts +35 -0
- package/src/plugins/rehype-table-scroll.ts +35 -0
- package/src/plugins/remark-add-zoomable.ts +28 -0
- package/src/plugins/remark-reading-time.ts +18 -0
- package/src/plugins/shiki-transformers.ts +212 -0
- package/src/scripts/lightbox.ts +63 -0
- package/src/scripts/reading-position.ts +56 -0
- package/src/scripts/theme-utils.ts +19 -0
- package/src/scripts/theme.ts +179 -0
- package/src/scripts/web-vitals.ts +96 -0
- package/src/styles/code-blocks.css +194 -0
- package/src/styles/components.css +252 -0
- package/src/styles/global.css +403 -0
- package/src/styles/typography.css +149 -0
- package/src/types.ts +89 -0
- package/src/utils/generateOgImages.ts +38 -0
- package/src/utils/getCategoryPath.ts +23 -0
- package/src/utils/getPath.ts +52 -0
- package/src/utils/getPostsByCategory.ts +17 -0
- package/src/utils/getPostsByGroupCondition.ts +25 -0
- package/src/utils/getPostsByLang.ts +27 -0
- package/src/utils/getPostsByTag.ts +10 -0
- package/src/utils/getReadingTime.ts +33 -0
- package/src/utils/getRelatedPosts.ts +59 -0
- package/src/utils/getSeriesData.ts +57 -0
- package/src/utils/getSortedPosts.ts +18 -0
- package/src/utils/getTagsWithCount.ts +38 -0
- package/src/utils/getUniqueCategories.ts +81 -0
- package/src/utils/getUniqueTags.ts +23 -0
- package/src/utils/i18n.ts +249 -0
- package/src/utils/loadGoogleFont.ts +38 -0
- package/src/utils/og-templates/post.js +229 -0
- package/src/utils/og-templates/site.js +128 -0
- package/src/utils/pathUtils.ts +17 -0
- package/src/utils/postFilter.ts +11 -0
- package/src/utils/slugify.ts +23 -0
- package/src/utils/toc.ts +27 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
---
|
|
2
|
+
// PostActions - Like and Bookmark functionality
|
|
3
|
+
// Uses localStorage for persistence on static sites
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<div
|
|
7
|
+
class="post-actions mt-6 flex flex-wrap items-center justify-center gap-4"
|
|
8
|
+
data-pagefind-ignore
|
|
9
|
+
>
|
|
10
|
+
{/* Like Button */}
|
|
11
|
+
<button
|
|
12
|
+
id="like-btn"
|
|
13
|
+
class="action-btn group text-muted-foreground flex items-center gap-2 rounded-full border border-border bg-background/50 px-4 py-2 text-sm transition-all hover:border-accent hover:text-accent"
|
|
14
|
+
aria-label="Like this post"
|
|
15
|
+
aria-pressed="false"
|
|
16
|
+
>
|
|
17
|
+
<svg
|
|
18
|
+
class="size-5 transition-transform group-hover:scale-110"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="none"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
stroke-width="2"
|
|
23
|
+
stroke-linecap="round"
|
|
24
|
+
stroke-linejoin="round"
|
|
25
|
+
>
|
|
26
|
+
<path
|
|
27
|
+
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
|
|
28
|
+
></path>
|
|
29
|
+
</svg>
|
|
30
|
+
<span class="like-text">喜欢</span>
|
|
31
|
+
<span id="like-count" class="rounded-full bg-muted px-2 text-xs">0</span>
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
{/* Bookmark Button */}
|
|
35
|
+
<button
|
|
36
|
+
id="bookmark-btn"
|
|
37
|
+
class="action-btn group text-muted-foreground flex items-center gap-2 rounded-full border border-border bg-background/50 px-4 py-2 text-sm transition-all hover:border-accent hover:text-accent"
|
|
38
|
+
aria-label="Bookmark this post"
|
|
39
|
+
aria-pressed="false"
|
|
40
|
+
>
|
|
41
|
+
<svg
|
|
42
|
+
class="size-5 transition-transform group-hover:scale-110"
|
|
43
|
+
viewBox="0 0 24 24"
|
|
44
|
+
fill="none"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
stroke-width="2"
|
|
47
|
+
stroke-linecap="round"
|
|
48
|
+
stroke-linejoin="round"
|
|
49
|
+
>
|
|
50
|
+
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
|
|
51
|
+
</svg>
|
|
52
|
+
<span class="bookmark-text">收藏</span>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<script is:inline>
|
|
57
|
+
const LIKES_KEY = "blog_likes";
|
|
58
|
+
const BOOKMARKS_KEY = "blog_bookmarks";
|
|
59
|
+
|
|
60
|
+
function getPostId() {
|
|
61
|
+
return window.location.pathname.replace(/\/$/, "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getLikes() {
|
|
65
|
+
try {
|
|
66
|
+
const stored = localStorage.getItem(LIKES_KEY);
|
|
67
|
+
return stored ? JSON.parse(stored) : {};
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getBookmarks() {
|
|
74
|
+
try {
|
|
75
|
+
const stored = localStorage.getItem(BOOKMARKS_KEY);
|
|
76
|
+
return stored ? JSON.parse(stored) : [];
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function saveLikes(likes) {
|
|
83
|
+
localStorage.setItem(LIKES_KEY, JSON.stringify(likes));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function saveBookmarks(bookmarks) {
|
|
87
|
+
localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isLiked(postId) {
|
|
91
|
+
const likedPosts = localStorage.getItem("blog_liked_posts");
|
|
92
|
+
if (likedPosts) {
|
|
93
|
+
try {
|
|
94
|
+
const posts = JSON.parse(likedPosts);
|
|
95
|
+
return posts.includes(postId);
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setLiked(postId, liked) {
|
|
104
|
+
let posts = [];
|
|
105
|
+
|
|
106
|
+
const stored = localStorage.getItem("blog_liked_posts");
|
|
107
|
+
if (stored) {
|
|
108
|
+
try {
|
|
109
|
+
posts = JSON.parse(stored);
|
|
110
|
+
} catch {
|
|
111
|
+
posts = [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (liked && !posts.includes(postId)) {
|
|
115
|
+
posts.push(postId);
|
|
116
|
+
} else if (!liked) {
|
|
117
|
+
posts = posts.filter(id => id !== postId);
|
|
118
|
+
}
|
|
119
|
+
localStorage.setItem("blog_liked_posts", JSON.stringify(posts));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isBookmarked(postId) {
|
|
123
|
+
return getBookmarks().includes(postId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function initPostActions() {
|
|
127
|
+
const likeBtn = document.getElementById("like-btn");
|
|
128
|
+
const bookmarkBtn = document.getElementById("bookmark-btn");
|
|
129
|
+
const likeCount = document.getElementById("like-count");
|
|
130
|
+
const likeText = likeBtn?.querySelector(".like-text");
|
|
131
|
+
const bookmarkText = bookmarkBtn?.querySelector(".bookmark-text");
|
|
132
|
+
|
|
133
|
+
if (!likeBtn || !bookmarkBtn || !likeCount) return;
|
|
134
|
+
|
|
135
|
+
const postId = getPostId();
|
|
136
|
+
|
|
137
|
+
// Initialize like state
|
|
138
|
+
const likes = getLikes();
|
|
139
|
+
const currentLikes = likes[postId] || 0;
|
|
140
|
+
likeCount.textContent = String(currentLikes);
|
|
141
|
+
|
|
142
|
+
const userLiked = isLiked(postId);
|
|
143
|
+
if (userLiked) {
|
|
144
|
+
likeBtn.classList.add(
|
|
145
|
+
"liked",
|
|
146
|
+
"border-accent",
|
|
147
|
+
"text-accent",
|
|
148
|
+
"bg-accent/10"
|
|
149
|
+
);
|
|
150
|
+
likeBtn.setAttribute("aria-pressed", "true");
|
|
151
|
+
if (likeText) likeText.textContent = "已喜欢";
|
|
152
|
+
// Fill heart
|
|
153
|
+
const heart = likeBtn.querySelector("svg path");
|
|
154
|
+
if (heart) {
|
|
155
|
+
heart.setAttribute("fill", "currentColor");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Initialize bookmark state
|
|
160
|
+
if (isBookmarked(postId)) {
|
|
161
|
+
bookmarkBtn.classList.add(
|
|
162
|
+
"bookmarked",
|
|
163
|
+
"border-accent",
|
|
164
|
+
"text-accent",
|
|
165
|
+
"bg-accent/10"
|
|
166
|
+
);
|
|
167
|
+
bookmarkBtn.setAttribute("aria-pressed", "true");
|
|
168
|
+
if (bookmarkText) bookmarkText.textContent = "已收藏";
|
|
169
|
+
// Fill bookmark
|
|
170
|
+
const bookmark = bookmarkBtn.querySelector("svg path");
|
|
171
|
+
if (bookmark) {
|
|
172
|
+
bookmark.setAttribute("fill", "currentColor");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Like button click handler
|
|
177
|
+
likeBtn.addEventListener("click", () => {
|
|
178
|
+
const likes = getLikes();
|
|
179
|
+
const userLiked = isLiked(postId);
|
|
180
|
+
|
|
181
|
+
if (userLiked) {
|
|
182
|
+
// Unlike
|
|
183
|
+
likes[postId] = Math.max(0, (likes[postId] || 1) - 1);
|
|
184
|
+
setLiked(postId, false);
|
|
185
|
+
likeBtn.classList.remove(
|
|
186
|
+
"liked",
|
|
187
|
+
"border-accent",
|
|
188
|
+
"text-accent",
|
|
189
|
+
"bg-accent/10"
|
|
190
|
+
);
|
|
191
|
+
likeBtn.setAttribute("aria-pressed", "false");
|
|
192
|
+
if (likeText) likeText.textContent = "喜欢";
|
|
193
|
+
// Unfill heart
|
|
194
|
+
const heart = likeBtn.querySelector("svg path");
|
|
195
|
+
if (heart) {
|
|
196
|
+
heart.setAttribute("fill", "none");
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Like
|
|
200
|
+
likes[postId] = (likes[postId] || 0) + 1;
|
|
201
|
+
setLiked(postId, true);
|
|
202
|
+
likeBtn.classList.add(
|
|
203
|
+
"liked",
|
|
204
|
+
"border-accent",
|
|
205
|
+
"text-accent",
|
|
206
|
+
"bg-accent/10"
|
|
207
|
+
);
|
|
208
|
+
likeBtn.setAttribute("aria-pressed", "true");
|
|
209
|
+
if (likeText) likeText.textContent = "已喜欢";
|
|
210
|
+
// Fill heart
|
|
211
|
+
const heart = likeBtn.querySelector("svg path");
|
|
212
|
+
if (heart) {
|
|
213
|
+
heart.setAttribute("fill", "currentColor");
|
|
214
|
+
}
|
|
215
|
+
// Add animation
|
|
216
|
+
likeBtn.classList.add("animate-pulse");
|
|
217
|
+
setTimeout(() => likeBtn.classList.remove("animate-pulse"), 300);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
saveLikes(likes);
|
|
221
|
+
likeCount.textContent = String(likes[postId] || 0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Bookmark button click handler
|
|
225
|
+
bookmarkBtn.addEventListener("click", () => {
|
|
226
|
+
const bookmarks = getBookmarks();
|
|
227
|
+
const isCurrentlyBookmarked = bookmarks.includes(postId);
|
|
228
|
+
|
|
229
|
+
if (isCurrentlyBookmarked) {
|
|
230
|
+
// Remove bookmark
|
|
231
|
+
const newBookmarks = bookmarks.filter(id => id !== postId);
|
|
232
|
+
saveBookmarks(newBookmarks);
|
|
233
|
+
bookmarkBtn.classList.remove(
|
|
234
|
+
"bookmarked",
|
|
235
|
+
"border-accent",
|
|
236
|
+
"text-accent",
|
|
237
|
+
"bg-accent/10"
|
|
238
|
+
);
|
|
239
|
+
bookmarkBtn.setAttribute("aria-pressed", "false");
|
|
240
|
+
if (bookmarkText) bookmarkText.textContent = "收藏";
|
|
241
|
+
// Unfill bookmark
|
|
242
|
+
const bookmark = bookmarkBtn.querySelector("svg path");
|
|
243
|
+
if (bookmark) {
|
|
244
|
+
bookmark.setAttribute("fill", "none");
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Add bookmark
|
|
248
|
+
bookmarks.push(postId);
|
|
249
|
+
saveBookmarks(bookmarks);
|
|
250
|
+
bookmarkBtn.classList.add(
|
|
251
|
+
"bookmarked",
|
|
252
|
+
"border-accent",
|
|
253
|
+
"text-accent",
|
|
254
|
+
"bg-accent/10"
|
|
255
|
+
);
|
|
256
|
+
bookmarkBtn.setAttribute("aria-pressed", "true");
|
|
257
|
+
if (bookmarkText) bookmarkText.textContent = "已收藏";
|
|
258
|
+
// Fill bookmark
|
|
259
|
+
const bookmark = bookmarkBtn.querySelector("svg path");
|
|
260
|
+
if (bookmark) {
|
|
261
|
+
bookmark.setAttribute("fill", "currentColor");
|
|
262
|
+
}
|
|
263
|
+
// Add animation
|
|
264
|
+
bookmarkBtn.classList.add("animate-pulse");
|
|
265
|
+
setTimeout(() => bookmarkBtn.classList.remove("animate-pulse"), 300);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Initialize on page load
|
|
271
|
+
initPostActions();
|
|
272
|
+
|
|
273
|
+
// Re-initialize after page transitions
|
|
274
|
+
document.addEventListener("astro:page-load", initPostActions);
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<style is:global>
|
|
278
|
+
.post-actions .action-btn {
|
|
279
|
+
transition: all 0.2s ease;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.post-actions .action-btn:hover {
|
|
283
|
+
transform: translateY(-2px);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.post-actions .action-btn:active {
|
|
287
|
+
transform: translateY(0);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.post-actions .action-btn.liked svg,
|
|
291
|
+
.post-actions .action-btn.bookmarked svg {
|
|
292
|
+
animation: heartbeat 0.3s ease;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@keyframes heartbeat {
|
|
296
|
+
0% {
|
|
297
|
+
transform: scale(1);
|
|
298
|
+
}
|
|
299
|
+
50% {
|
|
300
|
+
transform: scale(1.2);
|
|
301
|
+
}
|
|
302
|
+
100% {
|
|
303
|
+
transform: scale(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CollectionEntry } from "astro:content";
|
|
3
|
+
import { getLocalizedPostPath } from "../../utils/getPath";
|
|
4
|
+
import Datetime from "../../components/ui/Datetime.astro";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
posts: CollectionEntry<"blog">[];
|
|
8
|
+
lang?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
posts,
|
|
14
|
+
lang = "zh",
|
|
15
|
+
title = lang === "zh" ? "相关推荐" : "Related Posts",
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
posts.length > 0 && (
|
|
21
|
+
<div class="mt-8" data-pagefind-ignore>
|
|
22
|
+
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold text-foreground">
|
|
23
|
+
<svg
|
|
24
|
+
class="size-5 text-accent"
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
fill="none"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
stroke-width="2"
|
|
29
|
+
stroke-linecap="round"
|
|
30
|
+
stroke-linejoin="round"
|
|
31
|
+
>
|
|
32
|
+
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
|
|
33
|
+
</svg>
|
|
34
|
+
{title}
|
|
35
|
+
</h3>
|
|
36
|
+
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
37
|
+
{posts.map(post => (
|
|
38
|
+
<a
|
|
39
|
+
href={getLocalizedPostPath(lang, post.id)}
|
|
40
|
+
class="group flex flex-col rounded-lg border border-border bg-background/50 p-4 transition-all duration-200 hover:border-accent/50 hover:bg-muted/30 hover:shadow-sm"
|
|
41
|
+
>
|
|
42
|
+
<h4 class="line-clamp-2 text-sm font-medium text-foreground transition-colors group-hover:text-accent">
|
|
43
|
+
{post.data.title}
|
|
44
|
+
</h4>
|
|
45
|
+
<p class="text-muted-foreground mt-1.5 line-clamp-2 text-xs">
|
|
46
|
+
{post.data.description}
|
|
47
|
+
</p>
|
|
48
|
+
<div class="mt-auto pt-2">
|
|
49
|
+
<Datetime
|
|
50
|
+
pubDatetime={post.data.pubDatetime}
|
|
51
|
+
modDatetime={post.data.modDatetime}
|
|
52
|
+
lang={lang}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</a>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CollectionEntry } from "astro:content";
|
|
3
|
+
import { getCollection } from "astro:content";
|
|
4
|
+
import { getPostsByLang } from "../../utils/getPostsByLang";
|
|
5
|
+
import { getSeriesByName } from "../../utils/getSeriesData";
|
|
6
|
+
import { getLocalizedPostPath } from "../../utils/getPath";
|
|
7
|
+
import { t } from "../../utils/i18n";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
post: CollectionEntry<"blog">;
|
|
11
|
+
lang?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { post, lang = "zh" } = Astro.props;
|
|
15
|
+
const series = post.data.series;
|
|
16
|
+
|
|
17
|
+
if (!series) return;
|
|
18
|
+
|
|
19
|
+
const allPosts = await getCollection("blog", ({ data }) => !data.draft);
|
|
20
|
+
const langPosts = getPostsByLang(allPosts, lang);
|
|
21
|
+
const seriesInfo = getSeriesByName(langPosts, series.name);
|
|
22
|
+
|
|
23
|
+
if (!seriesInfo || seriesInfo.totalPosts < 2) return;
|
|
24
|
+
|
|
25
|
+
const currentOrder = series.order;
|
|
26
|
+
const seriesSlug = seriesInfo.slug;
|
|
27
|
+
const progressPct = (currentOrder / seriesInfo.totalPosts) * 100;
|
|
28
|
+
const orderStr = String(currentOrder).padStart(2, "0");
|
|
29
|
+
const totalStr = String(seriesInfo.totalPosts).padStart(2, "0");
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<nav
|
|
33
|
+
class="series-nav my-6 rounded-xl border p-4 sm:p-5"
|
|
34
|
+
aria-label={`${t("series.title", lang)}: ${series.name}`}
|
|
35
|
+
>
|
|
36
|
+
{/* Header */}
|
|
37
|
+
<div class="mb-3 flex items-center justify-between gap-2">
|
|
38
|
+
<a
|
|
39
|
+
href={`/${lang}/series/${encodeURIComponent(seriesSlug)}/`}
|
|
40
|
+
class="series-link flex items-center gap-2 text-sm font-bold"
|
|
41
|
+
>
|
|
42
|
+
<svg class="series-icon size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
43
|
+
<path d="M4 6h16M4 12h16M4 18h10" />
|
|
44
|
+
<circle cx="20" cy="18" r="2" fill="currentColor" stroke="none" />
|
|
45
|
+
</svg>
|
|
46
|
+
<span>{series.name}</span>
|
|
47
|
+
</a>
|
|
48
|
+
<span class="series-progress font-mono text-xs tracking-wider">
|
|
49
|
+
{orderStr}<span class="mx-0.5 opacity-40">/</span>{totalStr}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Progress bar */}
|
|
54
|
+
<div class="series-track relative mb-4 h-[3px] w-full overflow-visible rounded-full">
|
|
55
|
+
<div class="series-fill absolute inset-y-0 left-0 rounded-full" style={`width: ${progressPct}%`}>
|
|
56
|
+
<span class="series-pulse-dot"></span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Post list */}
|
|
61
|
+
<ol class="space-y-0.5">
|
|
62
|
+
{
|
|
63
|
+
seriesInfo.posts.map((p, idx) => {
|
|
64
|
+
const isCurrent = p.id === post.id;
|
|
65
|
+
const href = getLocalizedPostPath(lang, p.id, p.filePath);
|
|
66
|
+
const num = String(idx + 1).padStart(2, "0");
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<li>
|
|
70
|
+
{isCurrent ? (
|
|
71
|
+
<span class="series-item series-item--active flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm font-medium">
|
|
72
|
+
<span class="font-mono text-xs opacity-70">{num}</span>
|
|
73
|
+
<span class="truncate">{p.data.title}</span>
|
|
74
|
+
</span>
|
|
75
|
+
) : (
|
|
76
|
+
<a
|
|
77
|
+
href={href}
|
|
78
|
+
class="series-item flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm"
|
|
79
|
+
>
|
|
80
|
+
<span class="font-mono text-xs opacity-40">{num}</span>
|
|
81
|
+
<span class="truncate">{p.data.title}</span>
|
|
82
|
+
</a>
|
|
83
|
+
)}
|
|
84
|
+
</li>
|
|
85
|
+
);
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
</ol>
|
|
89
|
+
</nav>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
.series-nav {
|
|
93
|
+
background: linear-gradient(135deg, var(--series-bg-from), var(--series-bg-to));
|
|
94
|
+
border-color: var(--series-border);
|
|
95
|
+
box-shadow: 0 0 15px var(--series-glow);
|
|
96
|
+
transition:
|
|
97
|
+
background-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
98
|
+
border-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
99
|
+
box-shadow 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
100
|
+
color 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.series-link {
|
|
104
|
+
color: var(--foreground);
|
|
105
|
+
transition: color 0.2s;
|
|
106
|
+
}
|
|
107
|
+
.series-link:hover {
|
|
108
|
+
color: var(--accent);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.series-icon {
|
|
112
|
+
color: var(--accent);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.series-progress {
|
|
116
|
+
color: var(--foreground-soft);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Track */
|
|
120
|
+
.series-track {
|
|
121
|
+
background-color: var(--muted);
|
|
122
|
+
}
|
|
123
|
+
.series-fill {
|
|
124
|
+
background-color: var(--accent);
|
|
125
|
+
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Pulse dot */
|
|
129
|
+
.series-pulse-dot {
|
|
130
|
+
position: absolute;
|
|
131
|
+
right: -4px;
|
|
132
|
+
top: 50%;
|
|
133
|
+
transform: translateY(-50%);
|
|
134
|
+
width: 8px;
|
|
135
|
+
height: 8px;
|
|
136
|
+
border-radius: 50%;
|
|
137
|
+
background-color: var(--series-pulse);
|
|
138
|
+
animation: pulse-glow 2s ease-in-out infinite;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@keyframes pulse-glow {
|
|
142
|
+
0%, 100% {
|
|
143
|
+
box-shadow: 0 0 4px 1px var(--series-glow);
|
|
144
|
+
opacity: 1;
|
|
145
|
+
}
|
|
146
|
+
50% {
|
|
147
|
+
box-shadow: 0 0 10px 4px var(--series-glow);
|
|
148
|
+
opacity: 0.7;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* List items */
|
|
153
|
+
.series-item {
|
|
154
|
+
color: var(--foreground-soft);
|
|
155
|
+
transition:
|
|
156
|
+
color 0.2s,
|
|
157
|
+
background-color 0.2s;
|
|
158
|
+
}
|
|
159
|
+
a.series-item:hover {
|
|
160
|
+
color: var(--accent);
|
|
161
|
+
background-color: var(--muted);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.series-item--active {
|
|
165
|
+
color: var(--accent);
|
|
166
|
+
background-color: color-mix(in srgb, var(--accent) 8%, transparent);
|
|
167
|
+
border-left: 2px solid var(--accent);
|
|
168
|
+
padding-left: calc(0.75rem - 2px);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@media (prefers-reduced-motion: reduce) {
|
|
172
|
+
.series-pulse-dot {
|
|
173
|
+
animation: none;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SHARE_LINKS } from "@/constants";
|
|
3
|
+
import LinkButton from "../../components/ui/LinkButton.astro";
|
|
4
|
+
|
|
5
|
+
const URL = Astro.url;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
SHARE_LINKS.length > 0 && (
|
|
10
|
+
<div class="flex flex-none flex-col items-center justify-center gap-1 md:items-start">
|
|
11
|
+
<span class="italic">Share this post on:</span>
|
|
12
|
+
<div class="text-center">
|
|
13
|
+
{SHARE_LINKS.map(social => (
|
|
14
|
+
<LinkButton
|
|
15
|
+
href={`${social.href + URL}`}
|
|
16
|
+
class="scale-90 p-2 hover:rotate-6 sm:p-1"
|
|
17
|
+
title={social.linkTitle}
|
|
18
|
+
>
|
|
19
|
+
<social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
|
|
20
|
+
<span class="sr-only">{social.linkTitle}</span>
|
|
21
|
+
</LinkButton>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import IconChevronLeft from "../../assets/icons/IconChevronLeft.svg";
|
|
3
|
+
import LinkButton from "../../components/ui/LinkButton.astro";
|
|
4
|
+
import { SITE } from "@/config";
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
SITE.showBackButton && (
|
|
9
|
+
<div class="app-layout flex items-center justify-start">
|
|
10
|
+
<LinkButton
|
|
11
|
+
id="back-button"
|
|
12
|
+
href="/"
|
|
13
|
+
class="focus-outline -ms-2 mt-8 mb-2 hover:text-foreground/75"
|
|
14
|
+
>
|
|
15
|
+
<IconChevronLeft class="inline-block size-6 rtl:rotate-180" />
|
|
16
|
+
<span>Go back</span>
|
|
17
|
+
</LinkButton>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
/* Update Search Praam */
|
|
24
|
+
function updateGoBackUrl() {
|
|
25
|
+
const backButton: HTMLAnchorElement | null =
|
|
26
|
+
document.querySelector("#back-button");
|
|
27
|
+
|
|
28
|
+
const backUrl = sessionStorage.getItem("backUrl");
|
|
29
|
+
|
|
30
|
+
if (backUrl && backButton) {
|
|
31
|
+
backButton.href = backUrl;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
document.addEventListener("astro:page-load", updateGoBackUrl);
|
|
36
|
+
updateGoBackUrl();
|
|
37
|
+
</script>
|