Commit f3d8da7

HPCesia <me@hpcesia.com>
2025-01-24 06:13:25
feat: categories and tags index
1 parent 8c1fce2
Changed files (2)
src
pages
archives
src/pages/archives/categories/index.astro
@@ -1,7 +1,111 @@
 ---
+import type { BlogPostData } from '@/types/data';
+import { getCategories, getSortedPosts } from '@/utils/content-utils';
+import Button from '@components/widgets/Button.astro';
+import PostCard from '@components/widgets/PostCard.astro';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
 import MainLayout from '@layouts/MainLayout.astro';
+import { Icon } from 'astro-icon/components';
+
+const categories = await getCategories();
+const allPosts = await getSortedPosts();
+
+const categoryPosts = new Map<string, { body: string; data: BlogPostData }[]>();
+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);
 ---
 
-<MainLayout>
-  <div></div>
+<MainLayout title={i18n(I18nKey.categories)}>
+  <div class="mx-auto max-w-screen-xl">
+    <h1 class="mb-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" />
+            <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/`}
+                title={category}
+              >
+                {i18n(I18nKey.more)}
+                <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
+              </Button>
+            </div>
+            <div class="ml-4 flex flex-col gap-4">
+              {categoryPosts.get(category)?.map((post) => (
+                <PostCard
+                  title={post.data.title}
+                  url={`/posts/${post.data.abbrlink}/`}
+                  published={post.data.published}
+                  tags={post.data.tags || []}
+                  category={post.data.category}
+                  cover={post.data.cover}
+                  description={post.data.description}
+                />
+              ))}
+            </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>
+  </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,7 +1,107 @@
 ---
+import type { BlogPostData } from '@/types/data';
+import { getSortedPosts, getTags } from '@/utils/content-utils';
+import Button from '@components/widgets/Button.astro';
+import PostCard from '@components/widgets/PostCard.astro';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
 import MainLayout from '@layouts/MainLayout.astro';
+import { Icon } from 'astro-icon/components';
+
+const tags = await getTags();
+const allPosts = await getSortedPosts();
+
+const tagPosts = new Map<string, { body: string; data: BlogPostData }[]>();
+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);
 ---
 
-<MainLayout>
-  <div></div>
+<MainLayout title={i18n(I18nKey.tags)}>
+  <div class="mx-auto max-w-screen-xl">
+    <h1 class="mb-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" />
+            <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}>
+                {i18n(I18nKey.more)}
+                <Icon name="material-symbols:chevron-right-rounded" class="text-2xl" />
+              </Button>
+            </div>
+            <div class="ml-4 flex flex-col gap-4">
+              {tagPosts.get(tag)?.map((post) => (
+                <PostCard
+                  title={post.data.title}
+                  url={`/posts/${post.data.abbrlink}/`}
+                  published={post.data.published}
+                  tags={post.data.tags || []}
+                  category={post.data.category}
+                  cover={post.data.cover}
+                  description={post.data.description}
+                />
+              ))}
+            </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>
+  </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>