feat: allow collapsing widget content, add categories widget
(cherry picked from commit 9a4ca8f6163d5e1375aa7c612e1338cce5a8c0b5)
This commit is contained in:
parent
83b765a398
commit
f4dc88e982
|
@ -7,7 +7,8 @@ interface Props {
|
||||||
const { keyword, tags, categories} = Astro.props;
|
const { keyword, tags, categories} = Astro.props;
|
||||||
|
|
||||||
import Button from "./control/Button.astro";
|
import Button from "./control/Button.astro";
|
||||||
import {getPostUrlBySlug, getSortedPosts} from "../utils/content-utils";
|
import {getSortedPosts} from "../utils/content-utils";
|
||||||
|
import {getPostUrlBySlug} from "../utils/url-utils";
|
||||||
|
|
||||||
let posts = await getSortedPosts()
|
let posts = await getSortedPosts()
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,10 @@ function getLinkName(name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
<div class:list={[
|
<div transition:animate="none" class:list={[
|
||||||
className,
|
className,
|
||||||
"card-base max-w-[var(--page-width)] h-[72px] rounded-t-none mx-auto flex items-center justify-between px-4"]}>
|
"card-base sticky top-0 overflow-visible max-w-[var(--page-width)] h-[72px] rounded-t-none mx-auto flex items-center justify-between px-4"]}>
|
||||||
<a href="/"><Button height="52px" class="px-5 font-bold rounded-lg" light>
|
<a href="/page/1"><Button height="52px" class="px-5 font-bold rounded-lg" light>
|
||||||
<div class="flex flex-row text-[var(--primary)] items-center text-md">
|
<div class="flex flex-row text-[var(--primary)] items-center text-md">
|
||||||
<Icon name="material-symbols:home-outline-rounded" size={28} class="mb-1 mr-2" />
|
<Icon name="material-symbols:home-outline-rounded" size={28} class="mb-1 mr-2" />
|
||||||
{getConfig().title}
|
{getConfig().title}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
import WidgetLayout from "./WidgetLayout.astro";
|
||||||
|
|
||||||
|
import {i18n} from "../../i18n/translation";
|
||||||
|
import I18nKey from "../../i18n/i18nKey";
|
||||||
|
import {CategoryMap, getCategoryMap} from "../../utils/content-utils";
|
||||||
|
import CategoriesLink from "./CategoriesLink.astro";
|
||||||
|
|
||||||
|
const categories = await getCategoryMap();
|
||||||
|
|
||||||
|
const COLLAPSED_HEIGHT = "120px";
|
||||||
|
const COLLAPSE_THRESHOLD = 5;
|
||||||
|
|
||||||
|
function count(categoryMap: CategoryMap): number {
|
||||||
|
let res = 0;
|
||||||
|
for (const key in categoryMap) {
|
||||||
|
res++;
|
||||||
|
res += count(categoryMap[key].children);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCollapsed = count(categories) >= COLLAPSE_THRESHOLD;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
categories: CategoryMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}>
|
||||||
|
<CategoriesLink categories={categories}></CategoriesLink>
|
||||||
|
</WidgetLayout>
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
import {CategoryMap} from "../../utils/content-utils";
|
||||||
|
import {getCategoryUrl} from "../../utils/url-utils";
|
||||||
|
import ButtonLink from "../control/ButtonLink.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
categories: CategoryMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {categories} = Astro.props;
|
||||||
|
|
||||||
|
---
|
||||||
|
<div>
|
||||||
|
{Object.entries(categories).map(([key, value]) =>
|
||||||
|
<ButtonLink url={getCategoryUrl(key)} badge={value.count}>{value.name}</ButtonLink>
|
||||||
|
<div class="ml-2">
|
||||||
|
{Object.keys(value.children).length > 0 && <Astro.self categories={value.children}></Astro.self>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
|
@ -1,17 +1,17 @@
|
||||||
---
|
---
|
||||||
import WidgetLayout from "./WidgetLayout.astro";
|
import WidgetLayout from "./WidgetLayout.astro";
|
||||||
import ButtonLink from "../control/ButtonLink.astro";
|
import ButtonLink from "../control/ButtonLink.astro";
|
||||||
import {getPostUrlBySlug, getSortedPosts} from "../../utils/content-utils";
|
import {getSortedPosts} from "../../utils/content-utils";
|
||||||
import {i18n} from "../../i18n/translation";
|
import {i18n} from "../../i18n/translation";
|
||||||
import I18nKey from "../../i18n/i18nKey";
|
import I18nKey from "../../i18n/i18nKey";
|
||||||
|
import {getPostUrlBySlug} from "../../utils/url-utils";
|
||||||
|
|
||||||
let posts = await getSortedPosts()
|
let posts = await getSortedPosts()
|
||||||
|
|
||||||
const LIMIT = 5;
|
const LIMIT = 3;
|
||||||
|
|
||||||
posts = posts.slice(0, LIMIT)
|
posts = posts.slice(0, LIMIT)
|
||||||
|
|
||||||
// console.log(posts)
|
|
||||||
---
|
---
|
||||||
<WidgetLayout name={i18n(I18nKey.recentPosts)}>
|
<WidgetLayout name={i18n(I18nKey.recentPosts)}>
|
||||||
{posts.map(post =>
|
{posts.map(post =>
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
---
|
---
|
||||||
import Profile from "./Profile.astro";
|
import Profile from "./Profile.astro";
|
||||||
import RecentPost from "./RecentPost.astro";
|
import RecentPost from "./RecentPost.astro";
|
||||||
import Tag from "./Tag.astro";
|
import Tag from "./Tags.astro";
|
||||||
|
import Categories from "./Categories.astro";
|
||||||
|
|
||||||
const className = Astro.props.class;
|
const className = Astro.props.class;
|
||||||
---
|
---
|
||||||
<div id="sidebar" class:list={[className, "flex flex-col w-full gap-4"]} transition:persist>
|
<div id="sidebar" class:list={[className, "w-full"]} transition:persist>
|
||||||
<Profile></Profile>
|
<div class="flex flex-col w-full gap-4 mb-4">
|
||||||
<RecentPost></RecentPost>
|
<Profile></Profile>
|
||||||
<Tag></Tag>
|
</div>
|
||||||
|
<div class="flex flex-col w-full gap-4 top-4 sticky top-4" transition:animate="none">
|
||||||
|
<Categories></Categories>
|
||||||
|
<RecentPost></RecentPost>
|
||||||
|
<Tag></Tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -8,8 +8,12 @@ import I18nKey from "../../i18n/i18nKey";
|
||||||
|
|
||||||
const tags = await getTagList();
|
const tags = await getTagList();
|
||||||
|
|
||||||
|
const COLLAPSED_HEIGHT = "120px";
|
||||||
|
|
||||||
|
const isCollapsed = tags.length >= 20;
|
||||||
|
|
||||||
---
|
---
|
||||||
<WidgetLayout name={i18n(I18nKey.tags)}>
|
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
{tags.map(t => (
|
{tags.map(t => (
|
||||||
<ButtonTag href={`/archive/tag/${t.name}`}>
|
<ButtonTag href={`/archive/tag/${t.name}`}>
|
|
@ -1,18 +1,62 @@
|
||||||
---
|
---
|
||||||
|
import Button from "../control/Button.astro";
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import {i18n} from "../../i18n/translation";
|
||||||
|
import I18nKey from "../../i18n/i18nKey";
|
||||||
interface Props {
|
interface Props {
|
||||||
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
collapsedHeight?: string;
|
||||||
}
|
}
|
||||||
const props = Astro.props;
|
const props = Astro.props;
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
|
isCollapsed,
|
||||||
|
collapsedHeight,
|
||||||
} = Astro.props
|
} = Astro.props
|
||||||
|
|
||||||
---
|
---
|
||||||
<div class="pb-4 card-base">
|
<widget-layout data-id={id} data-isCollapsed={isCollapsed} class="pb-4 card-base">
|
||||||
<div class="font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-8 mt-4 mb-2
|
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
|
||||||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||||||
before:absolute before:left-[-16px] before:top-[5.5px]">{name}</div>
|
before:absolute before:left-[-16px] before:top-[5.5px]">{name}</div>
|
||||||
<div class="px-4">
|
<div id={id} class:list={["collapse-wrapper px-4 overflow-hidden", {"collapsed": isCollapsed}]}>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{isCollapsed && <div class="expand-btn px-4 -mb-2">
|
||||||
|
<Button light class=" w-full rounded-lg" height="36px">
|
||||||
|
<div class="text-[var(--primary)] flex items-center justify-center gap-2 -translate-x-2">
|
||||||
|
<Icon name="material-symbols:more-horiz" size={28}></Icon> {i18n(I18nKey.more)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>}
|
||||||
|
</widget-layout>
|
||||||
|
|
||||||
|
<style define:vars={{ collapsedHeight }}>
|
||||||
|
.collapsed {
|
||||||
|
height: var(--collapsedHeight);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class WidgetLayout extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (!this.dataset.isCollapsed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const id = this.dataset.id;
|
||||||
|
const btn = this.querySelector('.expand-btn');
|
||||||
|
const wrapper = this.querySelector(`#${id}`)
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
wrapper.classList.remove('collapsed');
|
||||||
|
btn.classList.add('hidden');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('widget-layout', WidgetLayout);
|
||||||
|
</script>
|
|
@ -21,6 +21,8 @@ enum I18nKey {
|
||||||
postsCount = "postsCount",
|
postsCount = "postsCount",
|
||||||
|
|
||||||
primaryColor = "primaryColor",
|
primaryColor = "primaryColor",
|
||||||
|
|
||||||
|
more = "more",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default I18nKey;
|
export default I18nKey;
|
|
@ -24,4 +24,6 @@ export const en: Translation = {
|
||||||
[Key.postsCount]: "posts",
|
[Key.postsCount]: "posts",
|
||||||
|
|
||||||
[Key.primaryColor]: "Primary Color",
|
[Key.primaryColor]: "Primary Color",
|
||||||
|
|
||||||
|
[Key.more]: "More",
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,4 +24,6 @@ export const zh_CN: Translation = {
|
||||||
[Key.postsCount]: "篇文章",
|
[Key.postsCount]: "篇文章",
|
||||||
|
|
||||||
[Key.primaryColor]: "主题色",
|
[Key.primaryColor]: "主题色",
|
||||||
|
|
||||||
|
[Key.more]: "更多",
|
||||||
};
|
};
|
|
@ -24,4 +24,6 @@ export const zh_TW: Translation = {
|
||||||
[Key.postsCount]: "篇文章",
|
[Key.postsCount]: "篇文章",
|
||||||
|
|
||||||
[Key.primaryColor]: "主題色",
|
[Key.primaryColor]: "主題色",
|
||||||
|
|
||||||
|
[Key.more]: "更多",
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
---
|
---
|
||||||
import { getCollection, getEntry } from "astro:content";
|
|
||||||
import MainGridLayout from "../../layouts/MainGridLayout.astro";
|
import MainGridLayout from "../../layouts/MainGridLayout.astro";
|
||||||
import TitleCard from "../../components/TitleCardNew.astro";
|
import TitleCard from "../../components/TitleCardNew.astro";
|
||||||
import Pagination from "../../components/control/Pagination.astro";
|
import Pagination from "../../components/control/Pagination.astro";
|
||||||
import {getPostUrlBySlug, getSortedPosts} from "../../utils/content-utils";
|
import {getSortedPosts} from "../../utils/content-utils";
|
||||||
import {getConfig} from "../../utils/config-utils";
|
import {getPostUrlBySlug} from "../../utils/url-utils";
|
||||||
|
|
||||||
export async function getStaticPaths({ paginate }) {
|
export async function getStaticPaths({ paginate }) {
|
||||||
// const allBlogPosts = await getCollection("posts");
|
|
||||||
const allBlogPosts = await getSortedPosts();
|
const allBlogPosts = await getSortedPosts();
|
||||||
return paginate(allBlogPosts, { pageSize: 6 });
|
return paginate(allBlogPosts, { pageSize: 6 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const {page} = Astro.props;
|
const {page} = Astro.props;
|
||||||
|
|
||||||
// page.data.map(entry => console.log(entry));
|
|
||||||
// console.log(page)
|
|
||||||
|
|
||||||
// console.log(getConfig());
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- 显示当前页面。也可以使用 Astro.params.page -->
|
<!-- 显示当前页面。也可以使用 Astro.params.page -->
|
||||||
|
|
|
@ -4,11 +4,11 @@ import MainGridLayout from "../../layouts/MainGridLayout.astro";
|
||||||
import ImageBox from "../../components/misc/ImageBox.astro";
|
import ImageBox from "../../components/misc/ImageBox.astro";
|
||||||
import {Icon} from "astro-icon/components";
|
import {Icon} from "astro-icon/components";
|
||||||
import PostMetadata from "../../components/PostMetadata.astro";
|
import PostMetadata from "../../components/PostMetadata.astro";
|
||||||
import {getPostUrlBySlug} from "../../utils/content-utils";
|
|
||||||
import Button from "../../components/control/Button.astro";
|
import Button from "../../components/control/Button.astro";
|
||||||
import {getConfig} from "../../utils/config-utils";
|
import {getConfig} from "../../utils/config-utils";
|
||||||
import {i18n} from "../../i18n/translation";
|
import {i18n} from "../../i18n/translation";
|
||||||
import I18nKey from "../../i18n/i18nKey";
|
import I18nKey from "../../i18n/i18nKey";
|
||||||
|
import {getPostUrlBySlug} from "../../utils/url-utils";
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const blogEntries = await getCollection('posts');
|
const blogEntries = await getCollection('posts');
|
||||||
|
|
|
@ -20,12 +20,6 @@ export async function getSortedPosts() {
|
||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostUrlBySlug(slug: string): string | null {
|
|
||||||
if (!slug)
|
|
||||||
return null;
|
|
||||||
return `/posts/${slug}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTagList(): Promise<{ name: string; count: number }[]> {
|
export async function getTagList(): Promise<{ name: string; count: number }[]> {
|
||||||
const allBlogPosts = await getCollection("posts");
|
const allBlogPosts = await getCollection("posts");
|
||||||
|
|
||||||
|
@ -52,3 +46,27 @@ export async function getTagList(): Promise<{ name: string; count: number }[]> {
|
||||||
|
|
||||||
return keys.map((key) => ({name: key, count: countMap[key]}));
|
return keys.map((key) => ({name: key, count: countMap[key]}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Category = {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
children: CategoryMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryMap = { [key: string]: Category };
|
||||||
|
|
||||||
|
export async function getCategoryMap(): Promise<CategoryMap> {
|
||||||
|
const allBlogPosts = await getCollection("posts");
|
||||||
|
let root: CategoryMap = {};
|
||||||
|
allBlogPosts.map((post) => {
|
||||||
|
let current = root;
|
||||||
|
if (!post.data.categories)
|
||||||
|
return;
|
||||||
|
for (const c of post.data.categories) {
|
||||||
|
if (!current[c]) current[c] = {name: c, count: 0, children: {}};
|
||||||
|
current[c].count++;
|
||||||
|
current = current[c].children;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return root;
|
||||||
|
}
|
|
@ -3,3 +3,16 @@ export function pathsEqual(path1: string, path2: string) {
|
||||||
const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase();
|
const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase();
|
||||||
return normalizedPath1 === normalizedPath2;
|
return normalizedPath1 === normalizedPath2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPostUrlBySlug(slug: string): string | null {
|
||||||
|
if (!slug)
|
||||||
|
return null;
|
||||||
|
return `/posts/${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategoryUrl(category: string): string | null {
|
||||||
|
if (!category)
|
||||||
|
return null;
|
||||||
|
return `/archive/category/${category}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue