Commit 02cee10

HPCesia <me@hpcesia.com>
2025-02-09 14:14:10
feat: refactor archive pages
- Use DaisyUI's timeline class for archive pages - Add toc to archive pages
1 parent 09e8ebb
Changed files (8)
src/components/Timeline.astro
@@ -1,55 +0,0 @@
----
-// Usage:
-//  <TimeLine items={items}>
-//    <fragement slot="title">
-//      {(item) => (titleHTML)}
-//    </fragement>
-//    <fragement slot="body">
-//      {(item) => (bodyHTML)}
-//    </fragement>
-//  </TimeLine>
-//
-// The result will same to:
-//  <div class="timeline-classes">
-//    {
-//      items.map((item) => (
-//        <div class="timeline-node-wrapper-classes">
-//          <div class="timeline-node-classes"></div>
-//          {titleHTML}
-//          {bodyHTML}
-//        </div>
-//    }
-//  </div>
-
-interface Props {
-  items: unknown[];
-  class?: string;
-}
-
-const { items, class: className } = Astro.props;
-
-const renderedItems = await Promise.all(
-  items.map(async (item) => {
-    return {
-      title: Astro.slots.has('title') ? await Astro.slots.render('title', [item]) : undefined,
-      body: Astro.slots.has('body') ? await Astro.slots.render('body', [item]) : undefined,
-    };
-  })
-);
----
-
-<div class:list={['relative flex flex-col empty:min-h-12', className]}>
-  <div
-    class="timeline absolute top-0 left-4 h-full w-0.5 bg-linear-to-b from-blue-500 to-purple-500 opacity-60"
-  >
-  </div>
-  {
-    renderedItems.map((item) => (
-      <div class="relative mb-12 pl-12">
-        <div class="absolute top-5 left-2 h-4 w-4 rounded-full bg-blue-500 shadow-lg transition-transform duration-300 hover:scale-125" />
-        <Fragment set:html={item.title} />
-        <Fragment set:html={item.body} />
-      </div>
-    ))
-  }
-</div>
src/i18n/langs/en.ts
@@ -25,6 +25,10 @@ export const en: Translation = {
   [Key.minutesCount]: 'minutes',
   [Key.postCount]: 'post',
   [Key.postsCount]: 'posts',
+  [Key.tagCount]: 'tag',
+  [Key.tagsCount]: 'tags',
+  [Key.categoryCount]: 'category',
+  [Key.categoriesCount]: 'categories',
 
   [Key.lightMode]: 'Light',
   [Key.darkMode]: 'Dark',
src/i18n/langs/zh_CN.ts
@@ -25,6 +25,10 @@ export const zh_CN: Translation = {
   [Key.minutesCount]: '分钟',
   [Key.postCount]: '篇文章',
   [Key.postsCount]: '篇文章',
+  [Key.tagCount]: '个标签',
+  [Key.tagsCount]: '个标签',
+  [Key.categoryCount]: '个分类',
+  [Key.categoriesCount]: '个分类',
 
   [Key.lightMode]: '亮色',
   [Key.darkMode]: '暗色',
src/i18n/langs/zh_TW.ts
@@ -25,6 +25,10 @@ export const zh_TW: Translation = {
   [Key.minutesCount]: '分鐘',
   [Key.postCount]: '篇文章',
   [Key.postsCount]: '篇文章',
+  [Key.tagCount]: '個標籤',
+  [Key.tagsCount]: '個標籤',
+  [Key.categoryCount]: '個分類',
+  [Key.categoriesCount]: '個分類',
 
   [Key.lightMode]: '亮色',
   [Key.darkMode]: '暗色',
src/i18n/I18nKey.ts
@@ -22,6 +22,10 @@ enum I18nKey {
   minutesCount = 'minutesCount',
   postCount = 'postCount',
   postsCount = 'postsCount',
+  tagCount = 'tagCount',
+  tagsCount = 'tagsCount',
+  categoryCount = 'categoryCount',
+  categoriesCount = 'categoriesCount',
 
   lightMode = 'lightMode',
   darkMode = 'darkMode',
src/pages/archives/categories/index.astro
@@ -1,12 +1,13 @@
 ---
 import type { BlogPostData } from '@/types/data';
 import { getCategories, getCategoryUrl, getSortedPosts } from '@/utils/content-utils';
-import Timeline from '@components/Timeline.astro';
 import Button from '@components/widgets/Button.astro';
-import PostCard from '@components/widgets/PostCard.astro';
+import MetaIcon from '@components/widgets/MetaIcon.astro';
+import ProfileCard from '@components/widgets/ProfileCard.astro';
+import TOC from '@components/widgets/TOC.astro';
 import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
-import MainLayout from '@layouts/MainLayout.astro';
+import GridLayout from '@layouts/GridLayout.astro';
 import { Icon } from 'astro-icon/components';
 
 const categories = await getCategories();
@@ -22,42 +23,106 @@ if (uncategorizedPosts.length > 0)
   categoryPosts.set(i18n(I18nKey.uncategorized) as string, uncategorizedPosts);
 ---
 
-<MainLayout title={i18n(I18nKey.categories)}>
-  <div class="mx-auto flex max-w-(--breakpoint-xl) flex-col items-center">
-    <h1 class="my-8 text-3xl font-bold">{i18n(I18nKey.categories)}</h1>
-    <Timeline items={Array.from(categoryPosts.keys())}>
-      <fragment slot="title">
+<GridLayout title={i18n(I18nKey.categories)}>
+  <div
+    class="card card-border border-base-300 mx-auto flex flex-col items-center border-2 px-6 py-4"
+  >
+    <div class="tooltip tooltip-right mx-auto w-fit">
+      <h1 class="text-center text-3xl font-bold">{i18n(I18nKey.categories)}</h1>
+      <div class="tooltip-content">
         {
-          (category: string) => (
-            <div class="mb-6 flex items-center justify-between">
-              <h2 class="text-2xl font-bold">{category}</h2>
-              <Button href={getCategoryUrl(category)} title={category} class="pl-3">
-                {i18n(I18nKey.more)}
-                <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
-              </Button>
-            </div>
-          )
+          [
+            `${allPosts.length} ${i18n(allPosts.length > 1 ? I18nKey.postsCount : I18nKey.postCount)}`,
+            ', ',
+            `${categories.length} ${i18n(categories.length > 1 ? I18nKey.categoriesCount : I18nKey.categoryCount)}`,
+          ]
         }
-      </fragment>
-      <fragment slot="body">
-        {
-          (category: string) => (
-            <div class="ml-4 flex flex-col gap-4">
-              {categoryPosts.get(category)?.map((post) => (
-                <PostCard
-                  title={post.data.title}
-                  url={`/posts/${post.data.slug}/`}
-                  published={post.data.published}
-                  tags={post.data.tags || []}
-                  category={post.data.category}
-                  cover={post.data.cover}
-                  description={post.data.description}
-                />
-              ))}
+      </div>
+    </div>
+    <ul
+      class="timeline timeline-snap-icon timeline-vertical max-md:timeline-compact w-full p-4"
+    >
+      {
+        Array.from(categoryPosts.entries()).map(([category, posts], index) => (
+          <li>
+            {index > 0 && <hr />}
+            <div class="timeline-middle">
+              <Icon name="material-symbols:add-circle-rounded" class="text-xl" />
             </div>
-          )
-        }
-      </fragment>
-    </Timeline>
+            <div class:list={[`timeline-${index % 2 === 0 ? 'start' : 'end'}`, 'w-full']}>
+              <div
+                class:list={[
+                  index % 2 === 0 && 'md:flex-row-reverse',
+                  'mx-4 flex flex-row items-center justify-between',
+                ]}
+              >
+                <h2 class="scroll-mt-20 text-2xl font-bold" id={`heading-${category}`}>
+                  {category}
+                </h2>
+                <Button href={getCategoryUrl(category)} title={category} class="pl-3">
+                  {i18n(I18nKey.more)}
+                  <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
+                </Button>
+              </div>
+              <ul class="list">
+                {posts.map(({ data }) => (
+                  <li class="list-row">
+                    <div class="list-col-grow">
+                      <a
+                        href={`/posts/${data.slug}`}
+                        title={data.title}
+                        class="text-lg font-bold"
+                      >
+                        {data.title}
+                      </a>
+                      <div class="text-base-content/60 mt-2 flex flex-wrap items-start gap-x-4 gap-y-2 text-sm">
+                        {[
+                          {
+                            icon: 'material-symbols:category-outline-rounded',
+                            text: data.category || i18n(I18nKey.uncategorized),
+                            link: getCategoryUrl(data.category),
+                          },
+                          ...(data.tags?.map((tag) => {
+                            return {
+                              icon: 'material-symbols:tag-rounded',
+                              text: tag,
+                              link: getCategoryUrl(tag),
+                            };
+                          }) || []),
+                        ].map((meta) => (
+                          <div class="flex items-center gap-0">
+                            <MetaIcon name={meta.icon} />
+                            {meta.link ? (
+                              <a href={meta.link} class="meta-text" title={meta.text}>
+                                {meta.text}
+                              </a>
+                            ) : (
+                              <span class="meta-text">{meta.text}</span>
+                            )}
+                          </div>
+                        ))}
+                      </div>
+                    </div>
+                  </li>
+                ))}
+              </ul>
+            </div>
+            <hr />
+          </li>
+        ))
+      }
+    </ul>
   </div>
-</MainLayout>
+  <Fragment slot="aside-fixed">
+    <ProfileCard />
+  </Fragment>
+  <Fragment slot="aside-sticky">
+    <TOC
+      headings={categories.map((category) => ({
+        text: category,
+        slug: `heading-${category}`,
+        depth: 2,
+      }))}
+    />
+  </Fragment>
+</GridLayout>
src/pages/archives/tags/index.astro
@@ -1,12 +1,13 @@
 ---
 import type { BlogPostData } from '@/types/data';
-import { getSortedPosts, getTags, getTagUrl } from '@/utils/content-utils';
-import Timeline from '@components/Timeline.astro';
+import { getCategoryUrl, getSortedPosts, getTags, getTagUrl } from '@/utils/content-utils';
 import Button from '@components/widgets/Button.astro';
-import PostCard from '@components/widgets/PostCard.astro';
+import MetaIcon from '@components/widgets/MetaIcon.astro';
+import ProfileCard from '@components/widgets/ProfileCard.astro';
+import TOC from '@components/widgets/TOC.astro';
 import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
-import MainLayout from '@layouts/MainLayout.astro';
+import GridLayout from '@layouts/GridLayout.astro';
 import { Icon } from 'astro-icon/components';
 
 const tags = await getTags();
@@ -23,42 +24,106 @@ const untaggedPosts = allPosts
 if (untaggedPosts.length > 0) tagPosts.set(i18n(I18nKey.untagged) as string, untaggedPosts);
 ---
 
-<MainLayout title={i18n(I18nKey.tags)}>
-  <div class="mx-auto flex max-w-(--breakpoint-xl) flex-col items-center">
-    <h1 class="my-8 text-3xl font-bold">{i18n(I18nKey.tags)}</h1>
-    <Timeline items={Array.from(tagPosts.keys())}>
-      <fragment slot="title">
+<GridLayout title={i18n(I18nKey.tags)}>
+  <div
+    class="card card-border border-base-300 mx-auto flex flex-col items-center border-2 px-6 py-4"
+  >
+    <div class="tooltip tooltip-right mx-auto w-fit">
+      <h1 class="text-center text-3xl font-bold">{i18n(I18nKey.tags)}</h1>
+      <div class="tooltip-content">
         {
-          (tag: string) => (
-            <div class="mb-6 flex items-center justify-between">
-              <h2 class="text-2xl font-bold">{tag}</h2>
-              <Button href={getTagUrl(tag)} title={tag} class="pl-3">
-                {i18n(I18nKey.more)}
-                <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
-              </Button>
-            </div>
-          )
+          [
+            `${allPosts.length} ${i18n(allPosts.length > 1 ? I18nKey.postsCount : I18nKey.postCount)}`,
+            ', ',
+            `${tags.length} ${i18n(tags.length > 1 ? I18nKey.tagsCount : I18nKey.tagCount)}`,
+          ]
         }
-      </fragment>
-      <fragment slot="body">
-        {
-          (tag: string) => (
-            <div class="ml-4 flex flex-col gap-4">
-              {tagPosts.get(tag)?.map((post) => (
-                <PostCard
-                  title={post.data.title}
-                  url={`/posts/${post.data.slug}/`}
-                  published={post.data.published}
-                  tags={post.data.tags || []}
-                  category={post.data.category}
-                  cover={post.data.cover}
-                  description={post.data.description}
-                />
-              ))}
+      </div>
+    </div>
+    <ul
+      class="timeline timeline-snap-icon timeline-vertical max-md:timeline-compact w-full p-4"
+    >
+      {
+        Array.from(tagPosts.entries()).map(([tag, posts], index) => (
+          <li>
+            {index > 0 && <hr />}
+            <div class="timeline-middle">
+              <Icon name="material-symbols:add-circle-rounded" class="text-xl" />
             </div>
-          )
-        }
-      </fragment>
-    </Timeline>
+            <div class:list={[`timeline-${index % 2 === 0 ? 'start' : 'end'}`, 'w-full']}>
+              <div
+                class:list={[
+                  index % 2 === 0 && 'md:flex-row-reverse',
+                  'mx-4 flex flex-row items-center justify-between',
+                ]}
+              >
+                <h2 class="scroll-mt-20 text-2xl font-bold" id={`heading-${tag}`}>
+                  {tag}
+                </h2>
+                <Button href={getTagUrl(tag)} title={tag} class="pl-3">
+                  {i18n(I18nKey.more)}
+                  <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
+                </Button>
+              </div>
+              <ul class="list">
+                {posts.map(({ data }) => (
+                  <li class="list-row">
+                    <div class="list-col-grow">
+                      <a
+                        href={`/posts/${data.slug}`}
+                        title={data.title}
+                        class="text-lg font-bold"
+                      >
+                        {data.title}
+                      </a>
+                      <div class="text-base-content/60 mt-2 flex flex-wrap items-start gap-x-4 gap-y-2 text-sm">
+                        {[
+                          {
+                            icon: 'material-symbols:category-outline-rounded',
+                            text: data.category || i18n(I18nKey.uncategorized),
+                            link: getCategoryUrl(data.category),
+                          },
+                          ...data.tags.map((tag) => {
+                            return {
+                              icon: 'material-symbols:tag-rounded',
+                              text: tag,
+                              link: getTagUrl(tag),
+                            };
+                          }),
+                        ].map((meta) => (
+                          <div class="flex items-center gap-0">
+                            <MetaIcon name={meta.icon} />
+                            {meta.link ? (
+                              <a href={meta.link} class="meta-text" title={meta.text}>
+                                {meta.text}
+                              </a>
+                            ) : (
+                              <span class="meta-text">{meta.text}</span>
+                            )}
+                          </div>
+                        ))}
+                      </div>
+                    </div>
+                  </li>
+                ))}
+              </ul>
+            </div>
+            <hr />
+          </li>
+        ))
+      }
+    </ul>
   </div>
-</MainLayout>
+  <Fragment slot="aside-fixed">
+    <ProfileCard />
+  </Fragment>
+  <Fragment slot="aside-sticky">
+    <TOC
+      headings={tags.map((tag) => ({
+        text: tag,
+        slug: `heading-${tag}`,
+        depth: 2,
+      }))}
+    />
+  </Fragment>
+</GridLayout>
src/pages/archives/[...time].astro
@@ -1,6 +1,7 @@
 ---
 import { siteConfig } from '@/config';
 import type { BlogPostData } from '@/types/data';
+import Button from '@components/widgets/Button.astro';
 import MetaIcon from '@components/widgets/MetaIcon.astro';
 import ProfileCard from '@components/widgets/ProfileCard.astro';
 import TOC from '@components/widgets/TOC.astro';
@@ -8,6 +9,7 @@ import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
 import GridLayout from '@layouts/GridLayout.astro';
 import { getCategoryUrl, getPostsCount, getSortedPosts, getTagUrl } from '@utils/content-utils';
+import { Icon } from 'astro-icon/components';
 
 export async function getStaticPaths() {
   const allBlogPosts = await getSortedPosts();
@@ -99,9 +101,11 @@ const postCount = await getPostsCount();
     </div>
   </div>
   <div class="card card-bordered border-base-300 border-2 px-6 py-4">
-    <h1 class="mb-2 text-center text-3xl font-bold">{i18n(I18nKey.archive)}</h1>
-    <div class="text-base-content/80 text-center">
-      {`${postCount} ${i18n(postCount > 1 ? I18nKey.postsCount : I18nKey.postCount)}`}
+    <div class="tooltip tooltip-right mx-auto w-fit">
+      <h1 class="text-center text-3xl font-bold">{i18n(I18nKey.archive)}</h1>
+      <div class="tooltip-content">
+        {`${postCount} ${i18n(postCount > 1 ? I18nKey.postsCount : I18nKey.postCount)}`}
+      </div>
     </div>
     {
       (() => {
@@ -166,23 +170,39 @@ const postCount = await getPostsCount();
           if (!yearData) {
             return <p>SHOULD NOT RENDER THIS, IS A BUG</p>;
           }
-          return yearData.map(({ month }) => (
-            <>
-              <div
-                class="divider mx-3 mt-8 scroll-mt-20 text-xl font-bold"
-                id={`${year}/${month}`}
-              >
-                <a
-                  href={`/archives/${year}/${month}`}
-                  title={`${year}/${month}`}
-                  class="hover:text-primary duration-200"
-                >
-                  {month}
-                </a>
-              </div>
-              <div class="mx-2">{renderMonth(year, month)}</div>
-            </>
-          ));
+          return (
+            <ul class="timeline timeline-snap-icon timeline-vertical max-md:timeline-compact w-full">
+              {yearData.map(({ month }, index) => (
+                <li>
+                  {index > 0 && <hr />}
+                  <div class="timeline-middle">
+                    <Icon name="material-symbols:add-circle-rounded" class="text-xl" />
+                  </div>
+                  <div class:list={[`timeline-${index % 2 === 0 ? 'start' : 'end'}`, 'w-full']}>
+                    <div
+                      class:list={[
+                        index % 2 === 0 && 'md:flex-row-reverse',
+                        'mx-4 flex scroll-mt-20 flex-row items-center justify-between',
+                      ]}
+                      id={`${year}-${month}`}
+                    >
+                      <div class="text-2xl font-bold">{month}</div>
+                      <Button
+                        href={`/archives/${year}/${month}`}
+                        title={`${year}/${month}`}
+                        class="pl-3"
+                      >
+                        {i18n(I18nKey.more)}
+                        <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
+                      </Button>
+                    </div>
+                    <div class="mx-2">{renderMonth(year, month)}</div>
+                  </div>
+                  <hr />
+                </li>
+              ))}
+            </ul>
+          );
         }
 
         function renderAll() {
@@ -230,7 +250,7 @@ const postCount = await getPostsCount();
             ...data.map(({ month }) => ({
               depth: 3,
               text: month.toString(),
-              slug: `${year}/${month}`,
+              slug: `${year}-${month}`,
             })),
           ])}
         />