Commit c71d953
Changed files (18)
src
components
i18n
layouts
src/components/misc/GlobalBackground.astro
@@ -0,0 +1,3 @@
+---
+
+---
src/components/widgets/WidgetIconButton.astro
@@ -0,0 +1,38 @@
+---
+import { AstroParameterConflictError } from '@/types/Errors';
+
+interface Props {
+ id: string;
+ description?: string;
+ href?: string;
+ onclick?: string;
+}
+let { id, description, href, onclick } = Astro.props;
+
+if (!description) description = '';
+
+if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
+if (href) onclick = `window.location.href(\`${href}\`)`;
+else if (!onclick) onclick = '';
+---
+
+<button id={id} onclick={onclick} title={description} class="widget-icon-button">
+ <slot />
+</button>
+
+<style lang="stylus">
+.widget-icon-button
+ display flex
+ align-items center
+ padding 0.5rem
+ margin 0.5rem
+ text-align center
+ word-break break-word
+ transition-duration 300ms
+ border-radius 9999px
+ width fit-content
+ height fit-content
+
+ &:hover
+ background-color #3b82f6
+</style>
src/components/widgets/WidgetTextButton.astro
@@ -0,0 +1,35 @@
+---
+import { AstroParameterConflictError } from '@/types/Errors';
+
+interface Props {
+ id: string;
+ href?: string;
+ onclick?: string;
+}
+let { id, href, onclick } = Astro.props;
+
+if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
+if (href) onclick = `window.location.href(\`${href}\`)`;
+else onclick = '';
+---
+
+<button id={id} onclick={onclick} class="widget-text-button">
+ <slot />
+</button>
+
+<style lang="stylus">
+.widget-text-button
+ display flex
+ align-items center
+ padding 0.5rem 0.75rem
+ margin 0.5rem
+ text-align center
+ word-break break-word
+ transition-duration 300ms
+ border-radius 9999px
+ width fit-content
+ height fit-content
+
+ &:hover
+ background-color #3b82f6
+</style>
src/components/Sidebar.astro
@@ -0,0 +1,57 @@
+---
+import { navbarConfig } from '@/config';
+
+import { Icon } from 'astro-icon/components';
+
+import WidgetTextButton from './widgets/WidgetTextButton.astro';
+import WidgetIconButton from './widgets/WidgetIconButton.astro';
+---
+
+<div id="sidebar">
+ <div id="sidebar-mask" class="fixed z-40 hidden w-full h-full bg-black/20 backdrop-blur-md backdrop-saturate-100">
+ </div>
+ <div
+ id="sidebar-menu"
+ class="md:hidden fixed z-50 -right-1/3 w-1/3 h-full duration-500 border-l-2 border-gray-200 bg-white ease-in-out"
+ >
+ {
+ navbarConfig.navbarCenterItems.map((item) => (
+ <WidgetTextButton id="nav-menu-item" href={item.href} onclick={item.onclick}>
+ <span class="text-xl">{item.text}</span>
+ </WidgetTextButton>
+ ))
+ }
+ {
+ navbarConfig.navbarRightItems.onlyWide.map((item) => (
+ <WidgetIconButton id="nav-menu-item" href={item.href} onclick={item.onclick} description={item.text}>
+ <Icon name={item.icon} class="text-2xl" />
+ </WidgetIconButton>
+ ))
+ }
+ </div>
+</div>
+
+<script>
+ const toggleSidebarBtns = document.querySelectorAll('button#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');
+ });
+ });
+
+ sidebarMask?.addEventListener('click', () => {
+ sidebarMask?.classList.add('hidden');
+ sidebarMenu?.classList.remove('-translate-x-full');
+ });
+
+ window.addEventListener('resize', () => {
+ if (window.innerWidth > 768) {
+ sidebarMask?.classList.add('hidden');
+ sidebarMenu?.classList.remove('-translate-x-full');
+ }
+ });
+</script>
src/components/SideMenu.astro
@@ -1,8 +0,0 @@
----
-
----
-
-<div id="side-menu">
- <div id="sm-hide"></div>
- <div id="sm-show"></div>
-</div>
src/i18n/langs/en.ts
@@ -0,0 +1,38 @@
+import Key from '../i18nKey';
+import type { Translation } from '../translation';
+
+export const en: Translation = {
+ [Key.home]: 'Home',
+ [Key.about]: 'About',
+ [Key.archive]: 'Archive',
+ [Key.search]: 'Search',
+
+ [Key.tags]: 'Tags',
+ [Key.categories]: 'Categories',
+ [Key.recentPosts]: 'Recent Posts',
+ [Key.randomPost]: 'Random Post',
+
+ [Key.comments]: 'Comments',
+ [Key.subscribe]: 'Subscribe',
+
+ [Key.untitled]: 'Untitled',
+ [Key.uncategorized]: 'Uncategorized',
+ [Key.noTags]: 'No Tags',
+
+ [Key.wordCount]: 'word',
+ [Key.wordsCount]: 'words',
+ [Key.minuteCount]: 'minute',
+ [Key.minutesCount]: 'minutes',
+ [Key.postCount]: 'post',
+ [Key.postsCount]: 'posts',
+
+ [Key.lightMode]: 'Light',
+ [Key.darkMode]: 'Dark',
+ [Key.systemMode]: 'System',
+
+ [Key.more]: 'More',
+
+ [Key.author]: 'Author',
+ [Key.publishedAt]: 'Published at',
+ [Key.license]: 'License',
+};
src/i18n/langs/zh_CN.ts
@@ -0,0 +1,38 @@
+import Key from '../i18nKey';
+import type { Translation } from '../translation';
+
+export const zh_CN: Translation = {
+ [Key.home]: '主页',
+ [Key.about]: '关于',
+ [Key.archive]: '归档',
+ [Key.search]: '搜索',
+
+ [Key.tags]: '标签',
+ [Key.categories]: '分类',
+ [Key.recentPosts]: '最新文章',
+ [Key.randomPost]: '随机文章',
+
+ [Key.comments]: '评论',
+ [Key.subscribe]: '订阅',
+
+ [Key.untitled]: '无标题',
+ [Key.uncategorized]: '未分类',
+ [Key.noTags]: '无标签',
+
+ [Key.wordCount]: '字',
+ [Key.wordsCount]: '字',
+ [Key.minuteCount]: '分钟',
+ [Key.minutesCount]: '分钟',
+ [Key.postCount]: '篇文章',
+ [Key.postsCount]: '篇文章',
+
+ [Key.lightMode]: '亮色',
+ [Key.darkMode]: '暗色',
+ [Key.systemMode]: '跟随系统',
+
+ [Key.more]: '更多',
+
+ [Key.author]: '作者',
+ [Key.publishedAt]: '发布于',
+ [Key.license]: '许可协议',
+};
src/i18n/langs/zh_TW.ts
@@ -0,0 +1,38 @@
+import Key from '../i18nKey';
+import type { Translation } from '../translation';
+
+export const zh_TW: Translation = {
+ [Key.home]: '首頁',
+ [Key.about]: '關於',
+ [Key.archive]: '彙整',
+ [Key.search]: '搜尋',
+
+ [Key.tags]: '標籤',
+ [Key.categories]: '分類',
+ [Key.recentPosts]: '最新文章',
+ [Key.randomPost]: '隨機文章',
+
+ [Key.comments]: '評論',
+ [Key.subscribe]: '訂閱',
+
+ [Key.untitled]: '無標題',
+ [Key.uncategorized]: '未分類',
+ [Key.noTags]: '無標籤',
+
+ [Key.wordCount]: '字',
+ [Key.wordsCount]: '字',
+ [Key.minuteCount]: '分鐘',
+ [Key.minutesCount]: '分鐘',
+ [Key.postCount]: '篇文章',
+ [Key.postsCount]: '篇文章',
+
+ [Key.lightMode]: '亮色',
+ [Key.darkMode]: '暗色',
+ [Key.systemMode]: '跟隨系統',
+
+ [Key.more]: '更多',
+
+ [Key.author]: '作者',
+ [Key.publishedAt]: '發佈於',
+ [Key.license]: '許可協議',
+};
src/i18n/i18nKey.ts
@@ -0,0 +1,37 @@
+enum I18nKey {
+ home = 'home',
+ about = 'about',
+ archive = 'archive',
+ search = 'search',
+
+ tags = 'tags',
+ categories = 'categories',
+ recentPosts = 'recentPosts',
+ randomPost = 'randomPost',
+
+ comments = 'comments',
+ subscribe = 'subscribe',
+
+ untitled = 'untitled',
+ uncategorized = 'uncategorized',
+ noTags = 'noTags',
+
+ wordCount = 'wordCount',
+ wordsCount = 'wordsCount',
+ minuteCount = 'minuteCount',
+ minutesCount = 'minutesCount',
+ postCount = 'postCount',
+ postsCount = 'postsCount',
+
+ lightMode = 'lightMode',
+ darkMode = 'darkMode',
+ systemMode = 'systemMode',
+
+ more = 'more',
+
+ author = 'author',
+ publishedAt = 'publishedAt',
+ license = 'license',
+}
+
+export default I18nKey;
src/i18n/translation.ts
@@ -0,0 +1,30 @@
+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';
+
+export type Translation = {
+ [K in I18nKey]: string;
+};
+
+const defaultTranslation = en;
+
+const map: { [key: string]: Translation } = {
+ en: en,
+ en_us: en,
+ en_gb: en,
+ en_au: en,
+ zh_cn: zh_CN,
+ zh_tw: zh_TW,
+};
+
+export function getTranslation(lang: string): Translation {
+ return map[lang.toLowerCase()] || defaultTranslation;
+}
+
+export function i18n(key: I18nKey): string {
+ const lang = siteConfig.lang || 'en';
+ return getTranslation(lang)[key];
+}
src/layouts/MainLayout.astro
@@ -1,9 +1,10 @@
---
-import Navbar from '@components/Navbar.astro';
-import SideMenu from '@components/SideMenu.astro';
-
import Layout from './Layout.astro';
+import Navbar from '@components/Navbar.astro';
+import SideToolBar from '@components/SideToolBar.astro';
+import Sidebar from '@components/Sidebar.astro';
+
interface Props {
title?: string;
description?: string;
@@ -15,13 +16,9 @@ const { title, description, lang, pageType } = Astro.props;
<Layout title={title} description={description} lang={lang}>
<slot slot="head" name="head" />
+ <Sidebar />
<div id="body-wrap">
- <div id="navbar-wrapper">
- <Navbar />
- </div>
- <div id="sidemenu-wrapper">
- <SideMenu />
- </div>
+ <Navbar title={title} />
<!-- Main content -->
<div id="content-wrapper">
<slot />
src/types/config.ts
@@ -1,7 +1,15 @@
export type SiteConfig = {
title: string;
-
lang: string;
+ favicon: (string | { src: string; theme?: 'light' | 'dark' })[];
+};
+
+export type NavbarConfig = {
+ navbarCenterItems: { text: string; href?: string; onclick?: string }[];
+ navbarRightItems: {
+ onlyWide: { icon: string; text?: string; href?: string; onclick?: string }[];
+ always: { icon: string; text?: string; href?: string; onclick?: string }[];
+ };
};
export type ProfileConfig = {
src/types/Errors.ts
@@ -0,0 +1,9 @@
+import { AstroError } from 'astro/errors';
+
+export class AstroParameterConflictError extends AstroError {
+ constructor(...params: string[]) {
+ let message = `The following parameters are in conflict: ${params.join(', ')}. Please ensure that only one of these parameters is provided.`;
+ super(message);
+ this.name = 'ParameterConflictError';
+ }
+}
src/config.ts
@@ -1,23 +1,43 @@
-import type {
- LicenseConfig,
- ProfileConfig,
- SiteConfig,
-} from "./types/config";
+import type { LicenseConfig, ProfileConfig, SiteConfig, NavbarConfig } from './types/config';
+
+import I18nKey from '@i18n/i18nKey';
+import { i18n } from '@i18n/translation';
export const siteConfig: SiteConfig = {
- title: "Astral Halo",
- lang: "en", // "en" | "zh_CN"
+ title: 'Astral Halo',
+ lang: 'zh_CN', // "en" | "zh_CN" | "zh_TW"
+ favicon: [''],
};
export const profileConfig: ProfileConfig = {
- avatar: "assets/images/demo-avatar.png",
- name: "John Doe",
- bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+ avatar: 'assets/images/demo-avatar.png',
+ name: 'John Doe',
+ bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
links: [],
};
+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.about), href: '/about' },
+ ],
+ navbarRightItems: {
+ onlyWide: [
+ // Items displayed only when the width is greater than 768px.
+ // 仅在宽度大于 768px 时显示的项目
+ { icon: 'material-symbols:rss-feed-rounded', text: i18n(I18nKey.subscribe), onclick: '' },
+ ],
+ always: [
+ { icon: 'material-symbols:casino', text: i18n(I18nKey.randomPost), onclick: '' },
+ { icon: 'material-symbols:search-rounded', text: i18n(I18nKey.search), onclick: '' },
+ ],
+ },
+};
+
export const licenseConfig: LicenseConfig = {
enable: true,
- name: "CC BY-NC-SA 4.0",
- url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
+ name: 'CC BY-NC-SA 4.0',
+ url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
};
astro.config.mjs
@@ -1,10 +1,14 @@
// @ts-check
// @ts-check
-import { defineConfig } from "astro/config";
+import { defineConfig } from 'astro/config';
+
+import tailwind from '@astrojs/tailwind';
+import icon from 'astro-icon';
// https://astro.build/config
export default defineConfig({
- site: "https://astral-halo.netilify.app/",
- base: "/",
- trailingSlash: "always",
+ site: 'https://astral-halo.netilify.app/',
+ base: '/',
+ trailingSlash: 'always',
+ integrations: [tailwind(), icon()],
});
package.json
@@ -9,19 +9,23 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^5.1.5"
+ "@astrojs/tailwind": "^5.1.4",
+ "@iconify-json/material-symbols": "^1.2.12",
+ "astro": "^5.1.5",
+ "astro-compress": "2.3.5",
+ "astro-icon": "^1.1.5",
+ "stylus": "^0.64.0",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.3"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
- "@astrojs/tailwind": "^5.1.4",
"@astrojs/ts-plugin": "^1.10.4",
- "astro-compress": "2.3.5",
"astro-eslint-parser": "^1.1.0",
"eslint": "^9.16.0",
"eslint-plugin-astro": "^1.3.1",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",
- "typescript": "^5.7.3",
"typescript-eslint": "^8.17.0"
}
}
\ No newline at end of file
tailwind.config.mjs
@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
+ theme: {
+ extend: {
+ letterSpacing: {
+ 'extra-wide': '0.25em',
+ },
+ },
+ },
+ plugins: [],
+};