Commit f0536d2
Changed files (39)
.vscode
public
src
assets
img
components
i18n
layouts
pages
archives
posts
scripts
types
utils
.vscode/extensions.json
@@ -1,6 +1,3 @@
{
- "recommendations": [
- "astro-build.astro-vscode",
- "bradlc.vscode-tailwindcss"
- ],
-}
\ No newline at end of file
+ "recommendations": ["astro-build.astro-vscode", "bradlc.vscode-tailwindcss"]
+}
.vscode/settings.json
@@ -6,12 +6,7 @@
"css.validate": false,
"scss.validate": false,
"less.validate": false,
- "stylelint.validate": [
- "css",
- "postcss",
- "scss",
- "astro",
- ],
+ "stylelint.validate": ["css", "postcss", "scss", "astro"],
"eslint.validate": [
"javascript",
"javascriptreact",
@@ -24,4 +19,4 @@
"source.fixAll.eslint": "always",
"source.fixAll.stylelint": "always"
}
-}
\ No newline at end of file
+}
public/favicon.svg
@@ -1,9 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
- <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
- <style>
- path { fill: #000; }
- @media (prefers-color-scheme: dark) {
- path { fill: #FFF; }
- }
- </style>
-</svg>
src/assets/img/avatar.jpg
Binary file
src/components/widgets/Button.astro
@@ -1,6 +1,6 @@
---
import { AstroParameterConflictError } from '@/types/Errors';
-import type { HTMLAttributes } from 'astro/types';
+import type { HTMLAttributes, HTMLTag } from 'astro/types';
interface Props extends HTMLAttributes<'button'> {
href?: string;
@@ -8,11 +8,15 @@ interface Props extends HTMLAttributes<'button'> {
const { href, onclick, class: className, ...rest } = Astro.props;
if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
+
+const Tag = (href ? 'a' : Fragment) as HTMLTag;
---
-<button {...{ href, onclick, ...rest }} class:list={[className]}>
- <slot />
-</button>
+<Tag {...{ href }}>
+ <button {...{ onclick, ...rest }} class:list={[className]}>
+ <slot />
+ </button>
+</Tag>
<style lang="scss">
button {
@@ -21,7 +25,6 @@ if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
padding: 0.5rem;
margin: 0.5rem;
text-align: center;
- word-break: break-word;
transition-duration: 300ms;
border-radius: 9999px;
width: fit-content;
@@ -37,15 +40,3 @@ if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
}
}
</style>
-
-<script>
- import { navigate } from 'astro:transitions/client';
- document.addEventListener('astro:page-load', () => {
- document.querySelectorAll('button[href]').forEach((btn) => {
- btn.addEventListener('click', () => {
- const href = btn.getAttribute('href') || '/404.html';
- navigate(href);
- });
- });
- });
-</script>
src/components/widgets/DarkModeButton.astro
@@ -2,7 +2,7 @@
import type { HTMLAttributes } from 'astro/types';
import Button from './Button.astro';
import { Icon } from 'astro-icon/components';
-import I18nKey from '@i18n/i18nKey';
+import I18nKey from '@i18n/I18nKey';
import { i18n } from '@i18n/translation';
interface Props extends Omit<HTMLAttributes<'button'>, 'onclick'> {
src/components/widgets/Pagination.astro
@@ -0,0 +1,159 @@
+---
+import { Icon } from 'astro-icon/components';
+import Button from './Button.astro';
+
+interface Props {
+ total: number;
+ current: number;
+ baseUrl: string;
+ specialPages?: { page: number; url: string }[];
+}
+
+const { total, current, baseUrl, specialPages } = Astro.props;
+
+let pages: { page: number; url: string }[] = [];
+
+function getPageUrl(page: number) {
+ return specialPages?.find((p) => p.page === page)?.url || `${baseUrl}/${page}`;
+}
+
+function pushPage(page: number) {
+ pages.push({ page, url: getPageUrl(page) });
+}
+
+if (total <= 5) Array.from({ length: total }).map((_, i) => pushPage(i + 1));
+else {
+ if (current <= 3) {
+ Array.from({ length: 3 }).map((_, i) => pushPage(i + 1));
+ pages.push({ page: -1, url: '' });
+ pushPage(total);
+ } else if (current >= total - 2) {
+ pushPage(1);
+ pages.push({ page: -1, url: '' });
+ Array.from({ length: 3 }).map((_, i) => pushPage(total - 2 + i));
+ } else {
+ pushPage(1);
+ pages.push({ page: -1, url: '' });
+ pushPage(current - 1);
+ pushPage(current);
+ pushPage(current + 1);
+ pages.push({ page: -1, url: '' });
+ pushPage(total);
+ }
+}
+---
+
+<div id="pagination" class="flex justify-center items-center gap-2">
+ {
+ pages.map((p) => {
+ return (
+ <Button
+ class:list={[
+ 'theme-bg theme-border border-2 !rounded-xl',
+ current === p.page && 'theme-card-bg-hl',
+ ]}
+ href={p.url}
+ >
+ <span class="mx-1">{`${p.page === -1 ? '...' : p.page}`}</span>
+ </Button>
+ );
+ })
+ }
+ {
+ total > 1 && (
+ <div
+ id="page-jumper"
+ class="flex flex-row items-center theme-card-bg theme-border mx-2 rounded-xl border-2"
+ data-base-url={baseUrl}
+ data-special-pages={specialPages?.map((p) => `${p.page}:${p.url}`).join(',')}
+ >
+ <input
+ id="page-jumper-input"
+ type="number"
+ min="1"
+ max={total}
+ class="duration-300 theme-bg border-none !active:border-none pl-4"
+ />
+ <Button
+ id="page-jumper-button"
+ class="theme-card-bg !rounded-xl !m-0 relative right-0 duration-300"
+ >
+ <Icon name="material-symbols:keyboard-double-arrow-right-rounded" class="my-1" />
+ </Button>
+ </div>
+ )
+ }
+</div>
+
+<style lang="scss">
+ /* hide arrows from number input */
+ input::-webkit-outer-spin-button,
+ input::-webkit-inner-spin-button {
+ appearance: none;
+ }
+
+ input[type='number'] {
+ appearance: textfield;
+ }
+
+ input:focus {
+ outline: none;
+ }
+</style>
+
+<script>
+ import { navigate } from 'astro:transitions/client';
+
+ document.addEventListener('astro:page-load', () => {
+ const pageJumper = document.getElementById('page-jumper');
+ const pageJumperInput = document.getElementById(
+ 'page-jumper-input'
+ ) as HTMLInputElement | null;
+ const pageJumperButton = document.getElementById('page-jumper-button');
+
+ function pageJumperMouseEnterCallback() {
+ if (pageJumperInput) pageJumperInput.style.width = '4rem';
+ pageJumperInput?.classList.remove('inert');
+ pageJumperInput?.classList.add('pl-4');
+ pageJumperInput?.focus();
+ }
+
+ function pageJumperMouseLeaveCallback() {
+ if (pageJumperInput) pageJumperInput.style.width = '0px';
+ pageJumperInput?.classList.add('inert');
+ pageJumperInput?.classList.remove('pl-4');
+ pageJumperInput?.blur();
+ }
+
+ function getPageUrl(page: number) {
+ const baseUrl = pageJumper?.getAttribute('data-base-url');
+ const specialPagesStr = pageJumper?.getAttribute('data-special-pages');
+ const specialPagesArray = specialPagesStr?.split(',');
+ const specialPages = specialPagesArray?.map((p) => {
+ const [page, url] = p.split(':', 2);
+ return { page: Number(page), url: url };
+ });
+ console.log(specialPages);
+ return specialPages?.find((p) => p.page === page)?.url || `${baseUrl}/${page}`;
+ }
+
+ function pageJumperExecHandler() {
+ const page = pageJumperInput?.value;
+ if (page) {
+ const pageUrl = getPageUrl(Number(page));
+ navigate(pageUrl);
+ }
+ }
+
+ pageJumper?.addEventListener('mouseenter', pageJumperMouseEnterCallback);
+ pageJumper?.addEventListener('mouseleave', pageJumperMouseLeaveCallback);
+ pageJumperMouseLeaveCallback();
+
+ pageJumperInput?.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') {
+ pageJumperExecHandler();
+ }
+ });
+ pageJumperButton?.addEventListener('click', pageJumperExecHandler);
+ });
+</script>
src/components/widgets/PostCard.astro
@@ -33,14 +33,14 @@ const metas: ({ icon: string; text: string; link?: string } | undefined)[] = [
? {
icon: 'material-symbols:category-outline-rounded',
text: category,
- link: `/archives/categories/${category}/1/`,
+ link: `/archives/categories/${category.replaceAll(/[\\/]/g, '-')}/1/`,
}
: undefined,
...tags.map((tag) => {
return {
icon: 'material-symbols:tag-rounded',
text: tag,
- link: `/archives/tags/${tag}/1/`,
+ link: `/archives/tags/${tag.replaceAll(/[\\/]/g, '-')}/1/`,
};
}),
];
src/components/widgets/PostCardCover.astro
@@ -12,7 +12,9 @@ const { url, title, cover } = Astro.props;
---
<a href={url} title={title} class="group">
- <div><Icon name="material-symbols:chevron-right-rounded" /></div>
+ <div>
+ <Icon name="material-symbols:chevron-right-rounded" />
+ </div>
<Image src={cover} alt={title} inferSize={true} />
</a>
src/components/widgets/ProfileCard.astro
@@ -0,0 +1,30 @@
+---
+import { profileConfig } from '@/config';
+import { Image } from 'astro:assets';
+import avatarImg from '/src/assets/img/avatar.jpg';
+
+const avaterAttr = profileConfig.avatar
+ ? {
+ src: profileConfig.avatar,
+ alt: profileConfig.name,
+ inferSize: true,
+ width: 800,
+ height: 800,
+ }
+ : { src: avatarImg, alt: profileConfig.name };
+---
+
+<div
+ id="profile-card"
+ class="theme-card-bg theme-border rounded-xl border-2 flex flex-col items-center text-center"
+>
+ <div class="m-3 w-fit min-w-20">
+ <a href="/about/">
+ <Image class="rounded-full border-4 w-20 h-20 theme-border" {...avaterAttr} />
+ </a>
+ </div>
+ <div class="mx-3 mb-5 flex flex-col w-full">
+ <div class="text-lg mb-3">{profileConfig.name}</div>
+ <div>{profileConfig.bio}</div>
+ </div>
+</div>
src/components/widgets/ReadMoreButton.astro
@@ -1,13 +1,15 @@
---
import type { ComponentProps } from 'astro/types';
import { Icon } from 'astro-icon/components';
+import { i18n } from '@i18n/translation';
+import I18nKey from '@i18n/I18nKey';
type Props = Omit<ComponentProps<typeof Icon>, 'name'>;
const { href, title, ...rest } = Astro.props;
---
-<a href={href} title={title}>
+<a href={href} title={title || i18n(I18nKey.more)}>
<Icon name="material-symbols:chevron-right-rounded" {...rest} />
</a>
src/components/CategoryBar.astro
@@ -1,5 +1,40 @@
---
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
+interface Props {
+ categories: string[];
+ currentCategory?: string;
+}
+
+const { categories, currentCategory } = Astro.props;
---
-<div id="category-bar" class="flex w-full rounded-md px-2 py-3"></div>
+<div
+ id="category-bar"
+ class="theme-card-bg theme-border flex w-full rounded-xl px-2 py-3 border-2 mb-4"
+>
+ <a href={`/`} class:list={[currentCategory ? '' : 'theme-card-bg-hl']}>
+ {i18n(I18nKey.recentPosts)}
+ </a>
+ {
+ categories.map((category) => (
+ <a
+ href={`/archives/categories/${category}/1/`}
+ class:list={[currentCategory === category ? 'theme-card-bg-hl' : '']}
+ >
+ {category}
+ </a>
+ ))
+ }
+</div>
+
+<style lang="scss">
+ a {
+ @apply mx-2 rounded-md px-2 py-1;
+
+ &:hover {
+ @apply bg-[var(--theme-color-light-lighten)] dark:bg-[var(--theme-color-dark-lighten)];
+ }
+ }
+</style>
src/components/PostPage.astro
@@ -2,6 +2,7 @@
import type { BlogPostData } from '@/types/data';
import { siteConfig } from '@/config';
import PostCard from './widgets/PostCard.astro';
+import Pagination from './widgets/Pagination.astro';
interface Props {
posts: {
@@ -10,12 +11,15 @@ interface Props {
}[];
currentPage: number;
postsPerPage?: number;
+ baseUrl: string;
+ specialPage?: { page: number; url: string }[];
}
-let { posts, currentPage, postsPerPage } = Astro.props;
+let { posts, currentPage, postsPerPage, baseUrl, specialPage } = Astro.props;
postsPerPage = postsPerPage || siteConfig.postsPerPage;
+const totalPages = Math.ceil(posts.length / postsPerPage);
posts = posts.slice((currentPage - 1) * postsPerPage, currentPage * postsPerPage);
---
@@ -36,4 +40,10 @@ posts = posts.slice((currentPage - 1) * postsPerPage, currentPage * postsPerPage
);
})
}
+ <Pagination
+ total={totalPages}
+ current={currentPage}
+ baseUrl={baseUrl}
+ specialPages={specialPage}
+ />
</div>
src/components/Sidebar.astro
@@ -15,7 +15,7 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
</div>
<div
id="sidebar-menu"
- class="fixed h-full z-50 -right-1/3 w-1/3 duration-500 ease-in-out border-l-2 theme-border theme-card-bg"
+ class="fixed h-full z-50 -right-1/2 w-1/2 duration-500 ease-in-out border-l-2 theme-border theme-card-bg"
>
<div id="sidebar-site-data"></div>
<DarkModeButton
src/components/SideToolBar.astro
@@ -4,7 +4,7 @@ import Button from './widgets/Button.astro';
import DarkModeButton from './widgets/DarkModeButton.astro';
---
-<div id="side-toolbar" transition:persist class="fixed bottom-10 right-0">
+<div id="side-toolbar" transition:persist class="fixed bottom-10 right-0 z-30">
<div id="stb-hide" class="translate-x-full duration-500 ease-in-out" inert>
<DarkModeButton id="stb-dark-mode" class="" />
</div>
src/components/TimeArchives.astro
@@ -0,0 +1,47 @@
+---
+import { getTimeArchives } from '@utils/content-utils';
+import PostCard from '@components/widgets/PostCard.astro';
+
+type AllTimeArchives = Awaited<ReturnType<typeof getTimeArchives>>;
+type YearArchives = AllTimeArchives[number];
+type MonthArchives = YearArchives['months'][number];
+
+interface Props {
+ group: AllTimeArchives | YearArchives | MonthArchives;
+}
+
+const { group } = Astro.props;
+---
+
+{
+ Array.isArray(group) ? (
+ group.map((year) => <Astro.self group={year} />)
+ ) : 'year' in group ? (
+ <Fragment>
+ <div class="text-2xl font-bold ml-2">
+ <a href={`/archives/${group.year}/`}>{group.year}</a>
+ </div>
+ {group.months.map((month) => (
+ <Astro.self group={month} />
+ ))}
+ </Fragment>
+ ) : (
+ <Fragment>
+ <div class="text-xl font-bold ml-3">{group.month}</div>
+ {group.posts.map((post) => {
+ const data = post.data;
+ return (
+ <PostCard
+ title={data.title}
+ url={'/posts/' + data.abbrlink + '/'}
+ published={data.published}
+ tags={data.tags}
+ category={data.category}
+ cover={data.cover}
+ description={data.description}
+ />
+ );
+ })}
+ </Fragment>
+ )
+}
src/i18n/langs/en.ts
@@ -1,4 +1,4 @@
-import Key from '../i18nKey';
+import Key from '../I18nKey';
import type { Translation } from '../translation';
export const en: Translation = {
src/i18n/langs/zh_CN.ts
@@ -1,4 +1,4 @@
-import Key from '../i18nKey';
+import Key from '../I18nKey';
import type { Translation } from '../translation';
export const zh_CN: Translation = {
src/i18n/langs/zh_TW.ts
@@ -1,4 +1,4 @@
-import Key from '../i18nKey';
+import Key from '../I18nKey';
import type { Translation } from '../translation';
export const zh_TW: Translation = {
src/i18n/translation.ts
@@ -1,14 +1,14 @@
import { siteConfig } from '@/config';
-import type I18nKey from './i18nKey';
-
-import { en } from './langs/en';
-import { zh_CN } from './langs/zh_CN';
-import { zh_TW } from './langs/zh_TW';
+import I18nKey from './I18nKey';
export type Translation = {
[K in I18nKey]: string;
};
+import { en } from './langs/en';
+import { zh_CN } from './langs/zh_CN';
+import { zh_TW } from './langs/zh_TW';
+
const defaultTranslation = en;
const map: { [key: string]: Translation } = {
@@ -24,7 +24,8 @@ export function getTranslation(lang: string): Translation {
return map[lang.toLowerCase()] || defaultTranslation;
}
-export function i18n(key: I18nKey): string {
+export function i18n(key: I18nKey | string): string {
const lang = siteConfig.lang || 'en';
- return getTranslation(lang)[key];
+ const translate = getTranslation(lang);
+ return key in I18nKey ? translate[key as I18nKey] : key;
}
src/layouts/GridLayout.astro
@@ -0,0 +1,19 @@
+---
+import MainLayout from '@layouts/MainLayout.astro';
+
+interface Props {
+ title?: string;
+ description?: string;
+ lang?: string;
+}
+const { title, description, lang } = Astro.props;
+---
+
+<MainLayout title={title} description={description} lang={lang}>
+ <div id="main-content" class="my-4 w-full mx-4">
+ <slot />
+ </div>
+ <div id="aside-content" class="max-xl:hidden my-4 mx-2 w-96">
+ <slot name="aside" slot="aside" />
+ </div>
+</MainLayout>
src/layouts/MainLayout.astro
@@ -16,11 +16,12 @@ const { title, description, lang } = Astro.props;
<slot slot="head" name="head" />
<Sidebar />
<SideToolBar />
- <div id="body-wrap">
- <Navbar />
+ <Navbar />
+ <div id="body-wrap" class="items-center w-full">
<!-- Main content -->
- <div id="content-wrapper">
+ <div id="content-wrapper" class="flex mx-auto gap-4 max-w-screen-xl">
<slot />
</div>
</div>
+ <div id="footer"></div>
</GlobalLayout>
src/pages/archives/categories/[category]/[page].astro
@@ -1,10 +1,12 @@
---
-import { getSortedPosts } from '@utils/content-utils';
-import MainLayout from '@layouts/MainLayout.astro';
+import { getCategories, getSortedPosts } from '@utils/content-utils';
import PostPage from '@components/PostPage.astro';
import { siteConfig } from '@/config';
import { i18n } from '@i18n/translation';
-import I18nKey from '@i18n/i18nKey';
+import I18nKey from '@i18n/I18nKey';
+import GridLayout from '@layouts/GridLayout.astro';
+import CategoryBar from '@components/CategoryBar.astro';
+import ProfileCard from '@components/widgets/ProfileCard.astro';
export async function getStaticPaths() {
const posts = await getSortedPosts();
@@ -15,10 +17,10 @@ export async function getStaticPaths() {
.map((category) => {
const categoryPosts = posts.filter((post) => post.data.category === category);
const pageNum = Math.ceil(categoryPosts.length / siteConfig.postsPerPage);
+ category = category.replaceAll(/[\\/]/g, '-');
return Array.from({ length: pageNum }, (_, i) => ({
- params: { category, page: (i + 1).toString() },
+ params: { category: category, page: (i + 1).toString() },
props: {
- category,
posts: categoryPosts.slice(
i * siteConfig.postsPerPage,
(i + 1) * siteConfig.postsPerPage
@@ -30,12 +32,19 @@ export async function getStaticPaths() {
.flat();
}
-const { category, posts, currentPage } = Astro.props;
+const { posts, currentPage } = Astro.props;
+const { category } = Astro.params;
+const categories = await getCategories();
---
-<MainLayout>
- <div id="main-content">
- <PostPage posts={posts} currentPage={currentPage} />
- </div>
- <div id="aside-content"></div>
-</MainLayout>
+<GridLayout>
+ <CategoryBar categories={categories} currentCategory={category} />
+ <PostPage
+ posts={posts}
+ currentPage={currentPage}
+ baseUrl={`/archives/categories/${category}`}
+ />
+ <Fragment slot="aside">
+ <ProfileCard />
+ </Fragment>
+</GridLayout>
src/pages/archives/[...time].astro
@@ -0,0 +1,37 @@
+---
+import TimeArchives from '@components/TimeArchives.astro';
+import ProfileCard from '@components/widgets/ProfileCard.astro';
+import GridLayout from '@layouts/GridLayout.astro';
+import { getTimeArchives } from '@utils/content-utils';
+
+type AllTimeArchives = Awaited<ReturnType<typeof getTimeArchives>>;
+type YearArchives = AllTimeArchives[number];
+type MonthArchives = YearArchives['months'][number];
+
+export async function getStaticPaths() {
+ const timeReducedPosts = await getTimeArchives();
+ let paths: {
+ params: { time: string | undefined };
+ props: { group: AllTimeArchives | YearArchives | MonthArchives };
+ }[] = [{ params: { time: undefined }, props: { group: timeReducedPosts } }];
+ paths = [
+ ...paths,
+ ...timeReducedPosts.map((group) => ({
+ params: { time: `${group.year}` },
+ props: { group: group },
+ })),
+ ];
+ return paths;
+}
+
+const { group } = Astro.props;
+---
+
+<GridLayout>
+ <div class="flex flex-col gap-4">
+ <TimeArchives group={group} />
+ </div>
+ <Fragment slot="aside">
+ <ProfileCard />
+ </Fragment>
+</GridLayout>
src/pages/posts/[article].astro
@@ -1,6 +1,8 @@
---
import { getCollection, render } from 'astro:content';
-import MainLayout from '@layouts/MainLayout.astro';
+import GridLayout from '@layouts/GridLayout.astro';
+import ProfileCard from '@components/widgets/ProfileCard.astro';
+import '@/styles/article.scss';
export async function getStaticPaths() {
const articles = await getCollection('posts');
@@ -14,9 +16,13 @@ const { article } = Astro.props;
const { Content } = await render(article);
---
-<MainLayout>
- <div id="main-content">
- <Content />
+<GridLayout>
+ <div class="theme-card-bg theme-border border-2 px-6 py-4 rounded-xl">
+ <article>
+ <Content />
+ </article>
</div>
- <div id="aside-content"></div>
-</MainLayout>
+ <Fragment slot="aside">
+ <ProfileCard />
+ </Fragment>
+</GridLayout>
src/pages/[...page].astro
@@ -1,9 +1,10 @@
---
-import { getSortedPosts } from '@utils/content-utils';
+import { getCategories, getSortedPosts } from '@utils/content-utils';
import { siteConfig } from '@/config';
-import categoryBar from '@components/categoryBar.astro';
+import CategoryBar from '@components/CategoryBar.astro';
import PostPage from '@components/PostPage.astro';
-import MainLayout from '@layouts/MainLayout.astro';
+import GridLayout from '@layouts/GridLayout.astro';
+import ProfileCard from '@components/widgets/ProfileCard.astro';
export async function getStaticPaths() {
const articles = await getSortedPosts();
@@ -17,16 +18,18 @@ export async function getStaticPaths() {
const { page } = Astro.props;
const articles = await getSortedPosts();
-const posts = articles.slice(
- (page - 1) * siteConfig.postsPerPage,
- page * siteConfig.postsPerPage
-);
+const categories = await getCategories();
---
-<MainLayout>
- <div id="main-content">
- <categoryBar></categoryBar>
- <PostPage posts={posts} currentPage={page} />
- </div>
- <div id="aside-content"></div>
-</MainLayout>
+<GridLayout>
+ <CategoryBar categories={categories} />
+ <PostPage
+ posts={articles}
+ currentPage={page}
+ baseUrl={`/page`}
+ specialPage={[{ page: 1, url: '/' }]}
+ />
+ <Fragment slot="aside">
+ <ProfileCard />
+ </Fragment>
+</GridLayout>
src/pages/about.astro
@@ -1,7 +1,7 @@
---
import MainLayout from '@layouts/MainLayout.astro';
-import I18nKey from '@i18n/i18nKey';
+import I18nKey from '@i18n/I18nKey';
import { i18n } from '@i18n/translation';
---
src/scripts/utils.ts
@@ -0,0 +1,29 @@
+export function scrollToTop(duration: number = 500): void {
+ const start = window.scrollY;
+ const startTime = performance.now();
+ function scrollStep(timestamp: number) {
+ const currentTime = timestamp - startTime;
+ const progress = Math.min(currentTime / duration, 1);
+ window.scrollTo(0, start * (1 - progress));
+ if (currentTime < duration) {
+ window.requestAnimationFrame(scrollStep);
+ }
+ }
+ window.requestAnimationFrame(scrollStep);
+}
+
+export function getReadingProgress(): number {
+ const docEl = document.documentElement;
+ const bodyEl = document.body;
+ const totalHeight =
+ Math.max(
+ bodyEl.clientHeight,
+ bodyEl.scrollHeight,
+ bodyEl.offsetHeight,
+ docEl.clientHeight,
+ docEl.scrollHeight,
+ docEl.offsetHeight
+ ) - docEl.clientHeight;
+ const scrollY = window.scrollY;
+ return Math.round((scrollY / totalHeight) * 100);
+}
src/styles/article.scss
@@ -0,0 +1,43 @@
+article {
+ h1 {
+ @apply text-2xl font-bold;
+ @apply mt-4 mb-2;
+ }
+
+ h2 {
+ @apply text-xl font-bold;
+ @apply mt-4 mb-2;
+ }
+
+ h3 {
+ @apply text-lg font-bold;
+ @apply mt-4 mb-2;
+ }
+
+ h4 {
+ @apply text-base font-bold;
+ @apply mt-4 mb-2;
+ }
+
+ h5 {
+ @apply text-base font-bold;
+ @apply mt-4 mb-2;
+ }
+
+ p {
+ @apply my-2;
+ }
+
+ img {
+ @apply mx-auto my-4 rounded-md;
+ }
+
+ hr {
+ @apply border-dashed border-2;
+ @apply mx-2 my-4;
+ }
+
+ a {
+ @apply text-[var(--theme-color-light)] dark:text-[var(--theme-color-dark)];
+ }
+}
src/styles/globals.scss
@@ -3,6 +3,10 @@
@tailwind components;
@tailwind utilities;
+:root {
+ font-size: 17px;
+}
+
@layer components {
.theme-bg {
@apply bg-[var(--theme-bg-color-light)] dark:bg-[var(--theme-bg-color-dark)];
@@ -12,6 +16,10 @@
@apply bg-[var(--theme-card-bg-color-light)] dark:bg-[var(--theme-card-bg-color-dark)];
}
+ .theme-card-bg-hl {
+ @apply bg-[var(--theme-color-light)] dark:bg-[var(--theme-color-dark)];
+ }
+
.theme-text {
@apply text-[var(--theme-text-color-light)] dark:text-[var(--theme-text-color-dark)];
}
src/styles/variables.scss
@@ -1,7 +1,7 @@
@use 'sass:color';
-$theme-color-light: #d75b3d;
-$theme-color-dark: #397d9f;
+$theme-color-light: #f69279;
+$theme-color-dark: #2e6c8b;
$theme-bg-color-light: theme('colors.neutral.50');
$theme-bg-color-dark: theme('colors.neutral.900');
$theme-card-bg-color-light: $theme-bg-color-light;
src/types/config.ts
@@ -1,3 +1,5 @@
+import type I18nKey from '@i18n/I18nKey';
+
export type SiteConfig = {
title: string;
subtitle: string;
@@ -18,11 +20,11 @@ export type ProfileConfig = {
};
export type NavbarConfig = {
- navbarCenterItems: { text: string; href?: string; onclick?: string }[];
+ navbarCenterItems: { text: string | I18nKey; href?: string; onclick?: string }[];
navbarRightItems: {
onlyWide: {
icon: string;
- text?: string;
+ text: string | I18nKey;
href?: string;
onclick?: string;
}[];
src/utils/content-utils.ts
@@ -1,5 +1,7 @@
import { getCollection } from 'astro:content';
import type { BlogPostData } from '@/types/data';
+import { i18n } from '@i18n/translation';
+import I18nKey from '@i18n/I18nKey';
export async function getSortedPosts(): Promise<{ body: string; data: BlogPostData }[]> {
const allBlogPosts = (await getCollection('posts')) as unknown as {
@@ -15,3 +17,51 @@ export async function getSortedPosts(): Promise<{ body: string; data: BlogPostDa
);
return sortedBlogPosts;
}
+
+export async function getCategories(): Promise<string[]> {
+ const allBlogPosts = await getSortedPosts();
+ const categories = [
+ ...new Set(allBlogPosts.map((post) => post.data.category || i18n(I18nKey.uncategorized))),
+ ];
+ return categories;
+}
+
+export async function getTags(): Promise<string[]> {
+ const allBlogPosts = await getSortedPosts();
+ const tags = [...new Set(allBlogPosts.map((post) => post.data.tags || []).flat())];
+ return tags;
+}
+
+export async function getTimeArchives() {
+ const allBlogPosts = await getSortedPosts();
+ const yearReducedPosts = allBlogPosts.reduce(
+ (acc: { year: number; posts: { body: string; data: BlogPostData }[] }[], item) => {
+ const year = item.data.published.getFullYear();
+ const existYear = acc.find((group) => group.year === year);
+ if (existYear) {
+ existYear.posts.push(item);
+ } else {
+ acc.push({ year, posts: [item] });
+ }
+ return acc;
+ },
+ []
+ );
+ const timeReducedPosts = yearReducedPosts.map((group) => ({
+ year: group.year,
+ months: group.posts.reduce(
+ (acc: { month: number; posts: { body: string; data: BlogPostData }[] }[], item) => {
+ const month = item.data.published.getMonth() + 1;
+ const existMonth = acc.find((group) => group.month === month);
+ if (existMonth) {
+ existMonth.posts.push(item);
+ } else {
+ acc.push({ month: month, posts: [item] });
+ }
+ return acc;
+ },
+ []
+ ),
+ }));
+ return timeReducedPosts;
+}
src/config.ts
@@ -6,19 +6,18 @@ import type {
ToolBarConfig,
} from './types/config';
-import I18nKey from '@i18n/i18nKey';
-import { i18n } from '@i18n/translation';
+import I18nKey from '@i18n/I18nKey';
export const siteConfig: SiteConfig = {
title: 'Astral Halo',
subtitle: '',
lang: 'zh_CN', // "en" | "zh_CN" | "zh_TW"
favicon: [''],
- postsPerPage: 10,
+ postsPerPage: 2,
};
export const profileConfig: ProfileConfig = {
- avatar: 'assets/images/demo-avatar.png',
+ // avatar: 'https://example.com/avatar.png', // must be a absolute URL, if not set, will use src/asset/img/avatar.jpg
name: 'John Doe',
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
links: [],
@@ -26,10 +25,10 @@ export const profileConfig: ProfileConfig = {
export const navbarConfig: NavbarConfig = {
navbarCenterItems: [
- { text: i18n(I18nKey.archive), href: '/archives/' },
- { text: i18n(I18nKey.categories), href: '/archives/categories/' },
- { text: i18n(I18nKey.tags), href: '/archives/tags/' },
- { text: i18n(I18nKey.about), href: '/about/' },
+ { text: I18nKey.archive, href: '/archives/' },
+ { text: I18nKey.categories, href: '/archives/categories/' },
+ { text: I18nKey.tags, href: '/archives/tags/' },
+ { text: I18nKey.about, href: '/about/' },
],
navbarRightItems: {
onlyWide: [
@@ -37,19 +36,19 @@ export const navbarConfig: NavbarConfig = {
// 仅在宽度大于 768px 时显示的项目
{
icon: 'material-symbols:rss-feed-rounded',
- text: i18n(I18nKey.subscribe),
+ text: I18nKey.subscribe,
onclick: '',
},
],
always: [
{
icon: 'material-symbols:casino',
- text: i18n(I18nKey.randomPost),
+ text: I18nKey.randomPost,
onclick: '',
},
{
icon: 'material-symbols:search-rounded',
- text: i18n(I18nKey.search),
+ text: I18nKey.search,
onclick: '',
},
],
src/env.d.ts
@@ -1,2 +1,3 @@
+/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference types="astro/client" />
/// <reference path="../.astro/types.d.ts" />
package.json
@@ -17,6 +17,7 @@
"autoprefixer": "^10.4.20",
"postcss-load-config": "^6.0.1",
"sass": "^1.83.4",
+ "sharp": "^0.33.5",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3"
},
@@ -35,4 +36,4 @@
"stylelint-config-standard-scss": "^14.0.0",
"typescript-eslint": "^8.20.0"
}
-}
\ No newline at end of file
+}
tsconfig.json
@@ -11,12 +11,13 @@
}
],
"paths": {
- "@components/*": ["src/components/*"],
"@assets/*": ["src/assets/*"],
+ "@components/*": ["src/components/*"],
"@constants/*": ["src/constants/*"],
- "@utils/*": ["src/utils/*"],
"@i18n/*": ["src/i18n/*"],
"@layouts/*": ["src/layouts/*"],
+ "@scripts/*": ["src/scripts/*"],
+ "@utils/*": ["src/utils/*"],
"@/*": ["src/*"]
}
},