Commit 8c92120
Changed files (9)
src
components
aside
siteinfo
i18n
types
src/components/aside/siteinfo/Stats.astro
@@ -0,0 +1,83 @@
+---
+import { asideConfig, siteConfig } from '@/config';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
+import { getOrCreateRenderResult, getPostsCount } from '@utils/content-utils';
+import { Icon } from 'astro-icon/components';
+import { getCollection } from 'astro:content';
+---
+
+<div class="stats stats-vertical w-full">
+ {
+ asideConfig.siteInfo.stats.map(async (entry) => {
+ switch (entry) {
+ case 'post-count': {
+ const postCount = await getPostsCount();
+ return (
+ <div class="stat">
+ <div class="stat-title flex flex-row items-center gap-1">
+ <Icon name="material-symbols:folder-open-rounded" />
+ <span>{i18n(I18nKey.totalPosts)}</span>
+ </div>
+ <div class="stat-value text-base">{`${postCount} ${postCount > 1 ? i18n(I18nKey.postsCount) : i18n(I18nKey.postCount)}`}</div>
+ </div>
+ );
+ }
+ case 'last-updated':
+ return (
+ <div class="stat">
+ <div class="stat-title flex flex-row items-center gap-1">
+ <Icon name="material-symbols:refresh-rounded" />
+ <span>{i18n(I18nKey.lastUpdated)}</span>
+ </div>
+ <div class="stat-value text-base">
+ <time datetime={new Date().toISOString()}>
+ {new Date().toLocaleDateString(siteConfig.lang.replace('_', '-'))}
+ </time>
+ </div>
+ </div>
+ );
+ case 'site-words-count':
+ return (
+ <div class="stat">
+ <div class="stat-title flex flex-row items-center gap-1">
+ <Icon name="material-symbols:docs-rounded" />
+ <span>{i18n(I18nKey.totalWords)}</span>
+ </div>
+ <div class="stat-value text-base">
+ {(async () => {
+ const entries = await getCollection('posts');
+ const words = await Promise.all(
+ entries.map(async (entry) => {
+ const { remarkPluginFrontmatter } = await getOrCreateRenderResult(entry);
+ return remarkPluginFrontmatter.words as number;
+ })
+ );
+ const total = words.reduce((acc, cur) => acc + cur, 0);
+ return `${total} ${total > 1 ? i18n(I18nKey.wordsCount) : i18n(I18nKey.wordCount)}`;
+ })()}
+ </div>
+ </div>
+ );
+ case 'site-run-days':
+ return (
+ <div class="stat">
+ <div class="stat-title flex flex-row items-center gap-1">
+ <Icon name="material-symbols:calendar-clock-rounded" />
+ <span>{i18n(I18nKey.runTime)}</span>
+ </div>
+ <div class="stat-value text-base">
+ <time
+ datetime={siteConfig.createAt.toISOString()}
+ data-force-relative="true"
+ data-no-ago="true"
+ >
+ {siteConfig.createAt.toLocaleDateString(siteConfig.lang.replace('_', '-'))}
+ </time>
+ </div>
+ </div>
+ );
+ }
+ })
+ }
+</div>
src/components/aside/siteinfo/Tags.astro
@@ -0,0 +1,49 @@
+---
+import Button from '@components/widgets/Button.astro';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
+import { getTags, getTagUrl } from '@utils/content-utils';
+const tagsMap = await getTags();
+---
+
+<div id="aside-siteinfo-tags">
+ <div
+ class="relative flex max-h-40 flex-wrap gap-0 overflow-hidden after:pointer-events-none after:absolute after:bottom-0 after:hidden after:h-24 after:w-full after:content-end after:bg-gradient-to-b after:from-black/0 after:to-black/15"
+ >
+ {
+ Array.from(tagsMap.entries()).map(([tag, count]) => (
+ <Button href={getTagUrl(tag)} title={`${tag}`} class="btn-ghost btn-primary btn-sm">
+ {tag}
+ <sup>{count}</sup>
+ </Button>
+ ))
+ }
+ </div>
+ <div class="btn bg-base-200/50 btn-wide hidden rounded-sm hover:brightness-125">
+ {i18n(I18nKey.more)}
+ </div>
+</div>
+
+<script>
+ function initTags() {
+ const tags = document.getElementById('aside-siteinfo-tags');
+ const tagsList = tags?.children[0] as HTMLDivElement;
+ const moreBtn = tags?.children[1] as HTMLDivElement;
+
+ const clientHeight = tagsList.clientHeight;
+ const scrollHeight = tagsList.scrollHeight;
+
+ if (scrollHeight > clientHeight) {
+ tagsList.classList.remove('after:hidden');
+ moreBtn.classList.remove('hidden');
+ }
+
+ moreBtn.addEventListener('click', () => {
+ tagsList.classList.remove('max-h-40');
+ tagsList.classList.add('after:hidden');
+ moreBtn.classList.add('hidden');
+ });
+ }
+
+ document.addEventListener('astro:page-load', initTags);
+</script>
src/components/aside/SiteInfoCard.astro
@@ -0,0 +1,31 @@
+---
+import { asideConfig } from '@/config';
+import Stats from './siteinfo/Stats.astro';
+import Tags from './siteinfo/Tags.astro';
+---
+
+<div class="card border-base-300 border-2" transition:name="site-info-card">
+ <div class="card-body px-4 py-2">
+ <ul>
+ {
+ asideConfig.siteInfo.contents.map((part) => {
+ const defaultClass = 'border-base-content/10 not-first:border-t';
+ switch (part) {
+ case 'stats':
+ return (
+ <li class:list={['', defaultClass]}>
+ <Stats />
+ </li>
+ );
+ case 'tags':
+ return (
+ <li class:list={['py-2', defaultClass]}>
+ <Tags />
+ </li>
+ );
+ }
+ })
+ }
+ </ul>
+ </div>
+</div>
src/i18n/langs/en.ts
@@ -19,6 +19,11 @@ export const en: Translation = {
[Key.uncategorized]: 'Uncategorized',
[Key.untagged]: 'No Tags',
+ [Key.totalPosts]: 'Total Posts',
+ [Key.totalWords]: 'Total Words',
+ [Key.lastUpdated]: 'Last Updated',
+ [Key.runTime]: 'Run Time',
+
[Key.wordCount]: 'word',
[Key.wordsCount]: 'words',
[Key.minuteCount]: 'minute',
src/i18n/langs/zh_CN.ts
@@ -19,6 +19,11 @@ export const zh_CN: Translation = {
[Key.uncategorized]: '未分类',
[Key.untagged]: '无标签',
+ [Key.totalPosts]: '文章总数',
+ [Key.totalWords]: '字数总计',
+ [Key.lastUpdated]: '最后更新',
+ [Key.runTime]: '运行时间',
+
[Key.wordCount]: '字',
[Key.wordsCount]: '字',
[Key.minuteCount]: '分钟',
src/i18n/langs/zh_TW.ts
@@ -19,6 +19,11 @@ export const zh_TW: Translation = {
[Key.uncategorized]: '未分類',
[Key.untagged]: '無標籤',
+ [Key.totalPosts]: '文章總數',
+ [Key.totalWords]: '字數總計',
+ [Key.lastUpdated]: '最後更新',
+ [Key.runTime]: '運行時間',
+
[Key.wordCount]: '字',
[Key.wordsCount]: '字',
[Key.minuteCount]: '分鐘',
src/i18n/I18nKey.ts
@@ -16,6 +16,11 @@ enum I18nKey {
uncategorized = 'uncategorized',
untagged = 'untagged',
+ totalPosts = 'totalPosts',
+ totalWords = 'totalWords',
+ lastUpdated = 'lastUpdated',
+ runTime = 'runTime',
+
wordCount = 'wordCount',
wordsCount = 'wordsCount',
minuteCount = 'minuteCount',
src/types/config.ts
@@ -1,5 +1,6 @@
import type I18nKey from '@i18n/I18nKey';
+// ============================================================================
export type Favicon = {
/**
* The URL of the favicon.
@@ -85,6 +86,8 @@ export type ButtonSubConfig<T extends string> = T extends 'text'
)
: never;
+// ============================================================================
+
export type SiteConfig = {
/**
* The title of the site.
@@ -110,6 +113,12 @@ export type SiteConfig = {
* 站点的 favicon。
*/
favicon: Favicon[];
+ /**
+ * The time when the site was created.
+ *
+ * 站点建立时间
+ */
+ createAt: Date;
/**
* The number of posts displayed per page.
*
@@ -207,6 +216,23 @@ export type ToolBarConfig = {
items: ButtonSubConfig<'icon'>[];
};
+export type AsideConfig = {
+ siteInfo: {
+ /**
+ * The contents displayed in the site info.
+ *
+ * 在站点信息中显示的内容。
+ */
+ contents: ('stats' | 'tags')[];
+ /**
+ * The stats displayed in the site info.
+ *
+ * 在站点信息中显示的统计数据。
+ */
+ stats: ('post-count' | 'last-updated' | 'site-words-count' | 'site-run-days')[];
+ };
+};
+
export type LicenseConfig = {
/**
* Whether to enable the license.
src/config.ts
@@ -1,5 +1,6 @@
import type {
ArticleConfig,
+ AsideConfig,
CommentConfig,
FooterConfig,
LicenseConfig,
@@ -20,6 +21,7 @@ export const siteConfig: SiteConfig = {
// Leave this array empty to use the default favicon.
// 留空数组以使用默认的 favicon。
],
+ createAt: new Date('2025-01-01'),
postsPerPage: 10,
};
@@ -79,6 +81,13 @@ export const toolBarConfig: ToolBarConfig = {
items: [],
};
+export const asideConfig: AsideConfig = {
+ siteInfo: {
+ contents: ['stats', 'tags'],
+ stats: ['post-count', 'last-updated', 'site-words-count', 'site-run-days'],
+ },
+};
+
export const licenseConfig: LicenseConfig = {
enable: true,
name: 'CC BY-NC-SA 4.0',