From 2d10f8016fe5e50aab7514513421dd7df1bf218f Mon Sep 17 00:00:00 2001 From: KazooTTT Date: Sat, 30 Nov 2024 18:21:58 +0800 Subject: [PATCH] feat: add dir tree --- astro.config.mjs | 2 +- src/components/CategoriesView.tsx | 113 ++++++++++++++++++ src/pages/blog/[...page].astro | 23 ++-- .../categories/[category]/[...page].astro | 18 ++- src/pages/categories/index.astro | 40 +++---- src/types.ts | 8 ++ src/utils/post.ts | 78 ++++++++++++ 7 files changed, 244 insertions(+), 38 deletions(-) create mode 100644 src/components/CategoriesView.tsx diff --git a/astro.config.mjs b/astro.config.mjs index a47d7a8..2888f32 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -30,7 +30,7 @@ export default defineConfig({ } }), redirects: { - '/articles': { + '/article': { status: 301, destination: '/posts' }, diff --git a/src/components/CategoriesView.tsx b/src/components/CategoriesView.tsx new file mode 100644 index 0000000..071764f --- /dev/null +++ b/src/components/CategoriesView.tsx @@ -0,0 +1,113 @@ +import type { CategoryHierarchy } from '@/types' +import React, { useState } from 'react' + +interface CategoriesViewProps { + allCategories: [string, number][] + allCategoriesHierarchy: CategoryHierarchy[] + defaultViewMode?: 'tree' | 'list' +} + +const CategoriesView: React.FC = ({ + allCategories, + allCategoriesHierarchy, + defaultViewMode = 'tree' +}) => { + const [viewMode, setViewMode] = useState<'tree' | 'list'>(defaultViewMode) + + return ( +
+
+

Categories

+ +
+ View: +
+ + +
+
+
+ + {allCategoriesHierarchy.length === 0 &&

No posts yet.

} + + {/* List View */} + {viewMode === 'list' && ( +
+ {allCategories.map(([category, count]) => ( +
+ + {category} + + + - {count} post{count > 1 && 's'} + +
+ ))} +
+ )} + + {/* Tree View */} + {viewMode === 'tree' && ( +
+ {allCategoriesHierarchy.map((category) => ( +
+ {/* Render root-level categories */} +
+ + {category.category} + + + - {category.count} post{category.count > 1 && 's'} + +
+ + {/* Render nested categories with indentation */} + {category.children && Object.values(category.children).length > 0 && ( +
+ {Object.values(category.children).map((childCategory) => ( +
+ + {childCategory.category} + + + - {childCategory.count} post{childCategory.count > 1 && 's'} + +
+ ))} +
+ )} +
+ ))} +
+ )} +
+ ) +} + +export default CategoriesView diff --git a/src/pages/blog/[...page].astro b/src/pages/blog/[...page].astro index 1a91884..85435c1 100644 --- a/src/pages/blog/[...page].astro +++ b/src/pages/blog/[...page].astro @@ -8,20 +8,25 @@ import Button from '@/components/Button.astro' import Pagination from '@/components/Paginator.astro' import PostPreview from '@/components/blog/PostPreview.astro' import PageLayout from '@/layouts/BaseLayout.astro' -import { getAllPosts, getUniqueCategories, getUniqueTags, sortMDByDate } from '@/utils' +import { + getAllPosts, + getUniqueCategoriesWithCount, + getUniqueTagsWithCount, + sortMDByDate +} from '@/utils' export const getStaticPaths = (async ({ paginate }) => { const allPosts = await getAllPosts() const allPostsByDate = sortMDByDate(allPosts) - const uniqueTags = getUniqueTags(allPosts) - const uniqueCategories = getUniqueCategories(allPosts) + const uniqueTags = getUniqueTagsWithCount(allPosts) + const uniqueCategories = getUniqueCategoriesWithCount(allPosts) return paginate(allPostsByDate, { pageSize: 50, props: { uniqueTags, uniqueCategories } }) }) satisfies GetStaticPaths interface Props { page: Page> - uniqueTags: string[] - uniqueCategories: string[] + uniqueTags: Array<[string, number]> + uniqueCategories: Array<[string, number]> } const { page, uniqueTags, uniqueCategories } = Astro.props @@ -104,7 +109,11 @@ const paginationProps = {
    {uniqueCategories.slice(0, 6).map((category) => (
  • -
  • ))}
@@ -145,7 +154,7 @@ const paginationProps = {
    {uniqueTags.slice(0, 6).map((tag) => (
  • -
  • ))}
diff --git a/src/pages/categories/[category]/[...page].astro b/src/pages/categories/[category]/[...page].astro index 28de28f..b4414c3 100644 --- a/src/pages/categories/[category]/[...page].astro +++ b/src/pages/categories/[category]/[...page].astro @@ -8,16 +8,26 @@ import Pagination from '@/components/Paginator.astro' import PostPreview from '@/components/blog/PostPreview.astro' import PageLayout from '@/layouts/BaseLayout.astro' import Button from '@/components/Button.astro' -import { getAllPosts, getUniqueCategories, sortMDByDate } from '@/utils' +import { getAllPosts, sortMDByDate } from '@/utils' +import { getCategoriesGroupByName } from 'src/utils/post' export const getStaticPaths: GetStaticPaths = async ({ paginate }) => { const allPosts = await getAllPosts() const allPostsByDate = sortMDByDate(allPosts) - const uniqueCategories = getUniqueCategories(allPostsByDate) + const categoriesHierarchy = getCategoriesGroupByName(allPostsByDate) - return uniqueCategories.flatMap((category) => { + // Flatten the hierarchy to get all possible category paths + const allCategories = categoriesHierarchy.flatMap((category) => { + const result = [category.fullCategory] + Object.values(category.children).forEach((child) => { + result.push(child.fullCategory) + }) + return result + }) + + return allCategories.flatMap((category) => { const filterPosts = allPostsByDate.filter((post) => - category === '未分类' ? !post.data.category : post.data.category === category + category === '未分类' ? !post.data?.category : post.data.category?.startsWith(category) ) return paginate(filterPosts, { pageSize: 50, diff --git a/src/pages/categories/index.astro b/src/pages/categories/index.astro index 61d37b6..5819965 100644 --- a/src/pages/categories/index.astro +++ b/src/pages/categories/index.astro @@ -1,15 +1,22 @@ --- import Button from '@/components/Button.astro' +import type { CategoryHierarchy } from 'src/types' import PageLayout from '@/layouts/BaseLayout.astro' import { getAllPosts, getUniqueCategoriesWithCount } from '@/utils' +import { getCategoriesGroupByName } from 'src/utils/post' +import CategoriesView from 'src/components/CategoriesView' const allPosts = await getAllPosts() const allCategories = getUniqueCategoriesWithCount(allPosts) +const allCategoriesHierarchy = getCategoriesGroupByName(allPosts) as CategoryHierarchy[] const meta = { description: "A list of all the topics I've written about in my posts", - title: 'All Categories' + title: 'All Categories' } + +// Default to tree view +const defaultViewMode = 'tree' --- @@ -29,30 +36,11 @@ const meta = { - -

Categories

- {allCategories.length === 0 &&

No posts yet.

} - - { - allCategories.length > 0 && ( -
    - {allCategories.map(([category, val]) => ( -
  • - - #{category} - - - - {val} post{val > 1 && 's'} - -
  • - ))} -
- ) - } +
diff --git a/src/types.ts b/src/types.ts index 29eed6d..6055e8b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,3 +23,11 @@ export type SiteMeta = { ogImage?: string | undefined articleDate?: string | undefined } + + +export interface CategoryHierarchy { + category: string + fullCategory: string + children: Record + count: number +} \ No newline at end of file diff --git a/src/utils/post.ts b/src/utils/post.ts index ee23391..0b73a2b 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -1,3 +1,4 @@ +import type { CategoryHierarchy } from '@/types' import type { CollectionEntry } from 'astro:content' import { getCollection } from 'astro:content' @@ -82,6 +83,83 @@ export function getUniqueCategoriesWithCount( ].sort((a, b) => b[1] - a[1]) } +export function getCategoriesGroupByName(posts: Array>): CategoryHierarchy[] { + const categories = getUniqueCategoriesWithCount(posts) + const hierarchicalCategories: CategoryHierarchy[] = [] + + categories.forEach(([fullName, count]) => { + const parts = fullName.split('-') + let current = hierarchicalCategories + + parts.forEach((part, index) => { + // If it's the last part, add count + if (index === parts.length - 1) { + // Check if category already exists + let categoryObj = current.find(cat => cat.category === parts[0]) + + if (!categoryObj) { + categoryObj = { + category: parts[0], + fullCategory: parts[0], + children: {}, + count: 0 + } + current.push(categoryObj) + } + + // If it's a nested category + if (parts.length > 1) { + if (!categoryObj.children[part]) { + categoryObj.children[part] = { + category: part, + fullCategory: `${categoryObj.fullCategory}-${part}`, + children: {}, + count + } + } else { + categoryObj.children[part].count = count + } + } else { + // Top-level category + categoryObj.count = count + } + } else { + // Ensure top-level category exists + let categoryObj = current.find(cat => cat.fullCategory === part) + if (!categoryObj) { + categoryObj = { + category: part, + fullCategory: part, + children: {}, + count: 0 + } + current.push(categoryObj) + } + } + }) + }) + + // Calculate total count for each category by summing subcategories + hierarchicalCategories.forEach(category => { + if (Object.keys(category.children).length > 0) { + category.count = Object.values(category.children) + .reduce((sum, child) => sum + (child.count || 0), 0) + } + }) + + // Filter out categories with zero count and sort by count + return hierarchicalCategories + .filter(category => category.count > 0) + .map(category => ({ + ...category, + children: Object.fromEntries( + Object.entries(category.children) + .filter(([_, child]) => child.count > 0) + ) + })) + .sort((a, b) => b.count - a.count) +} + export function getIdToSlugMap(posts: Array>): Record { return posts.reduce( (acc, post) => {