Commit c50018d
Changed files (30)
src
components
content
posts
pages
archives
categories
[category]
tags
[tag]
posts
styles
utils
src/components/widgets/Button.astro
@@ -30,6 +30,10 @@ if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
&:hover {
@apply bg-[var(--theme-color-light)] dark:bg-[var(--theme-color-dark)];
}
+
+ &:active {
+ @apply brightness-75 scale-95;
+ }
}
</style>
src/components/widgets/MetaIcon.astro
@@ -0,0 +1,25 @@
+---
+import type { ComponentProps } from 'astro/types';
+import { Icon } from 'astro-icon/components';
+
+type Props = ComponentProps<typeof Icon>;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+<Icon {...rest} class:list={['meta-icon', className]} />
+
+<style lang="scss">
+ .meta-icon {
+ width: 2rem;
+ height: 2rem;
+ align-items: center;
+ margin-right: 0.5rem;
+ display: flex;
+ justify-content: center;
+
+ @apply text-[var(--theme-color-light-darken)] dark:text-[var(--theme-color-dark-lighten)];
+ @apply bg-[var(--theme-color-light-transparent)] dark:bg-[var(--theme-color-dark-transparent)];
+ @apply rounded-md;
+ }
+</style>
src/components/widgets/PostCard.astro
@@ -0,0 +1,86 @@
+---
+import MetaIcon from './MetaIcon.astro';
+import ReadMoreButton from './ReadMoreButton.astro';
+import PostCardCover from './PostCardCover.astro';
+
+interface Props {
+ class?: string;
+ title: string;
+ url: string;
+ published: Date;
+ updated?: Date;
+ tags: string[];
+ category?: string;
+ cover?: string;
+ description: string;
+}
+
+const { title, url, published, updated, tags, category, cover } = Astro.props;
+const className = Astro.props.class;
+
+const hasCover = cover !== '' && cover !== undefined && cover !== null;
+
+const metas: ({ icon: string; text: string; link?: string } | undefined)[] = [
+ {
+ icon: 'material-symbols:calendar-clock-outline-rounded',
+ text: published.toLocaleDateString(),
+ },
+ updated && {
+ icon: 'material-symbols:edit-calendar-outline-rounded',
+ text: updated.toLocaleDateString(),
+ },
+ category
+ ? {
+ icon: 'material-symbols:category-outline-rounded',
+ text: category,
+ link: `/archives/categories/${category}/1/`,
+ }
+ : undefined,
+ ...tags.map((tag) => {
+ return {
+ icon: 'material-symbols:tag-rounded',
+ text: tag,
+ link: `/archives/tags/${tag}/1/`,
+ };
+ }),
+];
+---
+
+<div
+ class:list={[
+ 'theme-card-bg theme-border',
+ 'border-2 rounded-xl p-4 flex max-md:flex-col-reverse w-full',
+ className,
+ ]}
+>
+ <div class="items-center px-12 py-7 w-full h-full mr-auto">
+ <div class="text-2xl mb-5"><a href={url}>{title}</a></div>
+ <div class="flex flex-wrap items-center mb-3 gap-x-4 gap-y-2 theme-text-second">
+ {
+ metas.map((meta) => {
+ return (
+ meta && (
+ <div class="flex items-center gap-1">
+ <MetaIcon name={meta.icon} />
+ {meta.link ? (
+ <a href={meta.link} class="text-sm font-medium">
+ {meta.text}
+ </a>
+ ) : (
+ <span class="text-sm font-medium">{meta.text}</span>
+ )}
+ </div>
+ )
+ );
+ })
+ }
+ </div>
+ </div>
+ {
+ hasCover ? (
+ <PostCardCover url={url} title={title} cover={cover} />
+ ) : (
+ <ReadMoreButton href={url} title={title} />
+ )
+ }
+</div>
src/components/widgets/PostCardCover.astro
@@ -0,0 +1,38 @@
+---
+import { Icon } from 'astro-icon/components';
+import { Image } from 'astro:assets';
+
+interface Props {
+ url: string;
+ title: string;
+ cover: string;
+}
+
+const { url, title, cover } = Astro.props;
+---
+
+<a href={url} title={title} class="group">
+ <div><Icon name="material-symbols:chevron-right-rounded" /></div>
+ <Image src={cover} alt={title} inferSize={true} />
+</a>
+
+<style>
+ a {
+ @apply min-h-48 w-full md:w-3/4 md:max-w-96 relative;
+ @apply active:brightness-75 active:scale-95 duration-100;
+ }
+
+ img {
+ @apply w-full h-full object-cover rounded-md;
+ }
+
+ div {
+ @apply absolute inset-0 w-full h-full bg-black/60;
+ @apply flex items-center justify-center;
+ @apply opacity-0 group-hover:opacity-100 duration-300;
+ }
+
+ svg {
+ @apply w-24 h-24 text-white;
+ }
+</style>
src/components/widgets/ReadMoreButton.astro
@@ -0,0 +1,26 @@
+---
+import type { ComponentProps } from 'astro/types';
+import { Icon } from 'astro-icon/components';
+
+type Props = Omit<ComponentProps<typeof Icon>, 'name'>;
+
+const { href, title, ...rest } = Astro.props;
+---
+
+<a href={href} title={title}>
+ <Icon name="material-symbols:chevron-right-rounded" {...rest} />
+</a>
+
+<style>
+ a {
+ @apply max-md:hidden duration-100;
+ @apply hover:brightness-125;
+ @apply active:brightness-75 active:scale-95;
+ }
+
+ svg {
+ @apply min-h-48 h-full w-12 rounded-md;
+ @apply text-[var(--theme-color-light-darken)] dark:text-[var(--theme-color-dark-lighten)];
+ @apply bg-[var(--theme-color-light-transparent)] dark:bg-[var(--theme-color-dark-transparent)];
+ }
+</style>
src/components/CatagoryBar.astro
@@ -1,5 +0,0 @@
----
-
----
-
-<div id="catagory-bar" class="flex w-full rounded-md px-2 py-3"></div>
src/components/CategoryBar.astro
@@ -0,0 +1,5 @@
+---
+
+---
+
+<div id="category-bar" class="flex w-full rounded-md px-2 py-3"></div>
src/components/PostCards.astro
@@ -1,1 +0,0 @@
-<div id="post-cards"></div>
src/components/PostPage.astro
@@ -0,0 +1,39 @@
+---
+import type { BlogPostData } from '@/types/data';
+import { siteConfig } from '@/config';
+import PostCard from './widgets/PostCard.astro';
+
+interface Props {
+ posts: {
+ body: string;
+ data: BlogPostData;
+ }[];
+ currentPage: number;
+ postsPerPage?: number;
+}
+
+let { posts, currentPage, postsPerPage } = Astro.props;
+
+postsPerPage = postsPerPage || siteConfig.postsPerPage;
+
+posts = posts.slice((currentPage - 1) * postsPerPage, currentPage * postsPerPage);
+---
+
+<div id="post-page" class="flex flex-col gap-4">
+ {
+ 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}
+ />
+ );
+ })
+ }
+</div>
src/components/Sidebar.astro
@@ -10,12 +10,12 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
<div id="sidebar" transition:persist>
<div
id="sidebar-mask"
- class="fixed z-40 hidden w-full h-full bg-black/10 backdrop-blur-md backdrop-saturate-100"
+ class="fixed z-40 opacity-0 pointer-events-none w-full h-full bg-black/10 backdrop-blur-md backdrop-saturate-100 duration-500 ease-in-out"
>
</div>
<div
id="sidebar-menu"
- class="fixed md:hidden 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/3 w-1/3 duration-500 ease-in-out border-l-2 theme-border theme-card-bg"
>
<div id="sidebar-site-data"></div>
<DarkModeButton
@@ -54,26 +54,30 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
</div>
<script>
- const toggleSidebarBtns = document.querySelectorAll('button#toggle-sidebar-btn');
+ const toggleSidebarBtns = document.getElementById('nav-toggle-sidebar-btn');
const sidebarMask = document.getElementById('sidebar-mask');
const sidebarMenu = document.getElementById('sidebar-menu');
- toggleSidebarBtns.forEach((btn) => {
- btn.addEventListener('click', () => {
- sidebarMask?.classList.toggle('hidden');
- sidebarMenu?.classList.toggle('-translate-x-full');
- });
+ toggleSidebarBtns?.addEventListener('click', () => {
+ sidebarMask?.classList.remove('opacity-0');
+ sidebarMask?.classList.remove('pointer-events-none');
+ sidebarMenu?.classList.remove('-translate-x-full');
+ sidebarMenu?.removeAttribute('inert');
});
sidebarMask?.addEventListener('click', () => {
- sidebarMask?.classList.add('hidden');
+ sidebarMask?.classList.add('opacity-0');
+ sidebarMask?.classList.add('pointer-events-none');
sidebarMenu?.classList.remove('-translate-x-full');
+ sidebarMenu?.setAttribute('inert', 'true');
});
window.addEventListener('resize', () => {
if (window.innerWidth > 768) {
- sidebarMask?.classList.add('hidden');
+ sidebarMask?.classList.add('opacity-0');
+ sidebarMask?.classList.add('pointer-events-none');
sidebarMenu?.classList.remove('-translate-x-full');
+ sidebarMenu?.setAttribute('inert', 'true');
}
});
</script>
src/components/SideToolBar.astro
@@ -28,10 +28,12 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
stbTrigger?.addEventListener('click', () => {
stbShow?.classList.toggle('translate-x-full');
+ stbShow?.toggleAttribute('inert');
});
stbShowMore?.addEventListener('click', () => {
stbHide?.classList.toggle('translate-x-full');
+ stbHide?.toggleAttribute('inert');
});
window.addEventListener('scroll', () => {
@@ -39,6 +41,8 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
else {
stbShow?.classList.add('translate-x-full');
stbHide?.classList.add('translate-x-full');
+ stbShow?.toggleAttribute('inert', true);
+ stbHide?.toggleAttribute('inert', true);
}
});
</script>
src/content/posts/manual.md
@@ -0,0 +1,41 @@
+---
+title: Maunal
+abbrlink: '12345678'
+published: 2025-01-16 22:01:22
+category: TEST
+tags:
+ - test1
+ - test2
+ - test3
+ - test4
+ - test5
+ - test6
+ - test7
+ - test8
+ - test9
+ - test10
+---
+
+THIS IS A TEST
+
+# h1
+some content
+## h2
+some content
+### h3
+some content
+#### h4
+some content
+##### h5
+some content
+
+# h1 again
+some content
+## h2 again
+some content
+### h3 again
+some content
+#### h4 again
+some content
+##### h5 again
+some content
\ No newline at end of file
src/pages/archives/categories/[category]/[page].astro
@@ -0,0 +1,41 @@
+---
+import { getSortedPosts } from '@utils/content-utils';
+import MainLayout from '@layouts/MainLayout.astro';
+import PostPage from '@components/PostPage.astro';
+import { siteConfig } from '@/config';
+import { i18n } from '@i18n/translation';
+import I18nKey from '@i18n/i18nKey';
+
+export async function getStaticPaths() {
+ const posts = await getSortedPosts();
+ const categories = [
+ ...new Set(posts.map((post) => post.data.category || i18n(I18nKey.uncategorized))),
+ ];
+ return categories
+ .map((category) => {
+ const categoryPosts = posts.filter((post) => post.data.category === category);
+ const pageNum = Math.ceil(categoryPosts.length / siteConfig.postsPerPage);
+ return Array.from({ length: pageNum }, (_, i) => ({
+ params: { category, page: (i + 1).toString() },
+ props: {
+ category,
+ posts: categoryPosts.slice(
+ i * siteConfig.postsPerPage,
+ (i + 1) * siteConfig.postsPerPage
+ ),
+ currentPage: i + 1,
+ },
+ }));
+ })
+ .flat();
+}
+
+const { category, posts, currentPage } = Astro.props;
+---
+
+<MainLayout>
+ <div id="main-content">
+ <PostPage posts={posts} currentPage={currentPage} />
+ </div>
+ <div id="aside-content"></div>
+</MainLayout>
src/pages/archives/categories/index.astro
@@ -0,0 +1,7 @@
+---
+import MainLayout from '@layouts/MainLayout.astro';
+---
+
+<MainLayout>
+ <div></div>
+</MainLayout>
src/pages/posts/[article].astro
@@ -0,0 +1,22 @@
+---
+import { getCollection, render } from 'astro:content';
+import MainLayout from '@layouts/MainLayout.astro';
+
+export async function getStaticPaths() {
+ const articles = await getCollection('posts');
+ return articles.map((article) => ({
+ params: { article: article.data.abbrlink },
+ props: { article },
+ }));
+}
+
+const { article } = Astro.props;
+const { Content } = await render(article);
+---
+
+<MainLayout>
+ <div id="main-content">
+ <Content />
+ </div>
+ <div id="aside-content"></div>
+</MainLayout>
src/pages/[...page].astro
@@ -0,0 +1,32 @@
+---
+import { getSortedPosts } from '@utils/content-utils';
+import { siteConfig } from '@/config';
+import categoryBar from '@components/categoryBar.astro';
+import PostPage from '@components/PostPage.astro';
+import MainLayout from '@layouts/MainLayout.astro';
+
+export async function getStaticPaths() {
+ const articles = await getSortedPosts();
+ const pageNum = Math.ceil(articles.length / siteConfig.postsPerPage);
+ let pages = Array.from({ length: pageNum }).map((_, i) => ({
+ params: { page: i + 1 === 1 ? undefined : 'page/' + (i + 1).toString() },
+ props: { page: i + 1 },
+ }));
+ return pages;
+}
+
+const { page } = Astro.props;
+const articles = await getSortedPosts();
+const posts = articles.slice(
+ (page - 1) * siteConfig.postsPerPage,
+ page * siteConfig.postsPerPage
+);
+---
+
+<MainLayout>
+ <div id="main-content">
+ <categoryBar></categoryBar>
+ <PostPage posts={posts} currentPage={page} />
+ </div>
+ <div id="aside-content"></div>
+</MainLayout>
src/pages/about.astro
@@ -6,5 +6,5 @@ import { i18n } from '@i18n/translation';
---
<MainLayout title={i18n(I18nKey.about)}>
- <div></div>
+ <div>测试</div>
</MainLayout>
src/pages/index.astro
@@ -1,12 +0,0 @@
----
-import PostCards from '@components/PostCards.astro';
-import CatagoryBar from '@components/widgets/CatagoryBar.astro';
-import MainLayout from '@layouts/MainLayout.astro';
----
-
-<MainLayout>
- <div id="main-content">
- <CatagoryBar />
- <PostCards />
- </div>
-</MainLayout>
src/styles/globals.scss
@@ -16,6 +16,14 @@
@apply text-[var(--theme-text-color-light)] dark:text-[var(--theme-text-color-dark)];
}
+ .theme-text-hl {
+ @apply text-[var(--theme-color-light)] dark:text-[var(--theme-color-dark)];
+ }
+
+ .theme-text-second {
+ @apply text-[var(--theme-text-color-second-light)] dark:text-[var(--theme-text-color-second-dark)];
+ }
+
.theme-border {
@apply border-[var(--theme-border-color-light)] dark:border-[var(--theme-border-color-dark)];
}
src/styles/variables.scss
@@ -8,6 +8,8 @@ $theme-card-bg-color-light: $theme-bg-color-light;
$theme-card-bg-color-dark: $theme-bg-color-dark;
$theme-text-color-light: theme('colors.neutral.900');
$theme-text-color-dark: theme('colors.neutral.100');
+$theme-text-color-secondary-light: theme('colors.neutral.600');
+$theme-text-color-secondary-dark: theme('colors.neutral.400');
$theme-border-color-light: theme('colors.neutral.200');
$theme-border-color-dark: theme('colors.neutral.800');
@@ -26,6 +28,8 @@ $theme-border-color-dark: theme('colors.neutral.800');
--theme-card-bg-color-dark: #{$theme-card-bg-color-dark};
--theme-text-color-light: #{$theme-text-color-light};
--theme-text-color-dark: #{$theme-text-color-dark};
+ --theme-text-color-second-light: #{$theme-text-color-secondary-light};
+ --theme-text-color-second-dark: #{$theme-text-color-secondary-dark};
--theme-border-color-light: #{$theme-border-color-light};
--theme-border-color-dark: #{$theme-border-color-dark};
}
src/types/Color.ts
@@ -1,5 +0,0 @@
-export type HexColor = `#${string}`;
-export type RGBColor = `rgb(${number}, ${number}, ${number})`;
-export type RGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
-
-export type Color = HexColor | RGBColor | RGBAColor;
src/types/config.ts
@@ -1,9 +1,9 @@
-import type { Color } from './Color';
-
export type SiteConfig = {
title: string;
+ subtitle: string;
lang: string;
favicon: (string | { src: string; theme?: 'light' | 'dark' })[];
+ postsPerPage: number;
};
export type ProfileConfig = {
src/types/data.ts
@@ -0,0 +1,11 @@
+export type BlogPostData = {
+ body: string;
+ title: string;
+ abbrlink: string;
+ published: Date;
+ description: string;
+ tags: string[];
+ draft?: boolean;
+ cover?: string;
+ category?: string;
+};
src/utils/content-utils.ts
@@ -0,0 +1,17 @@
+import { getCollection } from 'astro:content';
+import type { BlogPostData } from '@/types/data';
+
+export async function getSortedPosts(): Promise<{ body: string; data: BlogPostData }[]> {
+ const allBlogPosts = (await getCollection('posts')) as unknown as {
+ body: string;
+ data: BlogPostData;
+ }[];
+ const sortedBlogPosts = allBlogPosts.sort(
+ (a: { data: BlogPostData }, b: { data: BlogPostData }) => {
+ const dateA = new Date(a.data.published);
+ const dateB = new Date(b.data.published);
+ return dateA > dateB ? -1 : 1;
+ }
+ );
+ return sortedBlogPosts;
+}
src/utils/url-utils.ts
@@ -0,0 +1,14 @@
+export function pathsEqual(path1: string, path2: string) {
+ const normalizedPath1 = path1.replace(/^\/|\/$/g, '').toLowerCase();
+ const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase();
+ return normalizedPath1 === normalizedPath2;
+}
+
+function joinUrl(...parts: string[]): string {
+ const joined = parts.join('/');
+ return joined.replace(/\/+/g, '/');
+}
+
+export function url(path: string) {
+ return joinUrl('', import.meta.env.BASE_URL, path);
+}
src/config.ts
@@ -11,8 +11,10 @@ import { i18n } from '@i18n/translation';
export const siteConfig: SiteConfig = {
title: 'Astral Halo',
+ subtitle: '',
lang: 'zh_CN', // "en" | "zh_CN" | "zh_TW"
favicon: [''],
+ postsPerPage: 10,
};
export const profileConfig: ProfileConfig = {
@@ -24,9 +26,9 @@ export const profileConfig: ProfileConfig = {
export const navbarConfig: NavbarConfig = {
navbarCenterItems: [
- { text: i18n(I18nKey.archive), href: '/archive/' },
- { text: i18n(I18nKey.categories), href: '/categories/' },
- { text: i18n(I18nKey.tags), href: '/tags/' },
+ { 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/' },
],
navbarRightItems: {
src/content.config.ts
@@ -0,0 +1,24 @@
+import { defineCollection, z } from 'astro:content';
+import { glob } from 'astro/loaders';
+
+const postsCollection = defineCollection({
+ loader: glob({
+ pattern: '**/*.md',
+ base: 'src/content/posts',
+ }),
+ schema: z.object({
+ title: z.string(),
+ abbrlink: z.string(),
+ published: z.date(),
+ updated: z.date().optional(),
+ draft: z.boolean().optional().default(false),
+ description: z.string().optional().default(''),
+ cover: z.string().optional().default(''),
+ tags: z.array(z.string()).optional().default([]),
+ category: z.string().optional().default(''),
+ lang: z.string().optional().default(''),
+ }),
+});
+export const collections = {
+ posts: postsCollection,
+};