@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,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
import IconX from "../../assets/icons/IconX.svg";
|
|
3
|
+
import IconHome from "../../assets/icons/IconHome.svg";
|
|
4
|
+
import IconArticle from "../../assets/icons/IconArticle.svg";
|
|
5
|
+
import IconTag from "../../assets/icons/IconTag.svg";
|
|
6
|
+
import IconUser from "../../assets/icons/IconUser.svg";
|
|
7
|
+
import IconSearch from "../../assets/icons/IconSearch.svg";
|
|
8
|
+
import IconArchive from "../../assets/icons/IconArchive.svg";
|
|
9
|
+
import IconSeries from "../../assets/icons/IconSeries.svg";
|
|
10
|
+
import IconProject from "../../assets/icons/IconProject.svg";
|
|
11
|
+
import IconMenuDeep from "../../assets/icons/IconMenuDeep.svg";
|
|
12
|
+
import { SITE } from "@/config";
|
|
13
|
+
import { t, type TranslationKey } from "../../utils/i18n";
|
|
14
|
+
import { isPathActive } from "../../utils/pathUtils";
|
|
15
|
+
|
|
16
|
+
const { pathname } = Astro.url;
|
|
17
|
+
const isActive = (path: string) => isPathActive(path, pathname);
|
|
18
|
+
|
|
19
|
+
const { lang = "zh" } = Astro.props;
|
|
20
|
+
|
|
21
|
+
const base = `/${lang}`;
|
|
22
|
+
|
|
23
|
+
const iconMap: Record<string, typeof IconHome> = {
|
|
24
|
+
home: IconHome,
|
|
25
|
+
posts: IconArticle,
|
|
26
|
+
tags: IconTag,
|
|
27
|
+
categories: IconTag,
|
|
28
|
+
series: IconSeries,
|
|
29
|
+
projects: IconProject,
|
|
30
|
+
about: IconUser,
|
|
31
|
+
friends: IconUser,
|
|
32
|
+
archives: IconArchive,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const hrefMap: Record<string, string> = {
|
|
36
|
+
home: `${base}/`,
|
|
37
|
+
posts: `${base}/posts`,
|
|
38
|
+
tags: `${base}/tags`,
|
|
39
|
+
categories: `${base}/categories`,
|
|
40
|
+
series: `${base}/series`,
|
|
41
|
+
projects: `${base}/projects`,
|
|
42
|
+
about: `${base}/about`,
|
|
43
|
+
friends: `${base}/friends`,
|
|
44
|
+
archives: `${base}/archives`,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const navItems = SITE.nav.items
|
|
48
|
+
.filter(i => i.enabled)
|
|
49
|
+
.map(i => ({
|
|
50
|
+
href: hrefMap[i.key] ?? `${base}/${i.key}`,
|
|
51
|
+
label: t(`nav.${i.key}` as TranslationKey, lang),
|
|
52
|
+
icon: iconMap[i.key] ?? IconArticle,
|
|
53
|
+
}));
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<a
|
|
57
|
+
id="skip-to-content"
|
|
58
|
+
href="#main-content"
|
|
59
|
+
class="absolute start-16 -top-full z-50 bg-background px-3 py-2 text-accent backdrop-blur-lg transition-all focus:top-4"
|
|
60
|
+
>
|
|
61
|
+
Skip to content
|
|
62
|
+
</a>
|
|
63
|
+
|
|
64
|
+
<header
|
|
65
|
+
class="sticky top-0 z-40 w-full border-b border-border bg-background/80 backdrop-blur-md"
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
class="flex h-14 w-full items-center justify-between px-4 sm:px-6 lg:px-8"
|
|
69
|
+
>
|
|
70
|
+
<a
|
|
71
|
+
href={`/${lang}/`}
|
|
72
|
+
class="text-lg font-semibold tracking-tight text-foreground transition-colors hover:text-accent sm:text-xl"
|
|
73
|
+
>
|
|
74
|
+
{SITE.title}
|
|
75
|
+
</a>
|
|
76
|
+
|
|
77
|
+
<nav id="nav-menu" class="flex items-center">
|
|
78
|
+
{/* Desktop Navigation */}
|
|
79
|
+
<ul class="hidden items-center gap-1 sm:flex">
|
|
80
|
+
{
|
|
81
|
+
navItems.map(({ href, label, icon: Icon }) => (
|
|
82
|
+
<li>
|
|
83
|
+
<a
|
|
84
|
+
href={href}
|
|
85
|
+
class:list={[
|
|
86
|
+
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
|
|
87
|
+
"text-muted-foreground hover:bg-muted/60 hover:text-foreground",
|
|
88
|
+
{ "bg-muted/80 text-foreground": isActive(href) },
|
|
89
|
+
]}
|
|
90
|
+
>
|
|
91
|
+
<Icon class="size-4" />
|
|
92
|
+
<span>{label}</span>
|
|
93
|
+
</a>
|
|
94
|
+
</li>
|
|
95
|
+
))
|
|
96
|
+
}
|
|
97
|
+
<li class="ml-1 flex items-center gap-1 border-l border-border pl-2">
|
|
98
|
+
<button
|
|
99
|
+
id="lang-switch-btn"
|
|
100
|
+
class="text-muted-foreground rounded-md px-2 py-1.5 text-xs font-semibold transition-colors hover:bg-muted/60 hover:text-foreground"
|
|
101
|
+
title="Switch language"
|
|
102
|
+
aria-label="Switch language"
|
|
103
|
+
>
|
|
104
|
+
<span id="lang-label">EN</span>
|
|
105
|
+
</button>
|
|
106
|
+
<a
|
|
107
|
+
href={`/${lang}/search`}
|
|
108
|
+
class:list={[
|
|
109
|
+
"flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium transition-colors",
|
|
110
|
+
"text-muted-foreground hover:bg-muted/60 hover:text-foreground",
|
|
111
|
+
{ "bg-muted/80 text-foreground": isActive(`/${lang}/search`) },
|
|
112
|
+
]}
|
|
113
|
+
title={t("nav.search", lang)}
|
|
114
|
+
aria-label={t("nav.search", lang)}
|
|
115
|
+
>
|
|
116
|
+
<IconSearch class="size-4" />
|
|
117
|
+
</a>
|
|
118
|
+
</li>
|
|
119
|
+
</ul>
|
|
120
|
+
|
|
121
|
+
{/* Mobile Menu Button */}
|
|
122
|
+
<button
|
|
123
|
+
id="menu-btn"
|
|
124
|
+
class="focus-outline rounded-md p-2 sm:hidden"
|
|
125
|
+
aria-label="Open Menu"
|
|
126
|
+
aria-expanded="false"
|
|
127
|
+
aria-controls="mobile-menu"
|
|
128
|
+
>
|
|
129
|
+
<IconX id="close-icon" class="hidden size-5" />
|
|
130
|
+
<IconMenuDeep id="menu-icon" class="size-5" />
|
|
131
|
+
</button>
|
|
132
|
+
</nav>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Mobile Navigation Panel */}
|
|
136
|
+
<div
|
|
137
|
+
id="mobile-menu"
|
|
138
|
+
class="hidden border-t border-border bg-background sm:hidden"
|
|
139
|
+
>
|
|
140
|
+
<ul class="flex flex-col gap-1 px-4 py-3">
|
|
141
|
+
{
|
|
142
|
+
navItems.map(({ href, label, icon: Icon }) => (
|
|
143
|
+
<li>
|
|
144
|
+
<a
|
|
145
|
+
href={href}
|
|
146
|
+
class:list={[
|
|
147
|
+
"flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors",
|
|
148
|
+
"text-muted-foreground hover:bg-muted/60 hover:text-foreground",
|
|
149
|
+
{ "bg-muted/80 text-foreground": isActive(href) },
|
|
150
|
+
]}
|
|
151
|
+
>
|
|
152
|
+
<Icon class="size-5" />
|
|
153
|
+
<span>{label}</span>
|
|
154
|
+
</a>
|
|
155
|
+
</li>
|
|
156
|
+
))
|
|
157
|
+
}
|
|
158
|
+
<li class="mt-1 border-t border-border pt-2">
|
|
159
|
+
<a
|
|
160
|
+
href={`/${lang}/search`}
|
|
161
|
+
class:list={[
|
|
162
|
+
"flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors",
|
|
163
|
+
"text-muted-foreground hover:bg-muted/60 hover:text-foreground",
|
|
164
|
+
{ "bg-muted/80 text-foreground": isActive(`/${lang}/search`) },
|
|
165
|
+
]}
|
|
166
|
+
>
|
|
167
|
+
<IconSearch class="size-5" />
|
|
168
|
+
<span>{t("nav.search", lang)}</span>
|
|
169
|
+
</a>
|
|
170
|
+
</li>
|
|
171
|
+
<li>
|
|
172
|
+
<button
|
|
173
|
+
id="lang-switch-btn-mobile"
|
|
174
|
+
class="text-muted-foreground flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors hover:bg-muted/60 hover:text-foreground"
|
|
175
|
+
aria-label="Switch language"
|
|
176
|
+
>
|
|
177
|
+
<svg
|
|
178
|
+
class="size-5"
|
|
179
|
+
viewBox="0 0 24 24"
|
|
180
|
+
fill="none"
|
|
181
|
+
stroke="currentColor"
|
|
182
|
+
stroke-width="2"
|
|
183
|
+
stroke-linecap="round"
|
|
184
|
+
stroke-linejoin="round"
|
|
185
|
+
>
|
|
186
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
187
|
+
<path d="M2 12h20"></path>
|
|
188
|
+
<path
|
|
189
|
+
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
|
190
|
+
></path>
|
|
191
|
+
</svg>
|
|
192
|
+
<span id="lang-label-mobile">English</span>
|
|
193
|
+
</button>
|
|
194
|
+
</li>
|
|
195
|
+
</ul>
|
|
196
|
+
</div>
|
|
197
|
+
</header>
|
|
198
|
+
|
|
199
|
+
<script>
|
|
200
|
+
function toggleNav() {
|
|
201
|
+
const menuBtn = document.querySelector("#menu-btn");
|
|
202
|
+
const mobileMenu = document.querySelector("#mobile-menu");
|
|
203
|
+
const menuIcon = document.querySelector("#menu-icon");
|
|
204
|
+
const closeIcon = document.querySelector("#close-icon");
|
|
205
|
+
|
|
206
|
+
if (!menuBtn || !mobileMenu || !menuIcon || !closeIcon) return;
|
|
207
|
+
|
|
208
|
+
menuBtn.addEventListener("click", () => {
|
|
209
|
+
const openMenu = menuBtn.getAttribute("aria-expanded") === "true";
|
|
210
|
+
|
|
211
|
+
menuBtn.setAttribute("aria-expanded", openMenu ? "false" : "true");
|
|
212
|
+
menuBtn.setAttribute("aria-label", openMenu ? "Open Menu" : "Close Menu");
|
|
213
|
+
|
|
214
|
+
mobileMenu.classList.toggle("hidden");
|
|
215
|
+
menuIcon.classList.toggle("hidden");
|
|
216
|
+
closeIcon.classList.toggle("hidden");
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
toggleNav();
|
|
221
|
+
document.addEventListener("astro:after-swap", toggleNav);
|
|
222
|
+
|
|
223
|
+
function initLangSwitcher() {
|
|
224
|
+
const btns = [
|
|
225
|
+
document.getElementById("lang-switch-btn"),
|
|
226
|
+
document.getElementById("lang-switch-btn-mobile"),
|
|
227
|
+
];
|
|
228
|
+
const label = document.getElementById("lang-label");
|
|
229
|
+
const labelMobile = document.getElementById("lang-label-mobile");
|
|
230
|
+
|
|
231
|
+
const path = window.location.pathname;
|
|
232
|
+
const isZh = path.startsWith("/zh/") || path === "/zh";
|
|
233
|
+
|
|
234
|
+
if (label) label.textContent = isZh ? "EN" : "中";
|
|
235
|
+
if (labelMobile) labelMobile.textContent = isZh ? "English" : "中文";
|
|
236
|
+
|
|
237
|
+
btns.forEach(btn => {
|
|
238
|
+
btn?.addEventListener("click", () => {
|
|
239
|
+
if (isZh) {
|
|
240
|
+
const newPath = path.replace(/^\/zh\/?/, "/en/") || "/en/";
|
|
241
|
+
window.location.href = newPath;
|
|
242
|
+
} else {
|
|
243
|
+
const newPath = path.replace(/^\/en\/?/, "/zh/") || "/zh/";
|
|
244
|
+
window.location.href = newPath;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
initLangSwitcher();
|
|
251
|
+
document.addEventListener("astro:after-swap", initLangSwitcher);
|
|
252
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Page } from "astro";
|
|
3
|
+
import type { CollectionEntry } from "astro:content";
|
|
4
|
+
import IconArrowLeft from "../../assets/icons/IconArrowLeft.svg";
|
|
5
|
+
import IconArrowRight from "../../assets/icons/IconArrowRight.svg";
|
|
6
|
+
import LinkButton from "../../components/ui/LinkButton.astro";
|
|
7
|
+
import { t } from "../../utils/i18n";
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
page: Page<CollectionEntry<"blog">>;
|
|
11
|
+
lang?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { page, lang = "zh" } = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
page.lastPage > 1 && (
|
|
19
|
+
<nav
|
|
20
|
+
class="mt-auto mb-8 flex justify-center"
|
|
21
|
+
role="navigation"
|
|
22
|
+
aria-label="Pagination Navigation"
|
|
23
|
+
>
|
|
24
|
+
<LinkButton
|
|
25
|
+
disabled={!page.url.prev}
|
|
26
|
+
href={page.url.prev as string}
|
|
27
|
+
class:list={["me-4 select-none", { "opacity-50": !page.url.prev }]}
|
|
28
|
+
aria-label="Goto Previous Page"
|
|
29
|
+
>
|
|
30
|
+
<IconArrowLeft class="inline-block rtl:rotate-180" />
|
|
31
|
+
{t("pagination.prev", lang)}
|
|
32
|
+
</LinkButton>
|
|
33
|
+
{page.currentPage} / {page.lastPage}
|
|
34
|
+
<LinkButton
|
|
35
|
+
disabled={!page.url.next}
|
|
36
|
+
href={page.url.next as string}
|
|
37
|
+
class:list={["ms-4 select-none", { "opacity-50": !page.url.next }]}
|
|
38
|
+
aria-label="Goto Next Page"
|
|
39
|
+
>
|
|
40
|
+
{t("pagination.next", lang)}
|
|
41
|
+
<IconArrowRight class="inline-block rtl:rotate-180" />
|
|
42
|
+
</LinkButton>
|
|
43
|
+
</nav>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SOCIALS } from "@/constants";
|
|
3
|
+
import LinkButton from "../../components/ui/LinkButton.astro";
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
7
|
+
{
|
|
8
|
+
SOCIALS.map(social => (
|
|
9
|
+
<LinkButton
|
|
10
|
+
href={social.href}
|
|
11
|
+
class="p-2 hover:rotate-6 sm:p-1"
|
|
12
|
+
title={social.linkTitle}
|
|
13
|
+
>
|
|
14
|
+
<social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
|
|
15
|
+
<span class="sr-only">{social.linkTitle}</span>
|
|
16
|
+
</LinkButton>
|
|
17
|
+
))
|
|
18
|
+
}
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SITE } from "@/config";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: string;
|
|
6
|
+
progressMax?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { class: className = "", progressMax = 50 } = Astro.props;
|
|
10
|
+
const sponsors = SITE.sponsor?.sponsors ?? [];
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
sponsors.length > 0 && (
|
|
15
|
+
<div class:list={["grid gap-3 sm:grid-cols-2 sm:gap-3.5 lg:grid-cols-3", className]}>
|
|
16
|
+
{sponsors.map(sponsor => (
|
|
17
|
+
<div class="relative h-full overflow-hidden rounded-xl border px-4 py-3">
|
|
18
|
+
<div class="mb-2 line-clamp-1 font-medium">{sponsor.name}</div>
|
|
19
|
+
<div class="text-xs text-foreground-soft">
|
|
20
|
+
{sponsor.date}
|
|
21
|
+
{sponsor.platform && ` via ${sponsor.platform}`}
|
|
22
|
+
</div>
|
|
23
|
+
<span class="absolute bottom-2 right-4 font-medium">
|
|
24
|
+
¥{sponsor.amount}
|
|
25
|
+
</span>
|
|
26
|
+
<div
|
|
27
|
+
class="absolute bottom-0 left-0 h-1 rounded-b-xl bg-accent/30 transition-all"
|
|
28
|
+
style={{ width: `${Math.min((sponsor.amount / progressMax) * 100, 100)}%` }}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SITE } from "@/config";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { class: className = "" } = Astro.props;
|
|
9
|
+
const methods = SITE.sponsor?.methods ?? [];
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
methods.length > 0 && (
|
|
14
|
+
<div class:list={["flex flex-col justify-center gap-4 sm:flex-row", className]}>
|
|
15
|
+
{methods.map(item => (
|
|
16
|
+
<div class="sponsorship-card relative justify-center overflow-hidden rounded-xl border bg-white">
|
|
17
|
+
<div class="sponsorship-card-icon absolute inset-0 flex items-center justify-center text-4xl opacity-80 transition-opacity">
|
|
18
|
+
<span>{item.icon === "wechat" ? "💬" : item.icon === "alipay" ? "💰" : "☕"}</span>
|
|
19
|
+
</div>
|
|
20
|
+
<img
|
|
21
|
+
class="sponsorship-card-img mx-auto my-0 max-w-60 transition-all"
|
|
22
|
+
src={item.image}
|
|
23
|
+
alt={item.name}
|
|
24
|
+
loading="lazy"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.sponsorship-card .sponsorship-card-img {
|
|
34
|
+
opacity: 0.3;
|
|
35
|
+
filter: blur(4px);
|
|
36
|
+
}
|
|
37
|
+
.sponsorship-card:hover .sponsorship-card-icon {
|
|
38
|
+
opacity: 0;
|
|
39
|
+
}
|
|
40
|
+
.sponsorship-card:hover .sponsorship-card-img {
|
|
41
|
+
opacity: 1;
|
|
42
|
+
filter: blur(0);
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* GitHub-style Alert component for MDX
|
|
4
|
+
*/
|
|
5
|
+
interface Props {
|
|
6
|
+
type: "note" | "tip" | "important" | "warning" | "caution";
|
|
7
|
+
title?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { type, title } = Astro.props;
|
|
11
|
+
|
|
12
|
+
const defaultTitles: Record<string, string> = {
|
|
13
|
+
note: "Note",
|
|
14
|
+
tip: "Tip",
|
|
15
|
+
important: "Important",
|
|
16
|
+
warning: "Warning",
|
|
17
|
+
caution: "Caution",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const displayTitle = title || defaultTitles[type] || "Note";
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<div class:list={["markdown-alert", `markdown-alert-${type}`]}>
|
|
24
|
+
<p class="markdown-alert-title">
|
|
25
|
+
{displayTitle}
|
|
26
|
+
</p>
|
|
27
|
+
<slot />
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CollectionEntry } from "astro:content";
|
|
3
|
+
import type { ImageMetadata } from "astro";
|
|
4
|
+
import { slugifyStr } from "../../utils/slugify";
|
|
5
|
+
import { getLocalizedPostPath } from "../../utils/getPath";
|
|
6
|
+
import { getReadingTime } from "../../utils/getReadingTime";
|
|
7
|
+
import { t } from "../../utils/i18n";
|
|
8
|
+
import IconArrowRight from "../../assets/icons/IconArrowRight.svg";
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
variant?: "h2" | "h3";
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
lang?: string;
|
|
14
|
+
priority?: boolean;
|
|
15
|
+
} & CollectionEntry<"blog">;
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
variant: Heading = "h2",
|
|
19
|
+
compact = false,
|
|
20
|
+
id,
|
|
21
|
+
data,
|
|
22
|
+
body,
|
|
23
|
+
filePath,
|
|
24
|
+
lang = "zh",
|
|
25
|
+
priority = false,
|
|
26
|
+
} = Astro.props;
|
|
27
|
+
|
|
28
|
+
const { title, description, category, ogImage, tags, pubDatetime, modDatetime, timezone } = data;
|
|
29
|
+
const postHref = getLocalizedPostPath(lang, id, filePath);
|
|
30
|
+
const { minutes: readingMinutes } = getReadingTime(body ?? "");
|
|
31
|
+
|
|
32
|
+
let coverSrc: string | undefined;
|
|
33
|
+
if (ogImage && typeof ogImage === "object" && "src" in ogImage) {
|
|
34
|
+
coverSrc = (ogImage as ImageMetadata).src;
|
|
35
|
+
} else if (typeof ogImage === "string" && ogImage.startsWith("http")) {
|
|
36
|
+
coverSrc = ogImage;
|
|
37
|
+
}
|
|
38
|
+
// Skip string paths that aren't URLs — they're likely broken local refs or dynamic OG stubs
|
|
39
|
+
|
|
40
|
+
const langLabel = id.startsWith("en/") ? "English" : "中文";
|
|
41
|
+
|
|
42
|
+
import dayjs from "dayjs";
|
|
43
|
+
import utcPlugin from "dayjs/plugin/utc";
|
|
44
|
+
import timezonePlugin from "dayjs/plugin/timezone";
|
|
45
|
+
import { SITE } from "@/config";
|
|
46
|
+
|
|
47
|
+
dayjs.extend(utcPlugin);
|
|
48
|
+
dayjs.extend(timezonePlugin);
|
|
49
|
+
|
|
50
|
+
const isModified = modDatetime && modDatetime > pubDatetime;
|
|
51
|
+
const datetime = dayjs(isModified ? modDatetime : pubDatetime).tz(timezone || SITE.timezone);
|
|
52
|
+
const dateStr = lang === "zh"
|
|
53
|
+
? datetime.format("YYYY 年 M 月 D 日")
|
|
54
|
+
: datetime.format("MMM D, YYYY");
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
{compact ? (
|
|
58
|
+
<li class="group">
|
|
59
|
+
<a
|
|
60
|
+
href={postHref}
|
|
61
|
+
class="block rounded-lg border border-border bg-card/50 p-4 transition-all duration-200 hover:border-accent/40 hover:shadow-md focus-visible:outline-2 focus-visible:outline-dashed focus-visible:outline-accent"
|
|
62
|
+
>
|
|
63
|
+
{category && (
|
|
64
|
+
<span class="mb-1 inline-block text-xs font-medium text-accent">
|
|
65
|
+
{category}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
<Heading
|
|
69
|
+
style={{ viewTransitionName: slugifyStr(title.replaceAll(".", "-")) }}
|
|
70
|
+
class="text-base font-semibold text-foreground transition-colors group-hover:text-accent"
|
|
71
|
+
>
|
|
72
|
+
{title}
|
|
73
|
+
</Heading>
|
|
74
|
+
<p class="mt-1 line-clamp-1 text-sm text-foreground-soft">{description}</p>
|
|
75
|
+
<div class="mt-2 flex items-center gap-3 text-xs text-foreground-soft">
|
|
76
|
+
<time datetime={datetime.toISOString()}>{dateStr}</time>
|
|
77
|
+
<span>·</span>
|
|
78
|
+
<span>{t("post.readingTime", lang).replace("{min}", String(readingMinutes))}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</a>
|
|
81
|
+
</li>
|
|
82
|
+
) : (
|
|
83
|
+
<li class="group">
|
|
84
|
+
<a
|
|
85
|
+
href={postHref}
|
|
86
|
+
class="relative flex min-h-[220px] overflow-hidden rounded-xl border border-border/60 transition-all duration-300 hover:border-accent/40 hover:shadow-lg hover:shadow-shadow-elevated/10 focus-visible:outline-2 focus-visible:outline-dashed focus-visible:outline-accent sm:min-h-[200px]"
|
|
87
|
+
>
|
|
88
|
+
{/* Background image or gradient fallback */}
|
|
89
|
+
{coverSrc ? (
|
|
90
|
+
<div class="absolute inset-0">
|
|
91
|
+
<img
|
|
92
|
+
src={coverSrc}
|
|
93
|
+
alt=""
|
|
94
|
+
class="h-full w-full object-cover object-center"
|
|
95
|
+
loading={priority ? "eager" : "lazy"}
|
|
96
|
+
decoding="async"
|
|
97
|
+
fetchpriority={priority ? "high" : undefined}
|
|
98
|
+
width="800"
|
|
99
|
+
height="450"
|
|
100
|
+
/>
|
|
101
|
+
<div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/60 to-black/30" />
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<div class="absolute inset-0 bg-gradient-to-br from-card via-muted/80 to-card" />
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{/* Content overlay */}
|
|
108
|
+
<div class={`relative z-10 flex w-full flex-col justify-between p-5 sm:p-6 ${coverSrc ? "text-white" : ""}`}>
|
|
109
|
+
{/* Top section: date + category */}
|
|
110
|
+
<div>
|
|
111
|
+
<div class="flex items-center gap-3">
|
|
112
|
+
<time
|
|
113
|
+
datetime={datetime.toISOString()}
|
|
114
|
+
class:list={[
|
|
115
|
+
"font-app text-xs tracking-wide",
|
|
116
|
+
coverSrc ? "text-white/80" : "text-foreground-soft"
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
{dateStr}
|
|
120
|
+
</time>
|
|
121
|
+
{category && (
|
|
122
|
+
<>
|
|
123
|
+
<span class:list={[coverSrc ? "text-white/40" : "text-foreground-soft/40"]}>·</span>
|
|
124
|
+
<span class:list={[
|
|
125
|
+
"text-xs font-medium",
|
|
126
|
+
coverSrc ? "text-white/80" : "text-accent"
|
|
127
|
+
]}>
|
|
128
|
+
{category}
|
|
129
|
+
</span>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Title */}
|
|
135
|
+
<Heading
|
|
136
|
+
style={{ viewTransitionName: slugifyStr(title.replaceAll(".", "-")) }}
|
|
137
|
+
class:list={[
|
|
138
|
+
"mt-2 text-lg font-bold leading-snug sm:text-xl",
|
|
139
|
+
coverSrc ? "text-white" : "text-foreground",
|
|
140
|
+
"transition-colors group-hover:text-accent"
|
|
141
|
+
]}
|
|
142
|
+
>
|
|
143
|
+
{title}
|
|
144
|
+
</Heading>
|
|
145
|
+
|
|
146
|
+
{/* Description */}
|
|
147
|
+
<p class:list={[
|
|
148
|
+
"mt-2 line-clamp-2 text-sm leading-relaxed",
|
|
149
|
+
coverSrc ? "text-white/75" : "text-foreground-soft"
|
|
150
|
+
]}>
|
|
151
|
+
{description}
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Bottom section: meta + tags */}
|
|
156
|
+
<div class="mt-4 flex flex-wrap items-end justify-between gap-3">
|
|
157
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
158
|
+
{/* Reading time */}
|
|
159
|
+
<span class:list={[
|
|
160
|
+
"flex items-center gap-1 text-xs",
|
|
161
|
+
coverSrc ? "text-white/70" : "text-foreground-soft"
|
|
162
|
+
]}>
|
|
163
|
+
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
164
|
+
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
|
165
|
+
</svg>
|
|
166
|
+
{readingMinutes} min
|
|
167
|
+
</span>
|
|
168
|
+
|
|
169
|
+
{/* Language badge */}
|
|
170
|
+
<span class:list={[
|
|
171
|
+
"flex items-center gap-1 text-xs",
|
|
172
|
+
coverSrc ? "text-white/70" : "text-foreground-soft"
|
|
173
|
+
]}>
|
|
174
|
+
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
175
|
+
<circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
|
176
|
+
</svg>
|
|
177
|
+
{langLabel}
|
|
178
|
+
</span>
|
|
179
|
+
|
|
180
|
+
{/* Tags */}
|
|
181
|
+
{tags.length > 0 && (
|
|
182
|
+
<div class="flex flex-wrap gap-1.5">
|
|
183
|
+
{tags.slice(0, 3).map(tag => (
|
|
184
|
+
<span class:list={[
|
|
185
|
+
"rounded-md px-2 py-0.5 text-xs",
|
|
186
|
+
coverSrc
|
|
187
|
+
? "bg-white/15 text-white/80 backdrop-blur-sm"
|
|
188
|
+
: "bg-muted text-foreground-soft"
|
|
189
|
+
]}>
|
|
190
|
+
{tag}
|
|
191
|
+
</span>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Arrow indicator */}
|
|
198
|
+
<IconArrowRight class:list={[
|
|
199
|
+
"size-5 shrink-0 transition-transform duration-200 group-hover:translate-x-1 rtl:rotate-180 rtl:group-hover:-translate-x-1",
|
|
200
|
+
coverSrc ? "text-white/60" : "text-foreground-soft"
|
|
201
|
+
]} />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</a>
|
|
205
|
+
</li>
|
|
206
|
+
)}
|