diff --git a/src/components/ArchivePanel.svelte b/src/components/ArchivePanel.svelte index 22939c2..f6542f6 100644 --- a/src/components/ArchivePanel.svelte +++ b/src/components/ArchivePanel.svelte @@ -3,33 +3,29 @@ import { onMount } from "svelte"; import I18nKey from "../i18n/i18nKey"; import { i18n } from "../i18n/translation"; +import type { PostForList } from "../utils/content-utils"; import { getPostUrlBySlug } from "../utils/url-utils"; export let tags: string[]; export let categories: string[]; -export let sortedPosts: Post[] = []; +export let sortedPosts: PostForList[] = []; +export let pinnedPosts: PostForList[] = []; +export let categoryPinnedPosts: PostForList[] = []; const params = new URLSearchParams(window.location.search); tags = params.has("tag") ? params.getAll("tag") : []; categories = params.has("category") ? params.getAll("category") : []; const uncategorized = params.get("uncategorized"); -interface Post { - slug: string; - data: { - title: string; - tags: string[]; - category?: string; - published: Date; - }; -} +const isCategoryMode = categories.length > 0; interface Group { year: number; - posts: Post[]; + posts: PostForList[]; } let groups: Group[] = []; +let displayPinnedPosts: PostForList[] = []; function formatDate(date: Date) { const month = (date.getMonth() + 1).toString().padStart(2, "0"); @@ -42,7 +38,7 @@ function formatTag(tagList: string[]) { } onMount(async () => { - let filteredPosts: Post[] = sortedPosts; + let filteredPosts: PostForList[] = sortedPosts; if (tags.length > 0) { filteredPosts = filteredPosts.filter( @@ -62,6 +58,39 @@ onMount(async () => { filteredPosts = filteredPosts.filter((post) => !post.data.category); } + if (isCategoryMode) { + displayPinnedPosts = categoryPinnedPosts.filter( + (post) => post.data.category && categories.includes(post.data.category), + ); + if (tags.length > 0) { + displayPinnedPosts = displayPinnedPosts.filter( + (post) => + Array.isArray(post.data.tags) && + post.data.tags.some((tag) => tags.includes(tag)), + ); + } + filteredPosts = filteredPosts.filter( + (post) => (post.data.category_pinned || 0) === 0, + ); + } else { + displayPinnedPosts = pinnedPosts; + if (tags.length > 0) { + displayPinnedPosts = displayPinnedPosts.filter( + (post) => + Array.isArray(post.data.tags) && + post.data.tags.some((tag) => tags.includes(tag)), + ); + } + if (uncategorized) { + displayPinnedPosts = displayPinnedPosts.filter( + (post) => !post.data.category, + ); + } + filteredPosts = filteredPosts.filter( + (post) => (post.data.pinned || 0) === 0, + ); + } + const grouped = filteredPosts.reduce( (acc, post) => { const year = post.data.published.getFullYear(); @@ -71,7 +100,7 @@ onMount(async () => { acc[year].push(post); return acc; }, - {} as Record, + {} as Record, ); const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({ @@ -86,66 +115,121 @@ onMount(async () => {
- {#each groups as group} -
-
-
- {group.year} -
-
-
-
-
- {group.posts.length} {i18n(group.posts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)} -
-
+ {#if displayPinnedPosts.length > 0} +
+
+
+ {i18n(I18nKey.pinned)} +
+
+
+
+
+ {displayPinnedPosts.length} {i18n(displayPinnedPosts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)} +
+
- {#each group.posts as post} - - - {/each} -
+ +
+ + {/each} +
+ {/if} + + {#each groups as group} +
+
+
+ {group.year} +
+
+
+
+
+ {group.posts.length} {i18n(group.posts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)} +
+
+ + {#each group.posts as post} + +
+
+ {formatDate(post.data.published)} +
+ +
+
+
+ +
+ {post.data.title} +
+ + +
+
+ {/each} +
+ {/each} + \ No newline at end of file diff --git a/src/components/PostCard.astro b/src/components/PostCard.astro index a7a7143..0f9704e 100644 --- a/src/components/PostCard.astro +++ b/src/components/PostCard.astro @@ -21,6 +21,7 @@ interface Props { description: string; draft: boolean; style: string; + pinned?: number; } const { entry, @@ -33,6 +34,7 @@ const { image, description, style, + pinned = 0, } = Astro.props; const className = Astro.props.class; @@ -52,6 +54,7 @@ const { remarkPluginFrontmatter } = await entry.render(); before:absolute before:top-[35px] before:left-[18px] before:hidden md:before:block "> {title} + {pinned > 0 && } diff --git a/src/components/PostPage.astro b/src/components/PostPage.astro index 4e46ed4..528acc5 100644 --- a/src/components/PostPage.astro +++ b/src/components/PostPage.astro @@ -21,6 +21,7 @@ const interval = 50; image={entry.data.image} description={entry.data.description} draft={entry.data.draft} + pinned={entry.data.pinned} class:list="onload-animation" style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`} > diff --git a/src/content/config.ts b/src/content/config.ts index 8bc07fe..a354d5a 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -11,6 +11,8 @@ const postsCollection = defineCollection({ tags: z.array(z.string()).optional().default([]), category: z.string().optional().nullable().default(""), lang: z.string().optional().default(""), + pinned: z.number().optional().default(0), + category_pinned: z.number().optional().default(0), /* For internal use */ prevTitle: z.string().default(""), diff --git a/src/i18n/i18nKey.ts b/src/i18n/i18nKey.ts index 33b18cf..0f2da7f 100644 --- a/src/i18n/i18nKey.ts +++ b/src/i18n/i18nKey.ts @@ -36,6 +36,7 @@ enum I18nKey { notFound = "notFound", notFoundDesc = "notFoundDesc", backToHome = "backToHome", + pinned = "pinned", } export default I18nKey; diff --git a/src/i18n/languages/en.ts b/src/i18n/languages/en.ts index a039cbc..4953f50 100644 --- a/src/i18n/languages/en.ts +++ b/src/i18n/languages/en.ts @@ -39,4 +39,5 @@ export const en: Translation = { [Key.notFound]: "Page Not Found", [Key.notFoundDesc]: "The link is broken, the page has gone missing", [Key.backToHome]: "Back to Home", + [Key.pinned]: "Pinned", }; diff --git a/src/i18n/languages/zh_CN.ts b/src/i18n/languages/zh_CN.ts index c493e41..391efd6 100644 --- a/src/i18n/languages/zh_CN.ts +++ b/src/i18n/languages/zh_CN.ts @@ -39,4 +39,5 @@ export const zh_CN: Translation = { [Key.notFound]: "页面未找到", [Key.notFoundDesc]: "你访问的链接已断开,页面走丢了", [Key.backToHome]: "返回首页", + [Key.pinned]: "置顶", }; diff --git a/src/i18n/languages/zh_TW.ts b/src/i18n/languages/zh_TW.ts index 5d25224..b24ee7f 100644 --- a/src/i18n/languages/zh_TW.ts +++ b/src/i18n/languages/zh_TW.ts @@ -39,4 +39,5 @@ export const zh_TW: Translation = { [Key.notFound]: "頁面未找到", [Key.notFoundDesc]: "你訪問的連結已斷開,頁面走丟了", [Key.backToHome]: "返回首頁", + [Key.pinned]: "置頂", }; diff --git a/src/pages/archive.astro b/src/pages/archive.astro index 90ede24..99e8923 100644 --- a/src/pages/archive.astro +++ b/src/pages/archive.astro @@ -3,12 +3,18 @@ import ArchivePanel from "@components/ArchivePanel.svelte"; import I18nKey from "@i18n/i18nKey"; import { i18n } from "@i18n/translation"; import MainGridLayout from "@layouts/MainGridLayout.astro"; -import { getSortedPostsList } from "../utils/content-utils"; +import { + getCategoryPinnedPosts, + getPinnedPosts, + getSortedPostsList, +} from "../utils/content-utils"; const sortedPostsList = await getSortedPostsList(); +const pinnedPosts = await getPinnedPosts(); +const categoryPinnedPosts = await getCategoryPinnedPosts(); --- - + diff --git a/src/utils/content-utils.ts b/src/utils/content-utils.ts index ca43516..deab49b 100644 --- a/src/utils/content-utils.ts +++ b/src/utils/content-utils.ts @@ -3,21 +3,35 @@ import I18nKey from "@i18n/i18nKey"; import { i18n } from "@i18n/translation"; import { getCategoryUrl } from "@utils/url-utils.ts"; -// // Retrieve posts and sort them by publication date -async function getRawSortedPosts() { +type Post = CollectionEntry<"posts">; + +function sortPosts(posts: Post[], useCategoryPinned = false): Post[] { + return posts.sort((a, b) => { + const pinnedA = useCategoryPinned + ? a.data.category_pinned || 0 + : a.data.pinned || 0; + const pinnedB = useCategoryPinned + ? b.data.category_pinned || 0 + : b.data.pinned || 0; + + const pinnedDiff = pinnedB - pinnedA; + if (pinnedDiff !== 0) return pinnedDiff; + + const dateA = new Date(a.data.published); + const dateB = new Date(b.data.published); + return dateB.valueOf() - dateA.valueOf(); + }); +} + +async function getRawSortedPosts(): Promise { const allBlogPosts = await getCollection("posts", ({ data }) => { return import.meta.env.PROD ? data.draft !== true : true; }); - const sorted = allBlogPosts.sort((a, b) => { - const dateA = new Date(a.data.published); - const dateB = new Date(b.data.published); - return dateA > dateB ? -1 : 1; - }); - return sorted; + return sortPosts(allBlogPosts); } -export async function getSortedPosts() { +export async function getSortedPosts(): Promise { const sorted = await getRawSortedPosts(); for (let i = 1; i < sorted.length; i++) { @@ -112,3 +126,37 @@ export async function getCategoryList(): Promise { } return ret; } + +export async function getPinnedPosts(): Promise { + const allBlogPosts = await getCollection("posts", ({ data }) => { + return import.meta.env.PROD ? data.draft !== true : true; + }); + + const pinnedPosts = allBlogPosts.filter( + (post) => (post.data.pinned || 0) > 0, + ); + + const sorted = sortPosts(pinnedPosts); + + return sorted.map((post) => ({ + slug: post.slug, + data: post.data, + })); +} + +export async function getCategoryPinnedPosts(): Promise { + const allBlogPosts = await getCollection("posts", ({ data }) => { + return import.meta.env.PROD ? data.draft !== true : true; + }); + + const categoryPinnedPosts = allBlogPosts.filter( + (post) => (post.data.category_pinned || 0) > 0, + ); + + const sorted = sortPosts(categoryPinnedPosts, true); + + return sorted.map((post) => ({ + slug: post.slug, + data: post.data, + })); +}