Commit c933bdf

HPCesia <me@hpcesia.com>
2025-01-26 16:42:08
refactor: extract Timeline component
1 parent 3d25ee8
Changed files (3)
src
components
pages
archives
src/components/Timeline.astro
@@ -0,0 +1,72 @@
+---
+// 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[];
+}
+
+const { items } = 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="relative flex flex-col">
+  <div
+    class="timeline absolute left-4 top-0 h-full w-0.5 bg-gradient-to-b from-blue-500 to-purple-500"
+  >
+  </div>
+  {
+    renderedItems.map((item) => (
+      <div class="relative mb-12 pl-12">
+        <div class="timeline-dot absolute left-2 top-5 h-4 w-4 rounded-full bg-blue-500" />
+        <Fragment set:html={item.title} />
+        <Fragment set:html={item.body} />
+      </div>
+    ))
+  }
+</div>
+
+<style>
+  .flex-col:empty {
+    @apply min-h-[3rem];
+  }
+
+  .timeline {
+    @apply opacity-60;
+  }
+
+  .timeline-dot {
+    @apply shadow-lg transition-transform duration-300;
+  }
+
+  .timeline-dot:hover {
+    @apply scale-125;
+  }
+</style>
src/pages/archives/categories/index.astro
@@ -1,6 +1,7 @@
 ---
 import type { BlogPostData } from '@/types/data';
 import { getCategories, 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 I18nKey from '@i18n/I18nKey';
@@ -16,32 +17,35 @@ categories.forEach((category) => {
   const posts = allPosts.filter((post) => post.data.category === category).slice(0, 3);
   categoryPosts.set(category, posts);
 });
-
 const uncategorizedPosts = allPosts.filter((post) => !post.data.category).slice(0, 3);
+if (uncategorizedPosts.length > 0)
+  categoryPosts.set(i18n(I18nKey.uncategorized), uncategorizedPosts);
 ---
 
 <MainLayout title={i18n(I18nKey.categories)}>
   <div class="mx-auto flex max-w-screen-xl flex-col items-center">
     <h1 class="my-8 text-3xl font-bold">{i18n(I18nKey.categories)}</h1>
-    <div class="relative flex flex-col">
-      <div
-        class="timeline absolute left-4 top-0 h-full w-0.5 bg-gradient-to-b from-blue-500 to-purple-500"
-      >
-      </div>
-      {
-        categories.map((category) => (
-          <div class="relative mb-12 pl-12">
-            <div class="timeline-dot absolute left-2 top-5 h-4 w-4 rounded-full bg-blue-500" />
+    <Timeline items={Array.from(categoryPosts.keys())}>
+      <fragment slot="title">
+        {
+          (category: string) => (
             <div class="mb-6 flex items-center justify-between">
               <h2 class="text-2xl font-bold">{category}</h2>
               <Button
-                href={`/archives/categories/${category.replaceAll(/[\\/]/g, '-')}/1/`}
+                href={`/archives/categories/${category === i18n(I18nKey.uncategorized) ? I18nKey.uncategorized : category.replaceAll(/[\\/]/g, '-')}/1/`}
                 title={category}
+                class="!pl-3"
               >
                 {i18n(I18nKey.more)}
                 <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
               </Button>
             </div>
+          )
+        }
+      </fragment>
+      <fragment slot="body">
+        {
+          (category: string) => (
             <div class="ml-4 flex flex-col gap-4">
               {categoryPosts.get(category)?.map((post) => (
                 <PostCard
@@ -55,57 +59,9 @@ const uncategorizedPosts = allPosts.filter((post) => !post.data.category).slice(
                 />
               ))}
             </div>
-          </div>
-        ))
-      }
-      {
-        uncategorizedPosts.length > 0 && (
-          <div class="relative mb-12 pl-12">
-            <div class="timeline-dot absolute left-2 top-5 h-4 w-4 rounded-full bg-blue-500" />
-            <div class="mb-6 flex items-center justify-between">
-              <h2 class="text-2xl font-bold">{i18n(I18nKey.uncategorized)}</h2>
-              <Button
-                href={`/archives/categories/uncategorized/1/`}
-                title={i18n(I18nKey.uncategorized)}
-              >
-                {i18n(I18nKey.more)}
-                <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
-              </Button>
-            </div>
-            <div class="ml-4 flex flex-col gap-4">
-              {uncategorizedPosts.map((post) => (
-                <PostCard
-                  title={post.data.title}
-                  url={`/posts/${post.data.abbrlink}/`}
-                  published={post.data.published}
-                  tags={post.data.tags || []}
-                  category={undefined}
-                  cover={post.data.cover}
-                  description={post.data.description}
-                />
-              ))}
-            </div>
-          </div>
-        )
-      }
-    </div>
+          )
+        }
+      </fragment>
+    </Timeline>
   </div>
 </MainLayout>
-
-<style>
-  .timeline {
-    @apply opacity-60;
-  }
-
-  .timeline-dot {
-    @apply shadow-lg transition-transform duration-300;
-  }
-
-  .timeline-dot:hover {
-    @apply scale-125;
-  }
-
-  .flex-col:empty {
-    @apply min-h-[3rem];
-  }
-</style>
src/pages/archives/tags/index.astro
@@ -1,6 +1,7 @@
 ---
 import type { BlogPostData } from '@/types/data';
 import { getSortedPosts, getTags } 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 I18nKey from '@i18n/I18nKey';
@@ -16,31 +17,36 @@ tags.forEach((tag) => {
   const posts = allPosts.filter((post) => post.data.tags?.includes(tag)).slice(0, 3);
   tagPosts.set(tag, posts);
 });
-
 const untaggedPosts = allPosts
   .filter((post) => !post.data.tags || post.data.tags.length === 0)
   .slice(0, 3);
+if (untaggedPosts.length > 0) tagPosts.set(i18n(I18nKey.untagged), untaggedPosts);
 ---
 
 <MainLayout title={i18n(I18nKey.tags)}>
   <div class="mx-auto flex max-w-screen-xl flex-col items-center">
     <h1 class="my-8 text-3xl font-bold">{i18n(I18nKey.tags)}</h1>
-    <div class="relative flex flex-col">
-      <div
-        class="timeline absolute left-4 top-0 h-full w-0.5 bg-gradient-to-b from-blue-500 to-purple-500"
-      >
-      </div>
-      {
-        tags.map((tag) => (
-          <div class="relative mb-12 pl-12">
-            <div class="timeline-dot absolute left-2 top-5 h-4 w-4 rounded-full bg-blue-500" />
+    <Timeline items={Array.from(tagPosts.keys())}>
+      <fragment slot="title">
+        {
+          (tag: string) => (
             <div class="mb-6 flex items-center justify-between">
               <h2 class="text-2xl font-bold">{tag}</h2>
-              <Button href={`/archives/tags/${tag.replaceAll(/[\\/]/g, '-')}/1/`} title={tag}>
+              <Button
+                href={`/archives/tags/${tag === i18n(I18nKey.untagged) ? I18nKey.untagged : tag.replaceAll(/[\\/]/g, '-')}/1/`}
+                title={tag}
+                class="!pl-3"
+              >
                 {i18n(I18nKey.more)}
                 <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
               </Button>
             </div>
+          )
+        }
+      </fragment>
+      <fragment slot="body">
+        {
+          (tag: string) => (
             <div class="ml-4 flex flex-col gap-4">
               {tagPosts.get(tag)?.map((post) => (
                 <PostCard
@@ -54,54 +60,9 @@ const untaggedPosts = allPosts
                 />
               ))}
             </div>
-          </div>
-        ))
-      }
-      {
-        untaggedPosts.length > 0 && (
-          <div class="relative mb-12 pl-12">
-            <div class="timeline-dot absolute left-2 top-5 h-4 w-4 rounded-full bg-blue-500" />
-            <div class="mb-6 flex items-center justify-between">
-              <h2 class="text-2xl font-bold">{i18n(I18nKey.untagged)}</h2>
-              <Button href={`/archives/tags/untagged/1/`} title={i18n(I18nKey.untagged)}>
-                {i18n(I18nKey.more)}
-                <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
-              </Button>
-            </div>
-            <div class="ml-4 flex flex-col gap-4">
-              {untaggedPosts.map((post) => (
-                <PostCard
-                  title={post.data.title}
-                  url={`/posts/${post.data.abbrlink}/`}
-                  published={post.data.published}
-                  tags={[]}
-                  category={post.data.category}
-                  cover={post.data.cover}
-                  description={post.data.description}
-                />
-              ))}
-            </div>
-          </div>
-        )
-      }
-    </div>
+          )
+        }
+      </fragment>
+    </Timeline>
   </div>
 </MainLayout>
-
-<style>
-  .timeline {
-    @apply opacity-60;
-  }
-
-  .timeline-dot {
-    @apply shadow-lg transition-transform duration-300;
-  }
-
-  .timeline-dot:hover {
-    @apply scale-125;
-  }
-
-  .flex-col:empty {
-    @apply min-h-[3rem];
-  }
-</style>