format all code (#386)

This commit is contained in:
Katsuyuki Karasawa
2025-04-08 23:08:31 +09:00
committed by GitHub
parent 7ea2f7f40f
commit 286b050fa8
61 changed files with 1329 additions and 1307 deletions

View File

@@ -7,7 +7,7 @@
}, },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"ignore": [] "ignore": ["src/**/*.css","src/public/**/*", "dist/**/*", "node_modules/**/*"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,

View File

@@ -1,67 +1,67 @@
--- ---
import { UNCATEGORIZED } from '@constants/constants' import { UNCATEGORIZED } from "@constants/constants";
import I18nKey from '../i18n/i18nKey' import I18nKey from "../i18n/i18nKey";
import { i18n } from '../i18n/translation' import { i18n } from "../i18n/translation";
import { getSortedPosts } from '../utils/content-utils' import { getSortedPosts } from "../utils/content-utils";
import { getPostUrlBySlug } from '../utils/url-utils' import { getPostUrlBySlug } from "../utils/url-utils";
interface Props { interface Props {
keyword?: string keyword?: string;
tags?: string[] tags?: string[];
categories?: string[] categories?: string[];
} }
const { tags, categories } = Astro.props const { tags, categories } = Astro.props;
let posts = await getSortedPosts() let posts = await getSortedPosts();
if (Array.isArray(tags) && tags.length > 0) { if (Array.isArray(tags) && tags.length > 0) {
posts = posts.filter( posts = posts.filter(
post => (post) =>
Array.isArray(post.data.tags) && Array.isArray(post.data.tags) &&
post.data.tags.some(tag => tags.includes(tag)), post.data.tags.some((tag) => tags.includes(tag)),
) );
} }
if (Array.isArray(categories) && categories.length > 0) { if (Array.isArray(categories) && categories.length > 0) {
posts = posts.filter( posts = posts.filter(
post => (post) =>
(post.data.category && categories.includes(post.data.category)) || (post.data.category && categories.includes(post.data.category)) ||
(!post.data.category && categories.includes(UNCATEGORIZED)), (!post.data.category && categories.includes(UNCATEGORIZED)),
) );
} }
const groups: { year: number; posts: typeof posts }[] = (() => { const groups: { year: number; posts: typeof posts }[] = (() => {
const groupedPosts = posts.reduce( const groupedPosts = posts.reduce(
(grouped: { [year: number]: typeof posts }, post) => { (grouped: { [year: number]: typeof posts }, post) => {
const year = post.data.published.getFullYear() const year = post.data.published.getFullYear();
if (!grouped[year]) { if (!grouped[year]) {
grouped[year] = [] grouped[year] = [];
} }
grouped[year].push(post) grouped[year].push(post);
return grouped return grouped;
}, },
{}, {},
) );
// convert the object to an array // convert the object to an array
const groupedPostsArray = Object.keys(groupedPosts).map(key => ({ const groupedPostsArray = Object.keys(groupedPosts).map((key) => ({
year: Number.parseInt(key), year: Number.parseInt(key),
posts: groupedPosts[Number.parseInt(key)], posts: groupedPosts[Number.parseInt(key)],
})) }));
// sort years by latest first // sort years by latest first
groupedPostsArray.sort((a, b) => b.year - a.year) groupedPostsArray.sort((a, b) => b.year - a.year);
return groupedPostsArray return groupedPostsArray;
})() })();
function formatDate(date: Date) { function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, '0') const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, '0') const day = date.getDate().toString().padStart(2, "0");
return `${month}-${day}` return `${month}-${day}`;
} }
function formatTag(tag: string[]) { function formatTag(tag: string[]) {
return tag.map(t => `#${t}`).join(' ') return tag.map((t) => `#${t}`).join(" ");
} }
--- ---

View File

@@ -1,6 +1,6 @@
--- ---
import { siteConfig } from '../config' import { siteConfig } from "../config";
--- ---
<div id="config-carrier" data-hue={siteConfig.themeColor.hue}> <div id="config-carrier" data-hue={siteConfig.themeColor.hue}>

View File

@@ -1,8 +1,8 @@
--- ---
import { profileConfig } from '../config' import { profileConfig } from "../config";
import { url } from '../utils/url-utils' import { url } from "../utils/url-utils";
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear();
--- ---
<!--<div class="border-t border-[var(&#45;&#45;primary)] mx-16 border-dashed py-8 max-w-[var(&#45;&#45;page-width)] flex flex-col items-center justify-center px-6">--> <!--<div class="border-t border-[var(&#45;&#45;primary)] mx-16 border-dashed py-8 max-w-[var(&#45;&#45;page-width)] flex flex-col items-center justify-center px-6">-->

View File

@@ -1,59 +1,59 @@
<script lang="ts"> <script lang="ts">
import type { LIGHT_DARK_MODE } from '@/types/config.ts' import type { LIGHT_DARK_MODE } from "@/types/config.ts";
import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from '@constants/constants.ts' import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants.ts";
import I18nKey from '@i18n/i18nKey' import I18nKey from "@i18n/i18nKey";
import { i18n } from '@i18n/translation' import { i18n } from "@i18n/translation";
import Icon from '@iconify/svelte' import Icon from "@iconify/svelte";
import { import {
applyThemeToDocument, applyThemeToDocument,
getStoredTheme, getStoredTheme,
setTheme, setTheme,
} from '@utils/setting-utils.ts' } from "@utils/setting-utils.ts";
import { onMount } from 'svelte' import { onMount } from "svelte";
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE] const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE];
let mode: LIGHT_DARK_MODE = $state(AUTO_MODE) let mode: LIGHT_DARK_MODE = $state(AUTO_MODE);
onMount(() => { onMount(() => {
mode = getStoredTheme() mode = getStoredTheme();
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)') const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
const changeThemeWhenSchemeChanged: Parameters< const changeThemeWhenSchemeChanged: Parameters<
typeof darkModePreference.addEventListener<'change'> typeof darkModePreference.addEventListener<"change">
>[1] = e => { >[1] = (e) => {
applyThemeToDocument(mode) applyThemeToDocument(mode);
} };
darkModePreference.addEventListener('change', changeThemeWhenSchemeChanged) darkModePreference.addEventListener("change", changeThemeWhenSchemeChanged);
return () => { return () => {
darkModePreference.removeEventListener( darkModePreference.removeEventListener(
'change', "change",
changeThemeWhenSchemeChanged, changeThemeWhenSchemeChanged,
) );
} };
}) });
function switchScheme(newMode: LIGHT_DARK_MODE) { function switchScheme(newMode: LIGHT_DARK_MODE) {
mode = newMode mode = newMode;
setTheme(newMode) setTheme(newMode);
} }
function toggleScheme() { function toggleScheme() {
let i = 0 let i = 0;
for (; i < seq.length; i++) { for (; i < seq.length; i++) {
if (seq[i] === mode) { if (seq[i] === mode) {
break break;
} }
} }
switchScheme(seq[(i + 1) % seq.length]) switchScheme(seq[(i + 1) % seq.length]);
} }
function showPanel() { function showPanel() {
const panel = document.querySelector('#light-dark-panel') const panel = document.querySelector("#light-dark-panel");
panel.classList.remove('float-panel-closed') panel.classList.remove("float-panel-closed");
} }
function hidePanel() { function hidePanel() {
const panel = document.querySelector('#light-dark-panel') const panel = document.querySelector("#light-dark-panel");
panel.classList.add('float-panel-closed') panel.classList.add("float-panel-closed");
} }
</script> </script>

View File

@@ -1,23 +1,23 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from "astro-icon/components";
import DisplaySettings from './widget/DisplaySettings.svelte' import { navBarConfig, siteConfig } from "../config";
import { LinkPreset, type NavBarLink } from '../types/config' import { LinkPresets } from "../constants/link-presets";
import { navBarConfig, siteConfig } from '../config' import { LinkPreset, type NavBarLink } from "../types/config";
import NavMenuPanel from './widget/NavMenuPanel.astro' import { url } from "../utils/url-utils";
import Search from './Search.svelte' import LightDarkSwitch from "./LightDarkSwitch.svelte";
import { LinkPresets } from '../constants/link-presets' import Search from "./Search.svelte";
import LightDarkSwitch from './LightDarkSwitch.svelte' import DisplaySettings from "./widget/DisplaySettings.svelte";
import { url } from '../utils/url-utils' import NavMenuPanel from "./widget/NavMenuPanel.astro";
const className = Astro.props.class const className = Astro.props.class;
let links: NavBarLink[] = navBarConfig.links.map( let links: NavBarLink[] = navBarConfig.links.map(
(item: NavBarLink | LinkPreset): NavBarLink => { (item: NavBarLink | LinkPreset): NavBarLink => {
if (typeof item === 'number') { if (typeof item === "number") {
return LinkPresets[item] return LinkPresets[item];
} }
return item return item;
}, },
) );
--- ---
<div id="navbar" class="z-50 onload-animation"> <div id="navbar" class="z-50 onload-animation">
<div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation --> <div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation -->

View File

@@ -1,46 +1,46 @@
--- ---
import path from 'node:path' import path from "node:path";
import { Icon } from 'astro-icon/components' import { Icon } from "astro-icon/components";
import I18nKey from '../i18n/i18nKey' import I18nKey from "../i18n/i18nKey";
import { i18n } from '../i18n/translation' import { i18n } from "../i18n/translation";
import { getDir } from '../utils/url-utils' import { getDir } from "../utils/url-utils";
import PostMetadata from './PostMeta.astro' import PostMetadata from "./PostMeta.astro";
import ImageWrapper from './misc/ImageWrapper.astro' import ImageWrapper from "./misc/ImageWrapper.astro";
interface Props { interface Props {
class?: string class?: string;
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // biome-ignore lint/suspicious/noExplicitAny: <explanation>
entry: any entry: any;
title: string title: string;
url: string url: string;
published: Date published: Date;
updated?: Date updated?: Date;
tags: string[] tags: string[];
category: string category: string;
image: string image: string;
description: string description: string;
draft: boolean draft: boolean;
style: string style: string;
} }
const { const {
entry, entry,
title, title,
url, url,
published, published,
updated, updated,
tags, tags,
category, category,
image, image,
description, description,
style, style,
} = Astro.props } = Astro.props;
const className = Astro.props.class const className = Astro.props.class;
const hasCover = image !== undefined && image !== null && image !== '' const hasCover = image !== undefined && image !== null && image !== "";
const coverWidth = '28%' const coverWidth = "28%";
const { remarkPluginFrontmatter } = await entry.render() const { remarkPluginFrontmatter } = await entry.render();
--- ---
<div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}> <div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}>
<div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}> <div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}>

View File

@@ -1,21 +1,28 @@
--- ---
import { formatDateToYYYYMMDD } from '../utils/date-utils' import { Icon } from "astro-icon/components";
import { Icon } from 'astro-icon/components' import I18nKey from "../i18n/i18nKey";
import { i18n } from '../i18n/translation' import { i18n } from "../i18n/translation";
import I18nKey from '../i18n/i18nKey' import { formatDateToYYYYMMDD } from "../utils/date-utils";
import { url } from '../utils/url-utils' import { url } from "../utils/url-utils";
interface Props { interface Props {
class: string class: string;
published: Date published: Date;
updated?: Date updated?: Date;
tags: string[] tags: string[];
category: string category: string;
hideTagsForMobile?: boolean hideTagsForMobile?: boolean;
hideUpdateDate?: boolean hideUpdateDate?: boolean;
} }
const { published, updated, tags, category, hideTagsForMobile = false, hideUpdateDate = false } = Astro.props const {
const className = Astro.props.class published,
updated,
tags,
category,
hideTagsForMobile = false,
hideUpdateDate = false,
} = Astro.props;
const className = Astro.props.class;
--- ---
<div class:list={["flex flex-wrap text-neutral-500 dark:text-neutral-400 items-center gap-4 gap-x-4 gap-y-2", className]}> <div class:list={["flex flex-wrap text-neutral-500 dark:text-neutral-400 items-center gap-4 gap-x-4 gap-y-2", className]}>

View File

@@ -1,11 +1,11 @@
--- ---
import { getPostUrlBySlug } from '@utils/url-utils' import { getPostUrlBySlug } from "@utils/url-utils";
import PostCard from './PostCard.astro' import PostCard from "./PostCard.astro";
const { page } = Astro.props const { page } = Astro.props;
let delay = 0 let delay = 0;
const interval = 50 const interval = 50;
--- ---
<div class="transition flex flex-col rounded-[var(--radius-large)] bg-[var(--card-bg)] py-1 md:py-0 md:bg-transparent md:gap-4 mb-4"> <div class="transition flex flex-col rounded-[var(--radius-large)] bg-[var(--card-bg)] py-1 md:py-0 md:bg-transparent md:gap-4 mb-4">
{page.data.map((entry: { data: { draft: boolean; title: string; tags: string[]; category: string; published: Date; image: string; description: string; updated: Date; }; slug: string; }) => { {page.data.map((entry: { data: { draft: boolean; title: string; tags: string[]; category: string; published: Date; image: string; description: string; updated: Date; }; slug: string; }) => {

View File

@@ -1,73 +1,73 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import I18nKey from "@i18n/i18nKey";
import { url } from '@utils/url-utils.ts' import { i18n } from "@i18n/translation";
import { i18n } from '@i18n/translation' import Icon from "@iconify/svelte";
import I18nKey from '@i18n/i18nKey' import { url } from "@utils/url-utils.ts";
import Icon from '@iconify/svelte' import { onMount } from "svelte";
let keywordDesktop = '' let keywordDesktop = "";
let keywordMobile = '' let keywordMobile = "";
let result = [] let result = [];
const fakeResult = [ const fakeResult = [
{ {
url: url('/'), url: url("/"),
meta: { meta: {
title: 'This Is a Fake Search Result', title: "This Is a Fake Search Result",
}, },
excerpt: excerpt:
'Because the search cannot work in the <mark>dev</mark> environment.', "Because the search cannot work in the <mark>dev</mark> environment.",
}, },
{ {
url: url('/'), url: url("/"),
meta: { meta: {
title: 'If You Want to Test the Search', title: "If You Want to Test the Search",
}, },
excerpt: 'Try running <mark>npm build && npm preview</mark> instead.', excerpt: "Try running <mark>npm build && npm preview</mark> instead.",
}, },
] ];
let search = (keyword: string, isDesktop: boolean) => {} let search = (keyword: string, isDesktop: boolean) => {};
onMount(() => { onMount(() => {
search = async (keyword: string, isDesktop: boolean) => { search = async (keyword: string, isDesktop: boolean) => {
let panel = document.getElementById('search-panel') let panel = document.getElementById("search-panel");
if (!panel) return if (!panel) return;
if (!keyword && isDesktop) { if (!keyword && isDesktop) {
panel.classList.add('float-panel-closed') panel.classList.add("float-panel-closed");
return return;
} }
let arr = [] let arr = [];
if (import.meta.env.PROD) { if (import.meta.env.PROD) {
const ret = await pagefind.search(keyword) const ret = await pagefind.search(keyword);
for (const item of ret.results) { for (const item of ret.results) {
arr.push(await item.data()) arr.push(await item.data());
} }
} else { } else {
// Mock data for non-production environment // Mock data for non-production environment
// arr = JSON.parse('[{"url":"/","content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","word_count":187,"filters":{},"meta":{"title":"This Is a Fake Search Result"},"anchors":[{"element":"h2","id":"front-matter-of-posts","text":"Front-matter of Posts","location":34},{"element":"h2","id":"where-to-place-the-post-files","text":"Where to Place the Post Files","location":151}],"weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"raw_content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","raw_url":"/posts/guide/","excerpt":"Because the search cannot work in the <mark>dev</mark> environment.","sub_results":[{"title":"Simple Guides for Fuwari - Fuwari","url":"/posts/guide/","weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"excerpt":"Simple Guides for <mark>Fuwari.</mark> Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers"}]},{"url":"/","content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","word_count":25,"filters":{},"meta":{"title":"If You Want to Test the Search"},"anchors":[{"element":"h1","id":"about","text":"About","location":0},{"element":"h3","id":"sources-of-images-used-in-this-site","text":"Sources of images used in this site","location":8}],"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"raw_content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","raw_url":"/about/","excerpt":"Try running <mark>npm build && npm preview</mark> instead.","sub_results":[{"title":"About","url":"/about/#about","anchor":{"element":"h1","id":"about","text":"About","location":0},"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"excerpt":"About. This is the demo site for <mark>Fuwari.</mark>"}]}]') // arr = JSON.parse('[{"url":"/","content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","word_count":187,"filters":{},"meta":{"title":"This Is a Fake Search Result"},"anchors":[{"element":"h2","id":"front-matter-of-posts","text":"Front-matter of Posts","location":34},{"element":"h2","id":"where-to-place-the-post-files","text":"Where to Place the Post Files","location":151}],"weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"raw_content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","raw_url":"/posts/guide/","excerpt":"Because the search cannot work in the <mark>dev</mark> environment.","sub_results":[{"title":"Simple Guides for Fuwari - Fuwari","url":"/posts/guide/","weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"excerpt":"Simple Guides for <mark>Fuwari.</mark> Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers"}]},{"url":"/","content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","word_count":25,"filters":{},"meta":{"title":"If You Want to Test the Search"},"anchors":[{"element":"h1","id":"about","text":"About","location":0},{"element":"h3","id":"sources-of-images-used-in-this-site","text":"Sources of images used in this site","location":8}],"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"raw_content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","raw_url":"/about/","excerpt":"Try running <mark>npm build && npm preview</mark> instead.","sub_results":[{"title":"About","url":"/about/#about","anchor":{"element":"h1","id":"about","text":"About","location":0},"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"excerpt":"About. This is the demo site for <mark>Fuwari.</mark>"}]}]')
arr = fakeResult arr = fakeResult;
} }
if (!arr.length && isDesktop) { if (!arr.length && isDesktop) {
panel.classList.add('float-panel-closed') panel.classList.add("float-panel-closed");
return return;
} }
if (isDesktop) { if (isDesktop) {
panel.classList.remove('float-panel-closed') panel.classList.remove("float-panel-closed");
} }
result = arr result = arr;
} };
}) });
const togglePanel = () => { const togglePanel = () => {
let panel = document.getElementById('search-panel') let panel = document.getElementById("search-panel");
panel?.classList.toggle('float-panel-closed') panel?.classList.toggle("float-panel-closed");
} };
$: search(keywordDesktop, true) $: search(keywordDesktop, true);
$: search(keywordMobile, false) $: search(keywordMobile, false);
</script> </script>
<!-- search bar for desktop view --> <!-- search bar for desktop view -->

View File

@@ -1,5 +1,5 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from "astro-icon/components";
--- ---
<!-- There can't be a filter on parent element, or it will break `fixed` --> <!-- There can't be a filter on parent element, or it will break `fixed` -->

View File

@@ -1,10 +1,10 @@
--- ---
interface Props { interface Props {
badge?: string badge?: string;
url?: string url?: string;
label?: string label?: string;
} }
const { badge, url, label } = Astro.props const { badge, url, label } = Astro.props;
--- ---
<a href={url} aria-label={label}> <a href={url} aria-label={label}>
<button <button

View File

@@ -1,11 +1,11 @@
--- ---
interface Props { interface Props {
size?: string size?: string;
dot?: boolean dot?: boolean;
href?: string href?: string;
label?: string label?: string;
} }
const { dot, href, label }: Props = Astro.props const { dot, href, label }: Props = Astro.props;
--- ---
<a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg"> <a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg">
{dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>} {dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>}

View File

@@ -1,53 +1,53 @@
--- ---
import type { Page } from 'astro' import type { Page } from "astro";
import { Icon } from 'astro-icon/components' import { Icon } from "astro-icon/components";
import { url } from '../../utils/url-utils' import { url } from "../../utils/url-utils";
interface Props { interface Props {
page: Page page: Page;
class?: string class?: string;
style?: string style?: string;
} }
const { page, style } = Astro.props const { page, style } = Astro.props;
const HIDDEN = -1 const HIDDEN = -1;
const className = Astro.props.class const className = Astro.props.class;
const ADJ_DIST = 2 const ADJ_DIST = 2;
const VISIBLE = ADJ_DIST * 2 + 1 const VISIBLE = ADJ_DIST * 2 + 1;
// for test // for test
let count = 1 let count = 1;
let l = page.currentPage let l = page.currentPage;
let r = page.currentPage let r = page.currentPage;
while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) { while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) {
count += 2 count += 2;
l-- l--;
r++ r++;
} }
while (0 < l - 1 && count < VISIBLE) { while (0 < l - 1 && count < VISIBLE) {
count++ count++;
l-- l--;
} }
while (r + 1 <= page.lastPage && count < VISIBLE) { while (r + 1 <= page.lastPage && count < VISIBLE) {
count++ count++;
r++ r++;
} }
let pages: number[] = [] let pages: number[] = [];
if (l > 1) pages.push(1) if (l > 1) pages.push(1);
if (l === 3) pages.push(2) if (l === 3) pages.push(2);
if (l > 3) pages.push(HIDDEN) if (l > 3) pages.push(HIDDEN);
for (let i = l; i <= r; i++) pages.push(i) for (let i = l; i <= r; i++) pages.push(i);
if (r < page.lastPage - 2) pages.push(HIDDEN) if (r < page.lastPage - 2) pages.push(HIDDEN);
if (r === page.lastPage - 2) pages.push(page.lastPage - 1) if (r === page.lastPage - 2) pages.push(page.lastPage - 1);
if (r < page.lastPage) pages.push(page.lastPage) if (r < page.lastPage) pages.push(page.lastPage);
const getPageUrl = (p: number) => { const getPageUrl = (p: number) => {
if (p === 1) return '/' if (p === 1) return "/";
return `/${p}/` return `/${p}/`;
} };
--- ---
<div class:list={[className, "flex flex-row gap-3 justify-center"]} style={style}> <div class:list={[className, "flex flex-row gap-3 justify-center"]} style={style}>

View File

@@ -1,47 +1,49 @@
--- ---
import path from 'node:path' import path from "node:path";
interface Props { interface Props {
id?: string id?: string;
src: string src: string;
class?: string class?: string;
alt?: string alt?: string;
position?: string position?: string;
basePath?: string basePath?: string;
} }
import { Image } from 'astro:assets' import { Image } from "astro:assets";
import { url } from '../../utils/url-utils' import { url } from "../../utils/url-utils";
const { id, src, alt, position = 'center', basePath = '/' } = Astro.props const { id, src, alt, position = "center", basePath = "/" } = Astro.props;
const className = Astro.props.class const className = Astro.props.class;
const isLocal = !( const isLocal = !(
src.startsWith('/') || src.startsWith("/") ||
src.startsWith('http') || src.startsWith("http") ||
src.startsWith('https') || src.startsWith("https") ||
src.startsWith('data:') src.startsWith("data:")
) );
const isPublic = src.startsWith('/') const isPublic = src.startsWith("/");
// TODO temporary workaround for images dynamic import // TODO temporary workaround for images dynamic import
// https://github.com/withastro/astro/issues/3373 // https://github.com/withastro/astro/issues/3373
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation> // biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
let img let img;
if (isLocal) { if (isLocal) {
const files = import.meta.glob<ImageMetadata>('../../**', { const files = import.meta.glob<ImageMetadata>("../../**", {
import: 'default', import: "default",
}) });
let normalizedPath = path let normalizedPath = path
.normalize(path.join('../../', basePath, src)) .normalize(path.join("../../", basePath, src))
.replace(/\\/g, '/') .replace(/\\/g, "/");
const file = files[normalizedPath] const file = files[normalizedPath];
if (!file) { if (!file) {
console.error(`\n[ERROR] Image file not found: ${normalizedPath.replace('../../', 'src/')}`) console.error(
} `\n[ERROR] Image file not found: ${normalizedPath.replace("../../", "src/")}`,
img = await file() );
}
img = await file();
} }
const imageClass = 'w-full h-full object-cover' const imageClass = "w-full h-full object-cover";
const imageStyle = `object-position: ${position}` const imageStyle = `object-position: ${position}`;
--- ---
<div id={id} class:list={[className, 'overflow-hidden relative']}> <div id={id} class:list={[className, 'overflow-hidden relative']}>
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div> <div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>

View File

@@ -1,22 +1,22 @@
--- ---
import { formatDateToYYYYMMDD } from '../../utils/date-utils' import { Icon } from "astro-icon/components";
import { Icon } from 'astro-icon/components' import { licenseConfig, profileConfig } from "../../config";
import { licenseConfig, profileConfig } from '../../config' import I18nKey from "../../i18n/i18nKey";
import { i18n } from '../../i18n/translation' import { i18n } from "../../i18n/translation";
import I18nKey from '../../i18n/i18nKey' import { formatDateToYYYYMMDD } from "../../utils/date-utils";
interface Props { interface Props {
title: string title: string;
slug: string slug: string;
pubDate: Date pubDate: Date;
class: string class: string;
} }
const { title, pubDate } = Astro.props const { title, pubDate } = Astro.props;
const className = Astro.props.class const className = Astro.props.class;
const profileConf = profileConfig const profileConf = profileConfig;
const licenseConf = licenseConfig const licenseConf = licenseConfig;
const postUrl = decodeURIComponent(Astro.url.toString()) const postUrl = decodeURIComponent(Astro.url.toString());
--- ---
<div class=`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`> <div class=`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`>
<div class="transition font-bold text-black/75 dark:text-white/75"> <div class="transition font-bold text-black/75 dark:text-white/75">

View File

@@ -1,11 +1,11 @@
--- ---
import '@fontsource-variable/jetbrains-mono' import "@fontsource-variable/jetbrains-mono";
import '@fontsource-variable/jetbrains-mono/wght-italic.css' import "@fontsource-variable/jetbrains-mono/wght-italic.css";
interface Props { interface Props {
class: string class: string;
} }
const className = Astro.props.class const className = Astro.props.class;
--- ---
<div data-pagefind-body class=`prose dark:prose-invert prose-base !max-w-none custom-md ${className}`> <div data-pagefind-body class=`prose dark:prose-invert prose-base !max-w-none custom-md ${className}`>
<!--<div class="prose dark:prose-invert max-w-none custom-md">--> <!--<div class="prose dark:prose-invert max-w-none custom-md">-->

View File

@@ -1,25 +1,25 @@
--- ---
import WidgetLayout from './WidgetLayout.astro' import WidgetLayout from "./WidgetLayout.astro";
import { i18n } from '../../i18n/translation' import I18nKey from "../../i18n/i18nKey";
import I18nKey from '../../i18n/i18nKey' import { i18n } from "../../i18n/translation";
import { getCategoryList } from '../../utils/content-utils' import { getCategoryList } from "../../utils/content-utils";
import { getCategoryUrl } from '../../utils/url-utils' import { getCategoryUrl } from "../../utils/url-utils";
import ButtonLink from '../control/ButtonLink.astro' import ButtonLink from "../control/ButtonLink.astro";
const categories = await getCategoryList() const categories = await getCategoryList();
const COLLAPSED_HEIGHT = '7.5rem' const COLLAPSED_HEIGHT = "7.5rem";
const COLLAPSE_THRESHOLD = 5 const COLLAPSE_THRESHOLD = 5;
const isCollapsed = categories.length >= COLLAPSE_THRESHOLD const isCollapsed = categories.length >= COLLAPSE_THRESHOLD;
interface Props { interface Props {
class?: string class?: string;
style?: string style?: string;
} }
const className = Astro.props.class const className = Astro.props.class;
const style = Astro.props.style const style = Astro.props.style;
--- ---
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} <WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}

View File

@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { i18n } from '@i18n/translation' import I18nKey from "@i18n/i18nKey";
import I18nKey from '@i18n/i18nKey' import { i18n } from "@i18n/translation";
import { getDefaultHue, getHue, setHue } from '@utils/setting-utils' import Icon from "@iconify/svelte";
import Icon from '@iconify/svelte' import { getDefaultHue, getHue, setHue } from "@utils/setting-utils";
let hue = getHue() let hue = getHue();
const defaultHue = getDefaultHue() const defaultHue = getDefaultHue();
function resetHue() { function resetHue() {
hue = getDefaultHue() hue = getDefaultHue();
} }
$: if (hue || hue === 0) { $: if (hue || hue === 0) {
setHue(hue) setHue(hue);
} }
</script> </script>

View File

@@ -1,13 +1,13 @@
--- ---
import { type NavBarLink } from '../../types/config' import { Icon } from "astro-icon/components";
import { Icon } from 'astro-icon/components' import { type NavBarLink } from "../../types/config";
import { url } from '../../utils/url-utils' import { url } from "../../utils/url-utils";
interface Props { interface Props {
links: NavBarLink[] links: NavBarLink[];
} }
const links = Astro.props.links const links = Astro.props.links;
--- ---
<div id="nav-menu-panel" class:list={["float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2"]}> <div id="nav-menu-panel" class:list={["float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2"]}>
{links.map((link) => ( {links.map((link) => (

View File

@@ -1,10 +1,10 @@
--- ---
import ImageWrapper from '../misc/ImageWrapper.astro' import { Icon } from "astro-icon/components";
import { Icon } from 'astro-icon/components' import { profileConfig } from "../../config";
import { profileConfig } from '../../config' import { url } from "../../utils/url-utils";
import { url } from '../../utils/url-utils' import ImageWrapper from "../misc/ImageWrapper.astro";
const config = profileConfig const config = profileConfig;
--- ---
<div class="card-base p-3"> <div class="card-base p-3">
<a aria-label="Go to About Page" href={url('/about/')} <a aria-label="Go to About Page" href={url('/about/')}

View File

@@ -1,16 +1,15 @@
--- ---
import Profile from './Profile.astro' import type { MarkdownHeading } from "astro";
import Tag from './Tags.astro' import Categories from "./Categories.astro";
import Categories from './Categories.astro' import Profile from "./Profile.astro";
import type { MarkdownHeading } from 'astro' import Tag from "./Tags.astro";
interface Props { interface Props {
class? : string class?: string;
headings? : MarkdownHeading[] headings?: MarkdownHeading[];
} }
const className = Astro.props.class const className = Astro.props.class;
--- ---
<div id="sidebar" class:list={[className, "w-full"]}> <div id="sidebar" class:list={[className, "w-full"]}>
<div class="flex flex-col w-full gap-4 mb-4"> <div class="flex flex-col w-full gap-4 mb-4">

View File

@@ -1,29 +1,29 @@
--- ---
import type { MarkdownHeading } from 'astro'; import type { MarkdownHeading } from "astro";
import { siteConfig } from "../../config"; import { siteConfig } from "../../config";
interface Props { interface Props {
class?: string class?: string;
headings: MarkdownHeading[] headings: MarkdownHeading[];
} }
let { headings = [] } = Astro.props; let { headings = [] } = Astro.props;
let minDepth = 10; let minDepth = 10;
for (const heading of headings) { for (const heading of headings) {
minDepth = Math.min(minDepth, heading.depth); minDepth = Math.min(minDepth, heading.depth);
} }
const className = Astro.props.class const className = Astro.props.class;
const removeTailingHash = (text: string) => { const removeTailingHash = (text: string) => {
let lastIndexOfHash = text.lastIndexOf('#'); let lastIndexOfHash = text.lastIndexOf("#");
if (lastIndexOfHash !== text.length - 1) { if (lastIndexOfHash !== text.length - 1) {
return text; return text;
} }
return text.substring(0, lastIndexOfHash); return text.substring(0, lastIndexOfHash);
} };
let heading1Count = 1; let heading1Count = 1;

View File

@@ -1,24 +1,24 @@
--- ---
import WidgetLayout from './WidgetLayout.astro' import I18nKey from "../../i18n/i18nKey";
import ButtonTag from '../control/ButtonTag.astro' import { i18n } from "../../i18n/translation";
import { getTagList } from '../../utils/content-utils' import { getTagList } from "../../utils/content-utils";
import { i18n } from '../../i18n/translation' import { url } from "../../utils/url-utils";
import I18nKey from '../../i18n/i18nKey' import ButtonTag from "../control/ButtonTag.astro";
import { url } from '../../utils/url-utils' import WidgetLayout from "./WidgetLayout.astro";
const tags = await getTagList() const tags = await getTagList();
const COLLAPSED_HEIGHT = '7.5rem' const COLLAPSED_HEIGHT = "7.5rem";
const isCollapsed = tags.length >= 20 const isCollapsed = tags.length >= 20;
interface Props { interface Props {
class?: string class?: string;
style?: string style?: string;
} }
const className = Astro.props.class const className = Astro.props.class;
const style = Astro.props.style const style = Astro.props.style;
--- ---
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}> <WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">

View File

@@ -1,17 +1,17 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from "astro-icon/components";
import { i18n } from '../../i18n/translation' import I18nKey from "../../i18n/i18nKey";
import I18nKey from '../../i18n/i18nKey' import { i18n } from "../../i18n/translation";
interface Props { interface Props {
id: string id: string;
name?: string name?: string;
isCollapsed?: boolean isCollapsed?: boolean;
collapsedHeight?: string collapsedHeight?: string;
class?: string class?: string;
style?: string style?: string;
} }
const { id, name, isCollapsed, collapsedHeight, style } = Astro.props const { id, name, isCollapsed, collapsedHeight, style } = Astro.props;
const className = Astro.props.class const className = Astro.props.class;
--- ---
<widget-layout data-id={id} data-is-collapsed={String(isCollapsed)} class={"pb-4 card-base " + className} style={style}> <widget-layout data-id={id} data-is-collapsed={String(isCollapsed)} class={"pb-4 card-base " + className} style={style}>
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 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

View File

@@ -1,82 +1,83 @@
import type { import type {
LicenseConfig, LicenseConfig,
NavBarConfig, NavBarConfig,
ProfileConfig, ProfileConfig,
SiteConfig, SiteConfig,
} from './types/config' } from "./types/config";
import { LinkPreset } from './types/config' import { LinkPreset } from "./types/config";
export const siteConfig: SiteConfig = { export const siteConfig: SiteConfig = {
title: 'Fuwari', title: "Fuwari",
subtitle: 'Demo Site', subtitle: "Demo Site",
lang: 'en', // 'en', 'zh_CN', 'zh_TW', 'ja', 'ko', 'es', 'th' lang: "en", // 'en', 'zh_CN', 'zh_TW', 'ja', 'ko', 'es', 'th'
themeColor: { themeColor: {
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345 hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
fixed: false, // Hide the theme color picker for visitors fixed: false, // Hide the theme color picker for visitors
}, },
banner: { banner: {
enable: false, enable: false,
src: 'assets/images/demo-banner.png', // Relative to the /src directory. Relative to the /public directory if it starts with '/' src: "assets/images/demo-banner.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
position: 'center', // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default
credit: { credit: {
enable: false, // Display the credit text of the banner image enable: false, // Display the credit text of the banner image
text: '', // Credit text to be displayed text: "", // Credit text to be displayed
url: '' // (Optional) URL link to the original artwork or artist's page url: "", // (Optional) URL link to the original artwork or artist's page
} },
}, },
toc: { toc: {
enable: true, // Display the table of contents on the right side of the post enable: true, // Display the table of contents on the right side of the post
depth: 2 // Maximum heading depth to show in the table, from 1 to 3 depth: 2, // Maximum heading depth to show in the table, from 1 to 3
}, },
favicon: [ // Leave this array empty to use the default favicon favicon: [
// { // Leave this array empty to use the default favicon
// src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory // {
// theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode // src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory
// sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes // theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode
// } // sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes
] // }
} ],
};
export const navBarConfig: NavBarConfig = { export const navBarConfig: NavBarConfig = {
links: [ links: [
LinkPreset.Home, LinkPreset.Home,
LinkPreset.Archive, LinkPreset.Archive,
LinkPreset.About, LinkPreset.About,
{ {
name: 'GitHub', name: "GitHub",
url: 'https://github.com/saicaca/fuwari', // Internal links should not include the base path, as it is automatically added url: "https://github.com/saicaca/fuwari", // Internal links should not include the base path, as it is automatically added
external: true, // Show an external link icon and will open in a new tab external: true, // Show an external link icon and will open in a new tab
}, },
], ],
} };
export const profileConfig: ProfileConfig = { export const profileConfig: ProfileConfig = {
avatar: 'assets/images/demo-avatar.png', // Relative to the /src directory. Relative to the /public directory if it starts with '/' avatar: "assets/images/demo-avatar.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
name: 'Lorem Ipsum', name: "Lorem Ipsum",
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
links: [ links: [
{ {
name: 'Twitter', name: "Twitter",
icon: 'fa6-brands:twitter', // Visit https://icones.js.org/ for icon codes icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes
// You will need to install the corresponding icon set if it's not already included // You will need to install the corresponding icon set if it's not already included
// `pnpm add @iconify-json/<icon-set-name>` // `pnpm add @iconify-json/<icon-set-name>`
url: 'https://twitter.com', url: "https://twitter.com",
}, },
{ {
name: 'Steam', name: "Steam",
icon: 'fa6-brands:steam', icon: "fa6-brands:steam",
url: 'https://store.steampowered.com', url: "https://store.steampowered.com",
}, },
{ {
name: 'GitHub', name: "GitHub",
icon: 'fa6-brands:github', icon: "fa6-brands:github",
url: 'https://github.com/saicaca/fuwari', url: "https://github.com/saicaca/fuwari",
}, },
], ],
} };
export const licenseConfig: LicenseConfig = { export const licenseConfig: LicenseConfig = {
enable: true, enable: true,
name: 'CC BY-NC-SA 4.0', name: "CC BY-NC-SA 4.0",
url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/', url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
} };

View File

@@ -1,19 +1,19 @@
export const UNCATEGORIZED = '__uncategorized__' export const UNCATEGORIZED = "__uncategorized__";
export const PAGE_SIZE = 8 export const PAGE_SIZE = 8;
export const LIGHT_MODE = 'light', export const LIGHT_MODE = "light",
DARK_MODE = 'dark', DARK_MODE = "dark",
AUTO_MODE = 'auto' AUTO_MODE = "auto";
export const DEFAULT_THEME = AUTO_MODE export const DEFAULT_THEME = AUTO_MODE;
// Banner height unit: vh // Banner height unit: vh
export const BANNER_HEIGHT = 35 export const BANNER_HEIGHT = 35;
export const BANNER_HEIGHT_EXTEND = 30 export const BANNER_HEIGHT_EXTEND = 30;
export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND;
// The height the main panel overlaps the banner, unit: rem // The height the main panel overlaps the banner, unit: rem
export const MAIN_PANEL_OVERLAPS_BANNER_HEIGHT = 3.5 export const MAIN_PANEL_OVERLAPS_BANNER_HEIGHT = 3.5;
// Page width: rem // Page width: rem
export const PAGE_WIDTH = 75 export const PAGE_WIDTH = 75;

View File

@@ -1,44 +1,44 @@
import type { Favicon } from '@/types/config.ts' import type { Favicon } from "@/types/config.ts";
export const defaultFavicons: Favicon[] = [ export const defaultFavicons: Favicon[] = [
{ {
src: '/favicon/favicon-light-32.png', src: "/favicon/favicon-light-32.png",
theme: 'light', theme: "light",
sizes: '32x32', sizes: "32x32",
}, },
{ {
src: '/favicon/favicon-light-128.png', src: "/favicon/favicon-light-128.png",
theme: 'light', theme: "light",
sizes: '128x128', sizes: "128x128",
}, },
{ {
src: '/favicon/favicon-light-180.png', src: "/favicon/favicon-light-180.png",
theme: 'light', theme: "light",
sizes: '180x180', sizes: "180x180",
}, },
{ {
src: '/favicon/favicon-light-192.png', src: "/favicon/favicon-light-192.png",
theme: 'light', theme: "light",
sizes: '192x192', sizes: "192x192",
}, },
{ {
src: '/favicon/favicon-dark-32.png', src: "/favicon/favicon-dark-32.png",
theme: 'dark', theme: "dark",
sizes: '32x32', sizes: "32x32",
}, },
{ {
src: '/favicon/favicon-dark-128.png', src: "/favicon/favicon-dark-128.png",
theme: 'dark', theme: "dark",
sizes: '128x128', sizes: "128x128",
}, },
{ {
src: '/favicon/favicon-dark-180.png', src: "/favicon/favicon-dark-180.png",
theme: 'dark', theme: "dark",
sizes: '180x180', sizes: "180x180",
}, },
{ {
src: '/favicon/favicon-dark-192.png', src: "/favicon/favicon-dark-192.png",
theme: 'dark', theme: "dark",
sizes: '192x192', sizes: "192x192",
}, },
] ];

View File

@@ -1,18 +1,18 @@
import { LinkPreset, type NavBarLink } from '@/types/config' import { LinkPreset, type NavBarLink } from "@/types/config";
import I18nKey from '@i18n/i18nKey' import I18nKey from "@i18n/i18nKey";
import { i18n } from '@i18n/translation' import { i18n } from "@i18n/translation";
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = { export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
[LinkPreset.Home]: { [LinkPreset.Home]: {
name: i18n(I18nKey.home), name: i18n(I18nKey.home),
url: '/', url: "/",
}, },
[LinkPreset.About]: { [LinkPreset.About]: {
name: i18n(I18nKey.about), name: i18n(I18nKey.about),
url: '/about/', url: "/about/",
}, },
[LinkPreset.Archive]: { [LinkPreset.Archive]: {
name: i18n(I18nKey.archive), name: i18n(I18nKey.archive),
url: '/archive/', url: "/archive/",
}, },
} };

View File

@@ -1,24 +1,24 @@
import { defineCollection, z } from 'astro:content' import { defineCollection, z } from "astro:content";
const postsCollection = defineCollection({ const postsCollection = defineCollection({
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
published: z.date(), published: z.date(),
updated: z.date().optional(), updated: z.date().optional(),
draft: z.boolean().optional().default(false), draft: z.boolean().optional().default(false),
description: z.string().optional().default(''), description: z.string().optional().default(""),
image: z.string().optional().default(''), image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]), tags: z.array(z.string()).optional().default([]),
category: z.string().optional().default(''), category: z.string().optional().default(""),
lang: z.string().optional().default(''), lang: z.string().optional().default(""),
/* For internal use */ /* For internal use */
prevTitle: z.string().default(''), prevTitle: z.string().default(""),
prevSlug: z.string().default(''), prevSlug: z.string().default(""),
nextTitle: z.string().default(''), nextTitle: z.string().default(""),
nextSlug: z.string().default(''), nextSlug: z.string().default(""),
}), }),
}) });
export const collections = { export const collections = {
posts: postsCollection, posts: postsCollection,
} };

10
src/global.d.ts vendored
View File

@@ -1,8 +1,8 @@
import type { AstroIntegration } from '@swup/astro' import type { AstroIntegration } from "@swup/astro";
declare global { declare global {
interface Window { interface Window {
// type from '@swup/astro' is incorrect // type from '@swup/astro' is incorrect
swup: AstroIntegration swup: AstroIntegration;
} }
} }

View File

@@ -1,37 +1,37 @@
enum I18nKey { enum I18nKey {
home = 'home', home = "home",
about = 'about', about = "about",
archive = 'archive', archive = "archive",
search = 'search', search = "search",
tags = 'tags', tags = "tags",
categories = 'categories', categories = "categories",
recentPosts = 'recentPosts', recentPosts = "recentPosts",
comments = 'comments', comments = "comments",
untitled = 'untitled', untitled = "untitled",
uncategorized = 'uncategorized', uncategorized = "uncategorized",
noTags = 'noTags', noTags = "noTags",
wordCount = 'wordCount', wordCount = "wordCount",
wordsCount = 'wordsCount', wordsCount = "wordsCount",
minuteCount = 'minuteCount', minuteCount = "minuteCount",
minutesCount = 'minutesCount', minutesCount = "minutesCount",
postCount = 'postCount', postCount = "postCount",
postsCount = 'postsCount', postsCount = "postsCount",
themeColor = 'themeColor', themeColor = "themeColor",
lightMode = 'lightMode', lightMode = "lightMode",
darkMode = 'darkMode', darkMode = "darkMode",
systemMode = 'systemMode', systemMode = "systemMode",
more = 'more', more = "more",
author = 'author', author = "author",
publishedAt = 'publishedAt', publishedAt = "publishedAt",
license = 'license', license = "license",
} }
export default I18nKey export default I18nKey;

View File

@@ -1,38 +1,38 @@
import Key from '../i18nKey' import Key from "../i18nKey";
import type { Translation } from '../translation' import type { Translation } from "../translation";
export const en: Translation = { export const en: Translation = {
[Key.home]: 'Home', [Key.home]: "Home",
[Key.about]: 'About', [Key.about]: "About",
[Key.archive]: 'Archive', [Key.archive]: "Archive",
[Key.search]: 'Search', [Key.search]: "Search",
[Key.tags]: 'Tags', [Key.tags]: "Tags",
[Key.categories]: 'Categories', [Key.categories]: "Categories",
[Key.recentPosts]: 'Recent Posts', [Key.recentPosts]: "Recent Posts",
[Key.comments]: 'Comments', [Key.comments]: "Comments",
[Key.untitled]: 'Untitled', [Key.untitled]: "Untitled",
[Key.uncategorized]: 'Uncategorized', [Key.uncategorized]: "Uncategorized",
[Key.noTags]: 'No Tags', [Key.noTags]: "No Tags",
[Key.wordCount]: 'word', [Key.wordCount]: "word",
[Key.wordsCount]: 'words', [Key.wordsCount]: "words",
[Key.minuteCount]: 'minute', [Key.minuteCount]: "minute",
[Key.minutesCount]: 'minutes', [Key.minutesCount]: "minutes",
[Key.postCount]: 'post', [Key.postCount]: "post",
[Key.postsCount]: 'posts', [Key.postsCount]: "posts",
[Key.themeColor]: 'Theme Color', [Key.themeColor]: "Theme Color",
[Key.lightMode]: 'Light', [Key.lightMode]: "Light",
[Key.darkMode]: 'Dark', [Key.darkMode]: "Dark",
[Key.systemMode]: 'System', [Key.systemMode]: "System",
[Key.more]: 'More', [Key.more]: "More",
[Key.author]: 'Author', [Key.author]: "Author",
[Key.publishedAt]: 'Published at', [Key.publishedAt]: "Published at",
[Key.license]: 'License', [Key.license]: "License",
} };

View File

@@ -1,38 +1,38 @@
import Key from '../i18nKey' import Key from "../i18nKey";
import type { Translation } from '../translation' import type { Translation } from "../translation";
export const es: Translation = { export const es: Translation = {
[Key.home]: 'Inicio', [Key.home]: "Inicio",
[Key.about]: 'Sobre mí', [Key.about]: "Sobre mí",
[Key.archive]: 'Archivo', [Key.archive]: "Archivo",
[Key.search]: 'Buscar', [Key.search]: "Buscar",
[Key.tags]: 'Etiquetas', [Key.tags]: "Etiquetas",
[Key.categories]: 'Categorías', [Key.categories]: "Categorías",
[Key.recentPosts]: 'Publicaciones recientes', [Key.recentPosts]: "Publicaciones recientes",
[Key.comments]: 'Comentarios', [Key.comments]: "Comentarios",
[Key.untitled]: 'Sin título', [Key.untitled]: "Sin título",
[Key.uncategorized]: 'Sin categoría', [Key.uncategorized]: "Sin categoría",
[Key.noTags]: 'Sin etiquetas', [Key.noTags]: "Sin etiquetas",
[Key.wordCount]: 'palabra', [Key.wordCount]: "palabra",
[Key.wordsCount]: 'palabras', [Key.wordsCount]: "palabras",
[Key.minuteCount]: 'minuto', [Key.minuteCount]: "minuto",
[Key.minutesCount]: 'minutos', [Key.minutesCount]: "minutos",
[Key.postCount]: 'publicación', [Key.postCount]: "publicación",
[Key.postsCount]: 'publicaciones', [Key.postsCount]: "publicaciones",
[Key.themeColor]: 'Color del tema', [Key.themeColor]: "Color del tema",
[Key.lightMode]: 'Claro', [Key.lightMode]: "Claro",
[Key.darkMode]: 'Oscuro', [Key.darkMode]: "Oscuro",
[Key.systemMode]: 'Sistema', [Key.systemMode]: "Sistema",
[Key.more]: 'Más', [Key.more]: "Más",
[Key.author]: 'Autor', [Key.author]: "Autor",
[Key.publishedAt]: 'Publicado el', [Key.publishedAt]: "Publicado el",
[Key.license]: 'Licencia', [Key.license]: "Licencia",
} };

View File

@@ -1,38 +1,38 @@
import Key from '../i18nKey' import Key from "../i18nKey";
import type { Translation } from '../translation' import type { Translation } from "../translation";
export const ja: Translation = { export const ja: Translation = {
[Key.home]: 'Home', [Key.home]: "Home",
[Key.about]: 'About', [Key.about]: "About",
[Key.archive]: 'Archive', [Key.archive]: "Archive",
[Key.search]: '検索', [Key.search]: "検索",
[Key.tags]: 'タグ', [Key.tags]: "タグ",
[Key.categories]: 'カテゴリ', [Key.categories]: "カテゴリ",
[Key.recentPosts]: '最近の投稿', [Key.recentPosts]: "最近の投稿",
[Key.comments]: 'コメント', [Key.comments]: "コメント",
[Key.untitled]: 'タイトルなし', [Key.untitled]: "タイトルなし",
[Key.uncategorized]: 'カテゴリなし', [Key.uncategorized]: "カテゴリなし",
[Key.noTags]: 'タグなし', [Key.noTags]: "タグなし",
[Key.wordCount]: '文字', [Key.wordCount]: "文字",
[Key.wordsCount]: '文字', [Key.wordsCount]: "文字",
[Key.minuteCount]: '分', [Key.minuteCount]: "分",
[Key.minutesCount]: '分', [Key.minutesCount]: "分",
[Key.postCount]: '件の投稿', [Key.postCount]: "件の投稿",
[Key.postsCount]: '件の投稿', [Key.postsCount]: "件の投稿",
[Key.themeColor]: 'テーマカラー', [Key.themeColor]: "テーマカラー",
[Key.lightMode]: 'ライト', [Key.lightMode]: "ライト",
[Key.darkMode]: 'ダーク', [Key.darkMode]: "ダーク",
[Key.systemMode]: 'システム', [Key.systemMode]: "システム",
[Key.more]: 'もっと', [Key.more]: "もっと",
[Key.author]: '作者', [Key.author]: "作者",
[Key.publishedAt]: '公開日', [Key.publishedAt]: "公開日",
[Key.license]: 'ライセンス', [Key.license]: "ライセンス",
} };

View File

@@ -1,38 +1,38 @@
import Key from '../i18nKey' import Key from "../i18nKey";
import type { Translation } from '../translation' import type { Translation } from "../translation";
export const ko: Translation = { export const ko: Translation = {
[Key.home]: '홈', [Key.home]: "홈",
[Key.about]: '소개', [Key.about]: "소개",
[Key.archive]: '아카이브', [Key.archive]: "아카이브",
[Key.search]: '검색', [Key.search]: "검색",
[Key.tags]: '태그', [Key.tags]: "태그",
[Key.categories]: '카테고리', [Key.categories]: "카테고리",
[Key.recentPosts]: '최근 게시물', [Key.recentPosts]: "최근 게시물",
[Key.comments]: '댓글', [Key.comments]: "댓글",
[Key.untitled]: '제목 없음', [Key.untitled]: "제목 없음",
[Key.uncategorized]: '분류되지 않음', [Key.uncategorized]: "분류되지 않음",
[Key.noTags]: '태그 없음', [Key.noTags]: "태그 없음",
[Key.wordCount]: '단어', [Key.wordCount]: "단어",
[Key.wordsCount]: '단어', [Key.wordsCount]: "단어",
[Key.minuteCount]: '분', [Key.minuteCount]: "분",
[Key.minutesCount]: '분', [Key.minutesCount]: "분",
[Key.postCount]: '게시물', [Key.postCount]: "게시물",
[Key.postsCount]: '게시물', [Key.postsCount]: "게시물",
[Key.themeColor]: '테마 색상', [Key.themeColor]: "테마 색상",
[Key.lightMode]: '밝은 모드', [Key.lightMode]: "밝은 모드",
[Key.darkMode]: '어두운 모드', [Key.darkMode]: "어두운 모드",
[Key.systemMode]: '시스템 모드', [Key.systemMode]: "시스템 모드",
[Key.more]: '더 보기', [Key.more]: "더 보기",
[Key.author]: '저자', [Key.author]: "저자",
[Key.publishedAt]: '게시일', [Key.publishedAt]: "게시일",
[Key.license]: '라이선스', [Key.license]: "라이선스",
} };

View File

@@ -1,38 +1,38 @@
import Key from '../i18nKey' import Key from "../i18nKey";
import type { Translation } from '../translation' import type { Translation } from "../translation";
export const th: Translation = { export const th: Translation = {
[Key.home]: 'หน้าแรก', [Key.home]: "หน้าแรก",
[Key.about]: 'เกี่ยวกับ', [Key.about]: "เกี่ยวกับ",
[Key.archive]: 'คลัง', [Key.archive]: "คลัง",
[Key.search]: 'ค้นหา', [Key.search]: "ค้นหา",
[Key.tags]: 'ป้ายกำกับ', [Key.tags]: "ป้ายกำกับ",
[Key.categories]: 'หมวดหมู่', [Key.categories]: "หมวดหมู่",
[Key.recentPosts]: 'โพสต์ล่าสุด', [Key.recentPosts]: "โพสต์ล่าสุด",
[Key.comments]: 'ความคิดเห็น', [Key.comments]: "ความคิดเห็น",
[Key.untitled]: 'ไม่ได้ตั้งชื่อ', [Key.untitled]: "ไม่ได้ตั้งชื่อ",
[Key.uncategorized]: 'ไม่ได้จัดหมวดหมู่', [Key.uncategorized]: "ไม่ได้จัดหมวดหมู่",
[Key.noTags]: 'ไม่มีป้ายกำกับ', [Key.noTags]: "ไม่มีป้ายกำกับ",
[Key.wordCount]: 'คำ', [Key.wordCount]: "คำ",
[Key.wordsCount]: 'คำ', [Key.wordsCount]: "คำ",
[Key.minuteCount]: 'นาที', [Key.minuteCount]: "นาที",
[Key.minutesCount]: 'นาที', [Key.minutesCount]: "นาที",
[Key.postCount]: 'โพสต์', [Key.postCount]: "โพสต์",
[Key.postsCount]: 'โพสต์', [Key.postsCount]: "โพสต์",
[Key.themeColor]: 'สีของธีม', [Key.themeColor]: "สีของธีม",
[Key.lightMode]: 'สว่าง', [Key.lightMode]: "สว่าง",
[Key.darkMode]: 'มืด', [Key.darkMode]: "มืด",
[Key.systemMode]: 'ตามระบบ', [Key.systemMode]: "ตามระบบ",
[Key.more]: 'ดูเพิ่ม', [Key.more]: "ดูเพิ่ม",
[Key.author]: 'ผู้เขียน', [Key.author]: "ผู้เขียน",
[Key.publishedAt]: 'เผยแพร่เมื่อ', [Key.publishedAt]: "เผยแพร่เมื่อ",
[Key.license]: 'สัญญาอนุญาต', [Key.license]: "สัญญาอนุญาต",
} };

View File

@@ -1,38 +1,38 @@
import Key from '../i18nKey' import Key from "../i18nKey";
import type { Translation } from '../translation' import type { Translation } from "../translation";
export const zh_CN: Translation = { export const zh_CN: Translation = {
[Key.home]: '主页', [Key.home]: "主页",
[Key.about]: '关于', [Key.about]: "关于",
[Key.archive]: '归档', [Key.archive]: "归档",
[Key.search]: '搜索', [Key.search]: "搜索",
[Key.tags]: '标签', [Key.tags]: "标签",
[Key.categories]: '分类', [Key.categories]: "分类",
[Key.recentPosts]: '最新文章', [Key.recentPosts]: "最新文章",
[Key.comments]: '评论', [Key.comments]: "评论",
[Key.untitled]: '无标题', [Key.untitled]: "无标题",
[Key.uncategorized]: '未分类', [Key.uncategorized]: "未分类",
[Key.noTags]: '无标签', [Key.noTags]: "无标签",
[Key.wordCount]: '字', [Key.wordCount]: "字",
[Key.wordsCount]: '字', [Key.wordsCount]: "字",
[Key.minuteCount]: '分钟', [Key.minuteCount]: "分钟",
[Key.minutesCount]: '分钟', [Key.minutesCount]: "分钟",
[Key.postCount]: '篇文章', [Key.postCount]: "篇文章",
[Key.postsCount]: '篇文章', [Key.postsCount]: "篇文章",
[Key.themeColor]: '主题色', [Key.themeColor]: "主题色",
[Key.lightMode]: '亮色', [Key.lightMode]: "亮色",
[Key.darkMode]: '暗色', [Key.darkMode]: "暗色",
[Key.systemMode]: '跟随系统', [Key.systemMode]: "跟随系统",
[Key.more]: '更多', [Key.more]: "更多",
[Key.author]: '作者', [Key.author]: "作者",
[Key.publishedAt]: '发布于', [Key.publishedAt]: "发布于",
[Key.license]: '许可协议', [Key.license]: "许可协议",
} };

View File

@@ -1,38 +1,38 @@
import Key from '../i18nKey' import Key from "../i18nKey";
import type { Translation } from '../translation' import type { Translation } from "../translation";
export const zh_TW: Translation = { export const zh_TW: Translation = {
[Key.home]: '首頁', [Key.home]: "首頁",
[Key.about]: '關於', [Key.about]: "關於",
[Key.archive]: '彙整', [Key.archive]: "彙整",
[Key.search]: '搜尋', [Key.search]: "搜尋",
[Key.tags]: '標籤', [Key.tags]: "標籤",
[Key.categories]: '分類', [Key.categories]: "分類",
[Key.recentPosts]: '最新文章', [Key.recentPosts]: "最新文章",
[Key.comments]: '評論', [Key.comments]: "評論",
[Key.untitled]: '無標題', [Key.untitled]: "無標題",
[Key.uncategorized]: '未分類', [Key.uncategorized]: "未分類",
[Key.noTags]: '無標籤', [Key.noTags]: "無標籤",
[Key.wordCount]: '字', [Key.wordCount]: "字",
[Key.wordsCount]: '字', [Key.wordsCount]: "字",
[Key.minuteCount]: '分鐘', [Key.minuteCount]: "分鐘",
[Key.minutesCount]: '分鐘', [Key.minutesCount]: "分鐘",
[Key.postCount]: '篇文章', [Key.postCount]: "篇文章",
[Key.postsCount]: '篇文章', [Key.postsCount]: "篇文章",
[Key.themeColor]: '主題色', [Key.themeColor]: "主題色",
[Key.lightMode]: '亮色', [Key.lightMode]: "亮色",
[Key.darkMode]: '暗色', [Key.darkMode]: "暗色",
[Key.systemMode]: '跟隨系統', [Key.systemMode]: "跟隨系統",
[Key.more]: '更多', [Key.more]: "更多",
[Key.author]: '作者', [Key.author]: "作者",
[Key.publishedAt]: '發佈於', [Key.publishedAt]: "發佈於",
[Key.license]: '許可協議', [Key.license]: "許可協議",
} };

View File

@@ -1,40 +1,40 @@
import { siteConfig } from '../config' import { siteConfig } from "../config";
import type I18nKey from './i18nKey' import type I18nKey from "./i18nKey";
import { en } from './languages/en' import { en } from "./languages/en";
import { es } from './languages/es' import { es } from "./languages/es";
import { ja } from './languages/ja' import { ja } from "./languages/ja";
import { ko } from './languages/ko' import { ko } from "./languages/ko";
import { th } from './languages/th' import { th } from "./languages/th";
import { zh_CN } from './languages/zh_CN' import { zh_CN } from "./languages/zh_CN";
import { zh_TW } from './languages/zh_TW' import { zh_TW } from "./languages/zh_TW";
export type Translation = { export type Translation = {
[K in I18nKey]: string [K in I18nKey]: string;
} };
const defaultTranslation = en const defaultTranslation = en;
const map: { [key: string]: Translation } = { const map: { [key: string]: Translation } = {
es: es, es: es,
en: en, en: en,
en_us: en, en_us: en,
en_gb: en, en_gb: en,
en_au: en, en_au: en,
zh_cn: zh_CN, zh_cn: zh_CN,
zh_tw: zh_TW, zh_tw: zh_TW,
ja: ja, ja: ja,
ja_jp: ja, ja_jp: ja,
ko: ko, ko: ko,
ko_kr: ko, ko_kr: ko,
th: th, th: th,
th_th: th, th_th: th,
} };
export function getTranslation(lang: string): Translation { export function getTranslation(lang: string): Translation {
return map[lang.toLowerCase()] || defaultTranslation return map[lang.toLowerCase()] || defaultTranslation;
} }
export function i18n(key: I18nKey): string { export function i18n(key: I18nKey): string {
const lang = siteConfig.lang || 'en' const lang = siteConfig.lang || "en";
return getTranslation(lang)[key] return getTranslation(lang)[key];
} }

View File

@@ -1,75 +1,75 @@
--- ---
import '@fontsource/roboto/400.css' import "@fontsource/roboto/400.css";
import '@fontsource/roboto/500.css' import "@fontsource/roboto/500.css";
import '@fontsource/roboto/700.css' import "@fontsource/roboto/700.css";
import { profileConfig, siteConfig } from '@/config' import { profileConfig, siteConfig } from "@/config";
import ConfigCarrier from '@components/ConfigCarrier.astro' import ConfigCarrier from "@components/ConfigCarrier.astro";
import { import {
AUTO_MODE, AUTO_MODE,
BANNER_HEIGHT, BANNER_HEIGHT,
BANNER_HEIGHT_EXTEND, BANNER_HEIGHT_EXTEND,
BANNER_HEIGHT_HOME, BANNER_HEIGHT_HOME,
DARK_MODE, DARK_MODE,
DEFAULT_THEME, DEFAULT_THEME,
LIGHT_MODE, LIGHT_MODE,
PAGE_WIDTH, PAGE_WIDTH,
} from '../constants/constants' } from "../constants/constants";
import { defaultFavicons } from '../constants/icon' import { defaultFavicons } from "../constants/icon";
import type { Favicon } from '../types/config' import type { Favicon } from "../types/config";
import { url, pathsEqual } from '../utils/url-utils' import { url, pathsEqual } from "../utils/url-utils";
import 'katex/dist/katex.css' import "katex/dist/katex.css";
interface Props { interface Props {
title?: string title?: string;
banner?: string banner?: string;
description?: string description?: string;
lang?: string lang?: string;
setOGTypeArticle?: boolean setOGTypeArticle?: boolean;
} }
let { title, banner, description, lang, setOGTypeArticle } = Astro.props let { title, banner, description, lang, setOGTypeArticle } = Astro.props;
// apply a class to the body element to decide the height of the banner, only used for initial page load // apply a class to the body element to decide the height of the banner, only used for initial page load
// Swup can update the body for each page visit, but it's after the page transition, causing a delay for banner height change // Swup can update the body for each page visit, but it's after the page transition, causing a delay for banner height change
// so use Swup hooks instead to change the height immediately when a link is clicked // so use Swup hooks instead to change the height immediately when a link is clicked
const isHomePage = pathsEqual(Astro.url.pathname, url('/')) const isHomePage = pathsEqual(Astro.url.pathname, url("/"));
// defines global css variables // defines global css variables
// why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757 // why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757
const configHue = siteConfig.themeColor.hue const configHue = siteConfig.themeColor.hue;
if (!banner || typeof banner !== 'string' || banner.trim() === '') { if (!banner || typeof banner !== "string" || banner.trim() === "") {
banner = siteConfig.banner.src banner = siteConfig.banner.src;
} }
// TODO don't use post cover as banner for now // TODO don't use post cover as banner for now
banner = siteConfig.banner.src banner = siteConfig.banner.src;
const enableBanner = siteConfig.banner.enable const enableBanner = siteConfig.banner.enable;
let pageTitle: string let pageTitle: string;
if (title) { if (title) {
pageTitle = `${title} - ${siteConfig.title}` pageTitle = `${title} - ${siteConfig.title}`;
} else { } else {
pageTitle = `${siteConfig.title} - ${siteConfig.subtitle}` pageTitle = `${siteConfig.title} - ${siteConfig.subtitle}`;
} }
const favicons: Favicon[] = const favicons: Favicon[] =
siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons;
// const siteLang = siteConfig.lang.replace('_', '-') // const siteLang = siteConfig.lang.replace('_', '-')
if (!lang) { if (!lang) {
lang = `${siteConfig.lang}` lang = `${siteConfig.lang}`;
} }
const siteLang = lang.replace('_', '-') const siteLang = lang.replace("_", "-");
const bannerOffsetByPosition = { const bannerOffsetByPosition = {
top: `${BANNER_HEIGHT_EXTEND}vh`, top: `${BANNER_HEIGHT_EXTEND}vh`,
center: `${BANNER_HEIGHT_EXTEND / 2}vh`, center: `${BANNER_HEIGHT_EXTEND / 2}vh`,
bottom: '0', bottom: "0",
} };
const bannerOffset = const bannerOffset =
bannerOffsetByPosition[siteConfig.banner.position || 'center'] bannerOffsetByPosition[siteConfig.banner.position || "center"];
--- ---
<!DOCTYPE html> <!DOCTYPE html>

View File

@@ -1,31 +1,44 @@
--- ---
import Footer from '@components/Footer.astro' import Footer from "@components/Footer.astro";
import Navbar from '@components/Navbar.astro' import Navbar from "@components/Navbar.astro";
import BackToTop from '@components/control/BackToTop.astro' import BackToTop from "@components/control/BackToTop.astro";
import SideBar from '@components/widget/SideBar.astro' import SideBar from "@components/widget/SideBar.astro";
import Layout from './Layout.astro' import type { MarkdownHeading } from "astro";
import { Icon } from 'astro-icon/components' import { Icon } from "astro-icon/components";
import { siteConfig } from '../config'
import type { MarkdownHeading } from 'astro'
import TOC from "../components/widget/TOC.astro";
import ImageWrapper from "../components/misc/ImageWrapper.astro"; import ImageWrapper from "../components/misc/ImageWrapper.astro";
import {BANNER_HEIGHT, BANNER_HEIGHT_EXTEND, MAIN_PANEL_OVERLAPS_BANNER_HEIGHT} from "../constants/constants"; import TOC from "../components/widget/TOC.astro";
import { siteConfig } from "../config";
import {
BANNER_HEIGHT,
BANNER_HEIGHT_EXTEND,
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT,
} from "../constants/constants";
import Layout from "./Layout.astro";
interface Props { interface Props {
title?: string title?: string;
banner?: string banner?: string;
description?: string description?: string;
lang?: string lang?: string;
setOGTypeArticle?: boolean; setOGTypeArticle?: boolean;
headings? : MarkdownHeading[] headings?: MarkdownHeading[];
} }
const { title, banner, description, lang, setOGTypeArticle, headings = [] } = Astro.props const {
title,
banner,
description,
lang,
setOGTypeArticle,
headings = [],
} = Astro.props;
const hasBannerCredit = const hasBannerCredit =
siteConfig.banner.enable && siteConfig.banner.credit.enable siteConfig.banner.enable && siteConfig.banner.credit.enable;
const hasBannerLink = !!siteConfig.banner.credit.url const hasBannerLink = !!siteConfig.banner.credit.url;
const mainPanelTop = siteConfig.banner.enable ? `calc(${BANNER_HEIGHT}vh - ${MAIN_PANEL_OVERLAPS_BANNER_HEIGHT}rem)` : "5.5rem" const mainPanelTop = siteConfig.banner.enable
? `calc(${BANNER_HEIGHT}vh - ${MAIN_PANEL_OVERLAPS_BANNER_HEIGHT}rem)`
: "5.5rem";
--- ---
<Layout title={title} banner={banner} description={description} lang={lang} setOGTypeArticle={setOGTypeArticle}> <Layout title={title} banner={banner} description={description} lang={lang} setOGTypeArticle={setOGTypeArticle}>

View File

@@ -1,20 +1,20 @@
--- ---
import type { GetStaticPaths } from 'astro' import type { GetStaticPaths } from "astro";
import PostPage from '../components/PostPage.astro' import PostPage from "../components/PostPage.astro";
import Pagination from '../components/control/Pagination.astro' import Pagination from "../components/control/Pagination.astro";
import { PAGE_SIZE } from '../constants/constants' import { PAGE_SIZE } from "../constants/constants";
import MainGridLayout from '../layouts/MainGridLayout.astro' import MainGridLayout from "../layouts/MainGridLayout.astro";
import { getSortedPosts } from '../utils/content-utils' import { getSortedPosts } from "../utils/content-utils";
export const getStaticPaths = (async ({ paginate }) => { export const getStaticPaths = (async ({ paginate }) => {
const allBlogPosts = await getSortedPosts() const allBlogPosts = await getSortedPosts();
return paginate(allBlogPosts, { pageSize: PAGE_SIZE }) return paginate(allBlogPosts, { pageSize: PAGE_SIZE });
}) satisfies GetStaticPaths }) satisfies GetStaticPaths;
// https://github.com/withastro/astro/issues/6507#issuecomment-1489916992 // https://github.com/withastro/astro/issues/6507#issuecomment-1489916992
const { page } = Astro.props const { page } = Astro.props;
const len = page.data.length const len = page.data.length;
--- ---
<MainGridLayout> <MainGridLayout>

View File

@@ -1,20 +1,20 @@
--- ---
import MainGridLayout from '../layouts/MainGridLayout.astro' import MainGridLayout from "../layouts/MainGridLayout.astro";
import { getEntry } from 'astro:content' import { getEntry } from "astro:content";
import { i18n } from '../i18n/translation' import { render } from "astro:content";
import I18nKey from '../i18n/i18nKey' import Markdown from "@components/misc/Markdown.astro";
import Markdown from '@components/misc/Markdown.astro' import I18nKey from "../i18n/i18nKey";
import { render } from 'astro:content' import { i18n } from "../i18n/translation";
const aboutPost = await getEntry('spec', 'about') const aboutPost = await getEntry("spec", "about");
if (!aboutPost) { if (!aboutPost) {
throw new Error("About page content not found"); throw new Error("About page content not found");
} }
const { Content } = await render(aboutPost) const { Content } = await render(aboutPost);
--- ---
<MainGridLayout title={i18n(I18nKey.about)} description={i18n(I18nKey.about)}> <MainGridLayout title={i18n(I18nKey.about)} description={i18n(I18nKey.about)}>
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32"> <div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32">

View File

@@ -1,22 +1,22 @@
--- ---
import { getCategoryList } from '@utils/content-utils' import ArchivePanel from "@components/ArchivePanel.astro";
import MainGridLayout from '@layouts/MainGridLayout.astro' import I18nKey from "@i18n/i18nKey";
import ArchivePanel from '@components/ArchivePanel.astro' import { i18n } from "@i18n/translation";
import { i18n } from '@i18n/translation' import MainGridLayout from "@layouts/MainGridLayout.astro";
import I18nKey from '@i18n/i18nKey' import { getCategoryList } from "@utils/content-utils";
export async function getStaticPaths() { export async function getStaticPaths() {
const categories = await getCategoryList() const categories = await getCategoryList();
return categories.map(category => { return categories.map((category) => {
return { return {
params: { params: {
category: category.name, category: category.name,
}, },
} };
}) });
} }
const category = Astro.params.category as string const category = Astro.params.category as string;
--- ---
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}> <MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>

View File

@@ -1,9 +1,9 @@
--- ---
import MainGridLayout from '@layouts/MainGridLayout.astro' import ArchivePanel from "@components/ArchivePanel.astro";
import ArchivePanel from '@components/ArchivePanel.astro' import { UNCATEGORIZED } from "@constants/constants";
import { i18n } from '@i18n/translation' import I18nKey from "@i18n/i18nKey";
import I18nKey from '@i18n/i18nKey' import { i18n } from "@i18n/translation";
import { UNCATEGORIZED } from '@constants/constants' import MainGridLayout from "@layouts/MainGridLayout.astro";
--- ---
<MainGridLayout title={i18n(I18nKey.archive)}> <MainGridLayout title={i18n(I18nKey.archive)}>

View File

@@ -1,8 +1,8 @@
--- ---
import MainGridLayout from '@layouts/MainGridLayout.astro' import ArchivePanel from "@components/ArchivePanel.astro";
import ArchivePanel from '@components/ArchivePanel.astro' import I18nKey from "@i18n/i18nKey";
import { i18n } from '@i18n/translation' import { i18n } from "@i18n/translation";
import I18nKey from '@i18n/i18nKey' import MainGridLayout from "@layouts/MainGridLayout.astro";
--- ---
<MainGridLayout title={i18n(I18nKey.archive)}> <MainGridLayout title={i18n(I18nKey.archive)}>

View File

@@ -1,30 +1,30 @@
--- ---
import ArchivePanel from '@components/ArchivePanel.astro' import ArchivePanel from "@components/ArchivePanel.astro";
import I18nKey from '@i18n/i18nKey' import I18nKey from "@i18n/i18nKey";
import { i18n } from '@i18n/translation' import { i18n } from "@i18n/translation";
import MainGridLayout from '@layouts/MainGridLayout.astro' import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from '@utils/content-utils' import { getSortedPosts } from "@utils/content-utils";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getSortedPosts() const posts = await getSortedPosts();
// タグを集めるための Set の型を指定 // タグを集めるための Set の型を指定
const allTags = posts.reduce<Set<string>>((acc, post) => { const allTags = posts.reduce<Set<string>>((acc, post) => {
// biome-ignore lint/complexity/noForEach: <explanation> // biome-ignore lint/complexity/noForEach: <explanation>
post.data.tags.forEach(tag => acc.add(tag)) post.data.tags.forEach((tag) => acc.add(tag));
return acc return acc;
}, new Set()) }, new Set());
const allTagsArray = Array.from(allTags) const allTagsArray = Array.from(allTags);
return allTagsArray.map(tag => ({ return allTagsArray.map((tag) => ({
params: { params: {
tag: tag, tag: tag,
}, },
})) }));
} }
const tag = Astro.params.tag as string const tag = Astro.params.tag as string;
--- ---
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}> <MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>

View File

@@ -1,47 +1,49 @@
--- ---
import path from 'node:path' import path from "node:path";
import License from '@components/misc/License.astro' import License from "@components/misc/License.astro";
import Markdown from '@components/misc/Markdown.astro' import Markdown from "@components/misc/Markdown.astro";
import I18nKey from '@i18n/i18nKey' import I18nKey from "@i18n/i18nKey";
import { i18n } from '@i18n/translation' import { i18n } from "@i18n/translation";
import MainGridLayout from '@layouts/MainGridLayout.astro' import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getDir, getPostUrlBySlug } from '@utils/url-utils' import { getSortedPosts } from "@utils/content-utils";
import { Icon } from 'astro-icon/components' import { getDir, getPostUrlBySlug } from "@utils/url-utils";
import { licenseConfig } from 'src/config' import { Icon } from "astro-icon/components";
import PostMetadata from '../../components/PostMeta.astro' import { licenseConfig } from "src/config";
import ImageWrapper from '../../components/misc/ImageWrapper.astro' import PostMetadata from "../../components/PostMeta.astro";
import { profileConfig, siteConfig } from '../../config' import ImageWrapper from "../../components/misc/ImageWrapper.astro";
import { formatDateToYYYYMMDD } from '../../utils/date-utils' import { profileConfig, siteConfig } from "../../config";
import { getSortedPosts } from '@utils/content-utils' import { formatDateToYYYYMMDD } from "../../utils/date-utils";
export async function getStaticPaths() { export async function getStaticPaths() {
const blogEntries = await getSortedPosts(); const blogEntries = await getSortedPosts();
return blogEntries.map(entry => ({ return blogEntries.map((entry) => ({
params: { slug: entry.slug }, params: { slug: entry.slug },
props: { entry }, props: { entry },
})) }));
} }
const { entry } = Astro.props const { entry } = Astro.props;
const { Content, headings } = await entry.render() const { Content, headings } = await entry.render();
const { remarkPluginFrontmatter } = await entry.render() const { remarkPluginFrontmatter } = await entry.render();
const jsonLd = { const jsonLd = {
'@context': 'https://schema.org', "@context": "https://schema.org",
'@type': 'BlogPosting', "@type": "BlogPosting",
headline: entry.data.title, headline: entry.data.title,
description: entry.data.description || entry.data.title, description: entry.data.description || entry.data.title,
keywords: entry.data.tags, keywords: entry.data.tags,
author: { author: {
'@type': 'Person', "@type": "Person",
name: profileConfig.name, name: profileConfig.name,
url: Astro.site, url: Astro.site,
}, },
datePublished: formatDateToYYYYMMDD(entry.data.published), datePublished: formatDateToYYYYMMDD(entry.data.published),
inLanguage: (entry.data.lang ? entry.data.lang.replace('_', '-') : siteConfig.lang.replace('_', '-')), inLanguage: entry.data.lang
// TODO include cover image here ? entry.data.lang.replace("_", "-")
} : siteConfig.lang.replace("_", "-"),
// TODO include cover image here
};
--- ---
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}> <MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}>
<script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script> <script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>

View File

@@ -1,16 +1,16 @@
import type { APIRoute } from 'astro' import type { APIRoute } from "astro";
const robotsTxt = ` const robotsTxt = `
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href} Sitemap: ${new URL("sitemap-index.xml", import.meta.env.SITE).href}
`.trim() `.trim();
export const GET: APIRoute = () => { export const GET: APIRoute = () => {
return new Response(robotsTxt, { return new Response(robotsTxt, {
headers: { headers: {
'Content-Type': 'text/plain; charset=utf-8', "Content-Type": "text/plain; charset=utf-8",
}, },
}) });
} };

View File

@@ -1,33 +1,33 @@
import { siteConfig } from '@/config' import { siteConfig } from "@/config";
import rss from '@astrojs/rss' import rss from "@astrojs/rss";
import { getSortedPosts } from '@utils/content-utils' import { getSortedPosts } from "@utils/content-utils";
import type { APIContext } from 'astro' import type { APIContext } from "astro";
import MarkdownIt from 'markdown-it' import MarkdownIt from "markdown-it";
import sanitizeHtml from 'sanitize-html' import sanitizeHtml from "sanitize-html";
const parser = new MarkdownIt() const parser = new MarkdownIt();
export async function GET(context: APIContext) { export async function GET(context: APIContext) {
const blog = await getSortedPosts() const blog = await getSortedPosts();
return rss({ return rss({
title: siteConfig.title, title: siteConfig.title,
description: siteConfig.subtitle || 'No description', description: siteConfig.subtitle || "No description",
site: context.site ?? 'https://fuwari.vercel.app', site: context.site ?? "https://fuwari.vercel.app",
items: blog.map(post => { items: blog.map((post) => {
const content = const content =
typeof post.body === 'string' ? post.body : String(post.body || '') typeof post.body === "string" ? post.body : String(post.body || "");
return { return {
title: post.data.title, title: post.data.title,
pubDate: post.data.published, pubDate: post.data.published,
description: post.data.description || '', description: post.data.description || "",
link: `/posts/${post.slug}/`, link: `/posts/${post.slug}/`,
content: sanitizeHtml(parser.render(content), { content: sanitizeHtml(parser.render(content), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
}), }),
} };
}), }),
customData: `<language>${siteConfig.lang}</language>`, customData: `<language>${siteConfig.lang}</language>`,
}) });
} }

View File

@@ -1,5 +1,5 @@
/// <reference types="mdast" /> /// <reference types="mdast" />
import { h } from 'hastscript' import { h } from "hastscript";
/** /**
* Creates an admonition component. * Creates an admonition component.
@@ -11,23 +11,23 @@ import { h } from 'hastscript'
* @returns {import('mdast').Parent} The created admonition component. * @returns {import('mdast').Parent} The created admonition component.
*/ */
export function AdmonitionComponent(properties, children, type) { export function AdmonitionComponent(properties, children, type) {
if (!Array.isArray(children) || children.length === 0) if (!Array.isArray(children) || children.length === 0)
return h( return h(
'div', "div",
{ class: 'hidden' }, { class: "hidden" },
'Invalid admonition directive. (Admonition directives must be of block type ":::note{name="name"} <content> :::")', 'Invalid admonition directive. (Admonition directives must be of block type ":::note{name="name"} <content> :::")',
) );
let label = null let label = null;
if (properties?.['has-directive-label']) { if (properties?.["has-directive-label"]) {
label = children[0] // The first child is the label label = children[0]; // The first child is the label
// biome-ignore lint/style/noParameterAssign: <explanation> // biome-ignore lint/style/noParameterAssign: <explanation>
children = children.slice(1) children = children.slice(1);
label.tagName = 'div' // Change the tag <p> to <div> label.tagName = "div"; // Change the tag <p> to <div>
} }
return h("blockquote", { class: `admonition bdm-${type}` }, [ return h("blockquote", { class: `admonition bdm-${type}` }, [
h('span', { class: "bdm-title" }, label ? label : type.toUpperCase()), h("span", { class: "bdm-title" }, label ? label : type.toUpperCase()),
...children, ...children,
]) ]);
} }

View File

@@ -1,5 +1,5 @@
/// <reference types="mdast" /> /// <reference types="mdast" />
import { h } from 'hastscript' import { h } from "hastscript";
/** /**
* Creates a GitHub Card component. * Creates a GitHub Card component.
@@ -10,54 +10,54 @@ import { h } from 'hastscript'
* @returns {import('mdast').Parent} The created GitHub Card component. * @returns {import('mdast').Parent} The created GitHub Card component.
*/ */
export function GithubCardComponent(properties, children) { export function GithubCardComponent(properties, children) {
if (Array.isArray(children) && children.length !== 0) if (Array.isArray(children) && children.length !== 0)
return h('div', { class: 'hidden' }, [ return h("div", { class: "hidden" }, [
'Invalid directive. ("github" directive must be leaf type "::github{repo="owner/repo"}")', 'Invalid directive. ("github" directive must be leaf type "::github{repo="owner/repo"}")',
]) ]);
if (!properties.repo || !properties.repo.includes('/')) if (!properties.repo || !properties.repo.includes("/"))
return h( return h(
'div', "div",
{ class: 'hidden' }, { class: "hidden" },
'Invalid repository. ("repo" attributte must be in the format "owner/repo")', 'Invalid repository. ("repo" attributte must be in the format "owner/repo")',
) );
const repo = properties.repo const repo = properties.repo;
const cardUuid = `GC${Math.random().toString(36).slice(-6)}` // Collisions are not important const cardUuid = `GC${Math.random().toString(36).slice(-6)}`; // Collisions are not important
const nAvatar = h(`div#${cardUuid}-avatar`, { class: 'gc-avatar' }) const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
const nLanguage = h( const nLanguage = h(
`span#${cardUuid}-language`, `span#${cardUuid}-language`,
{ class: 'gc-language' }, { class: "gc-language" },
'Waiting...', "Waiting...",
) );
const nTitle = h("div", { class: 'gc-titlebar' }, [ const nTitle = h("div", { class: "gc-titlebar" }, [
h('div', { class: 'gc-titlebar-left' }, [ h("div", { class: "gc-titlebar-left" }, [
h('div', { class: 'gc-owner' }, [ h("div", { class: "gc-owner" }, [
nAvatar, nAvatar,
h('div', { class: 'gc-user' }, repo.split('/')[0]), h("div", { class: "gc-user" }, repo.split("/")[0]),
]), ]),
h('div', { class: 'gc-divider' }, '/'), h("div", { class: "gc-divider" }, "/"),
h('div', { class: 'gc-repo' }, repo.split('/')[1]), h("div", { class: "gc-repo" }, repo.split("/")[1]),
]), ]),
h('div', { class: 'github-logo' }), h("div", { class: "github-logo" }),
]) ]);
const nDescription = h( const nDescription = h(
`div#${cardUuid}-description`, `div#${cardUuid}-description`,
{ class: 'gc-description' }, { class: "gc-description" },
'Waiting for api.github.com...', "Waiting for api.github.com...",
) );
const nStars = h(`div#${cardUuid}-stars`, { class: 'gc-stars' }, '00K') const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
const nForks = h(`div#${cardUuid}-forks`, { class: 'gc-forks' }, '0K') const nForks = h(`div#${cardUuid}-forks`, { class: "gc-forks" }, "0K");
const nLicense = h(`div#${cardUuid}-license`, { class: 'gc-license' }, '0K') const nLicense = h(`div#${cardUuid}-license`, { class: "gc-license" }, "0K");
const nScript = h( const nScript = h(
`script#${cardUuid}-script`, `script#${cardUuid}-script`,
{ type: 'text/javascript', defer: true }, { type: "text/javascript", defer: true },
` `
fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => { fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => {
if (data.description) { if (data.description) {
document.getElementById('${cardUuid}-description').innerText = data.description.replace(/:[a-zA-Z0-9_]+:/g, ''); document.getElementById('${cardUuid}-description').innerText = data.description.replace(/:[a-zA-Z0-9_]+:/g, '');
@@ -83,21 +83,21 @@ export function GithubCardComponent(properties, children) {
console.warn("[GITHUB-CARD] (Error) Loading card for ${repo} | ${cardUuid}.") console.warn("[GITHUB-CARD] (Error) Loading card for ${repo} | ${cardUuid}.")
}) })
`, `,
) );
return h( return h(
`a#${cardUuid}-card`, `a#${cardUuid}-card`,
{ {
class: 'card-github fetch-waiting no-styling', class: "card-github fetch-waiting no-styling",
href: `https://github.com/${repo}`, href: `https://github.com/${repo}`,
target: '_blank', target: "_blank",
repo, repo,
}, },
[ [
nTitle, nTitle,
nDescription, nDescription,
h('div', { class: 'gc-infobar' }, [nStars, nForks, nLicense, nLanguage]), h("div", { class: "gc-infobar" }, [nStars, nForks, nLicense, nLanguage]),
nScript, nScript,
], ],
) );
} }

View File

@@ -1,30 +1,30 @@
import { h } from 'hastscript' import { h } from "hastscript";
import { visit } from 'unist-util-visit' import { visit } from "unist-util-visit";
export function parseDirectiveNode() { export function parseDirectiveNode() {
return (tree, { data }) => { return (tree, { data }) => {
visit(tree, node => { visit(tree, (node) => {
if ( if (
node.type === 'containerDirective' || node.type === "containerDirective" ||
node.type === 'leafDirective' || node.type === "leafDirective" ||
node.type === 'textDirective' node.type === "textDirective"
) { ) {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation> // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const data = node.data || (node.data = {}) const data = node.data || (node.data = {});
node.attributes = node.attributes || {} node.attributes = node.attributes || {};
if ( if (
node.children.length > 0 && node.children.length > 0 &&
node.children[0].data && node.children[0].data &&
node.children[0].data.directiveLabel node.children[0].data.directiveLabel
) { ) {
// Add a flag to the node to indicate that it has a directive label // Add a flag to the node to indicate that it has a directive label
node.attributes['has-directive-label'] = true node.attributes["has-directive-label"] = true;
} }
const hast = h(node.name, node.attributes) const hast = h(node.name, node.attributes);
data.hName = hast.tagName data.hName = hast.tagName;
data.hProperties = hast.properties data.hProperties = hast.properties;
} }
}) });
} };
} }

View File

@@ -1,17 +1,17 @@
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation> // biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
import { toString } from 'mdast-util-to-string' import { toString } from "mdast-util-to-string";
/* Use the post's first paragraph as the excerpt */ /* Use the post's first paragraph as the excerpt */
export function remarkExcerpt() { export function remarkExcerpt() {
return (tree, { data }) => { return (tree, { data }) => {
let excerpt = '' let excerpt = "";
for (const node of tree.children) { for (const node of tree.children) {
if (node.type !== 'paragraph') { if (node.type !== "paragraph") {
continue continue;
} }
excerpt = toString(node) excerpt = toString(node);
break break;
} }
data.astro.frontmatter.excerpt = excerpt data.astro.frontmatter.excerpt = excerpt;
} };
} }

View File

@@ -1,15 +1,15 @@
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation> // biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
import { toString } from 'mdast-util-to-string' import { toString } from "mdast-util-to-string";
import getReadingTime from 'reading-time' import getReadingTime from "reading-time";
export function remarkReadingTime() { export function remarkReadingTime() {
return (tree, { data }) => { return (tree, { data }) => {
const textOnPage = toString(tree) const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage) const readingTime = getReadingTime(textOnPage);
data.astro.frontmatter.minutes = Math.max( data.astro.frontmatter.minutes = Math.max(
1, 1,
Math.round(readingTime.minutes), Math.round(readingTime.minutes),
) );
data.astro.frontmatter.words = readingTime.words data.astro.frontmatter.words = readingTime.words;
} };
} }

View File

@@ -1,88 +1,88 @@
import type { AUTO_MODE, DARK_MODE, LIGHT_MODE } from '@constants/constants' import type { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants";
export type SiteConfig = { export type SiteConfig = {
title: string title: string;
subtitle: string subtitle: string;
lang: string lang: string;
themeColor: { themeColor: {
hue: number hue: number;
fixed: boolean fixed: boolean;
} };
banner: { banner: {
enable: boolean enable: boolean;
src: string src: string;
position?: 'top' | 'center' | 'bottom' position?: "top" | "center" | "bottom";
credit: { credit: {
enable: boolean enable: boolean;
text: string text: string;
url?: string url?: string;
} };
} };
toc: { toc: {
enable: boolean enable: boolean;
depth: 1 | 2 | 3 depth: 1 | 2 | 3;
} };
favicon: Favicon[] favicon: Favicon[];
} };
export type Favicon = { export type Favicon = {
src: string src: string;
theme?: 'light' | 'dark' theme?: "light" | "dark";
sizes?: string sizes?: string;
} };
export enum LinkPreset { export enum LinkPreset {
Home = 0, Home = 0,
Archive = 1, Archive = 1,
About = 2, About = 2,
} }
export type NavBarLink = { export type NavBarLink = {
name: string name: string;
url: string url: string;
external?: boolean external?: boolean;
} };
export type NavBarConfig = { export type NavBarConfig = {
links: (NavBarLink | LinkPreset)[] links: (NavBarLink | LinkPreset)[];
} };
export type ProfileConfig = { export type ProfileConfig = {
avatar?: string avatar?: string;
name: string name: string;
bio?: string bio?: string;
links: { links: {
name: string name: string;
url: string url: string;
icon: string icon: string;
}[] }[];
} };
export type LicenseConfig = { export type LicenseConfig = {
enable: boolean enable: boolean;
name: string name: string;
url: string url: string;
} };
export type LIGHT_DARK_MODE = export type LIGHT_DARK_MODE =
| typeof LIGHT_MODE | typeof LIGHT_MODE
| typeof DARK_MODE | typeof DARK_MODE
| typeof AUTO_MODE | typeof AUTO_MODE;
export type BlogPostData = { export type BlogPostData = {
body: string body: string;
title: string title: string;
published: Date published: Date;
description: string description: string;
tags: string[] tags: string[];
draft?: boolean draft?: boolean;
image?: string image?: string;
category?: string category?: string;
prevTitle?: string prevTitle?: string;
prevSlug?: string prevSlug?: string;
nextTitle?: string nextTitle?: string;
nextSlug?: string nextSlug?: string;
} };

View File

@@ -1,86 +1,84 @@
import { getCollection } from 'astro:content' import { getCollection } from "astro:content";
import I18nKey from '@i18n/i18nKey' import I18nKey from "@i18n/i18nKey";
import { i18n } from '@i18n/translation' import { i18n } from "@i18n/translation";
export async function getSortedPosts() { export async function getSortedPosts() {
const allBlogPosts = (await getCollection('posts', ({ data }) => { const allBlogPosts = await getCollection("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true return import.meta.env.PROD ? data.draft !== true : true;
})) });
const sorted = allBlogPosts.sort( const sorted = allBlogPosts.sort((a, b) => {
(a, b) => { const dateA = new Date(a.data.published);
const dateA = new Date(a.data.published) const dateB = new Date(b.data.published);
const dateB = new Date(b.data.published) return dateA > dateB ? -1 : 1;
return dateA > dateB ? -1 : 1 });
},
)
for (let i = 1; i < sorted.length; i++) { for (let i = 1; i < sorted.length; i++) {
sorted[i].data.nextSlug = sorted[i - 1].slug sorted[i].data.nextSlug = sorted[i - 1].slug;
sorted[i].data.nextTitle = sorted[i - 1].data.title sorted[i].data.nextTitle = sorted[i - 1].data.title;
} }
for (let i = 0; i < sorted.length - 1; i++) { for (let i = 0; i < sorted.length - 1; i++) {
sorted[i].data.prevSlug = sorted[i + 1].slug sorted[i].data.prevSlug = sorted[i + 1].slug;
sorted[i].data.prevTitle = sorted[i + 1].data.title sorted[i].data.prevTitle = sorted[i + 1].data.title;
} }
return sorted return sorted;
} }
export type Tag = { export type Tag = {
name: string name: string;
count: number count: number;
} };
export async function getTagList(): Promise<Tag[]> { export async function getTagList(): Promise<Tag[]> {
const allBlogPosts = await getCollection<'posts'>('posts', ({ data }) => { const allBlogPosts = await getCollection<"posts">("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true return import.meta.env.PROD ? data.draft !== true : true;
}) });
const countMap: { [key: string]: number } = {} const countMap: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { tags: string[] } }) => { allBlogPosts.map((post: { data: { tags: string[] } }) => {
post.data.tags.map((tag: string) => { post.data.tags.map((tag: string) => {
if (!countMap[tag]) countMap[tag] = 0 if (!countMap[tag]) countMap[tag] = 0;
countMap[tag]++ countMap[tag]++;
}) });
}) });
// sort tags // sort tags
const keys: string[] = Object.keys(countMap).sort((a, b) => { const keys: string[] = Object.keys(countMap).sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase()) return a.toLowerCase().localeCompare(b.toLowerCase());
}) });
return keys.map(key => ({ name: key, count: countMap[key] })) return keys.map((key) => ({ name: key, count: countMap[key] }));
} }
export type Category = { export type Category = {
name: string name: string;
count: number count: number;
} };
export async function getCategoryList(): Promise<Category[]> { export async function getCategoryList(): Promise<Category[]> {
const allBlogPosts = await getCollection<'posts'>('posts', ({ data }) => { const allBlogPosts = await getCollection<"posts">("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true return import.meta.env.PROD ? data.draft !== true : true;
}) });
const count: { [key: string]: number } = {} const count: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { category: string | number } }) => { allBlogPosts.map((post: { data: { category: string | number } }) => {
if (!post.data.category) { if (!post.data.category) {
const ucKey = i18n(I18nKey.uncategorized) const ucKey = i18n(I18nKey.uncategorized);
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1 count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1;
return return;
} }
count[post.data.category] = count[post.data.category] count[post.data.category] = count[post.data.category]
? count[post.data.category] + 1 ? count[post.data.category] + 1
: 1 : 1;
}) });
const lst = Object.keys(count).sort((a, b) => { const lst = Object.keys(count).sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase()) return a.toLowerCase().localeCompare(b.toLowerCase());
}) });
const ret: Category[] = [] const ret: Category[] = [];
for (const c of lst) { for (const c of lst) {
ret.push({ name: c, count: count[c] }) ret.push({ name: c, count: count[c] });
} }
return ret return ret;
} }

View File

@@ -1,3 +1,3 @@
export function formatDateToYYYYMMDD(date: Date): string { export function formatDateToYYYYMMDD(date: Date): string {
return date.toISOString().substring(0, 10) return date.toISOString().substring(0, 10);
} }

View File

@@ -1,54 +1,54 @@
import type { LIGHT_DARK_MODE } from '@/types/config' import type { LIGHT_DARK_MODE } from "@/types/config";
import { import {
AUTO_MODE, AUTO_MODE,
DARK_MODE, DARK_MODE,
DEFAULT_THEME, DEFAULT_THEME,
LIGHT_MODE, LIGHT_MODE,
} from '@constants/constants.ts' } from "@constants/constants.ts";
export function getDefaultHue(): number { export function getDefaultHue(): number {
const fallback = '250' const fallback = "250";
const configCarrier = document.getElementById('config-carrier') const configCarrier = document.getElementById("config-carrier");
return Number.parseInt(configCarrier?.dataset.hue || fallback) return Number.parseInt(configCarrier?.dataset.hue || fallback);
} }
export function getHue(): number { export function getHue(): number {
const stored = localStorage.getItem('hue') const stored = localStorage.getItem("hue");
return stored ? Number.parseInt(stored) : getDefaultHue() return stored ? Number.parseInt(stored) : getDefaultHue();
} }
export function setHue(hue: number): void { export function setHue(hue: number): void {
localStorage.setItem('hue', String(hue)) localStorage.setItem("hue", String(hue));
const r = document.querySelector(':root') as HTMLElement const r = document.querySelector(":root") as HTMLElement;
if (!r) { if (!r) {
return return;
} }
r.style.setProperty('--hue', String(hue)) r.style.setProperty("--hue", String(hue));
} }
export function applyThemeToDocument(theme: LIGHT_DARK_MODE) { export function applyThemeToDocument(theme: LIGHT_DARK_MODE) {
switch (theme) { switch (theme) {
case LIGHT_MODE: case LIGHT_MODE:
document.documentElement.classList.remove('dark') document.documentElement.classList.remove("dark");
break break;
case DARK_MODE: case DARK_MODE:
document.documentElement.classList.add('dark') document.documentElement.classList.add("dark");
break break;
case AUTO_MODE: case AUTO_MODE:
if (window.matchMedia('(prefers-color-scheme: dark)').matches) { if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add('dark') document.documentElement.classList.add("dark");
} else { } else {
document.documentElement.classList.remove('dark') document.documentElement.classList.remove("dark");
} }
break break;
} }
} }
export function setTheme(theme: LIGHT_DARK_MODE): void { export function setTheme(theme: LIGHT_DARK_MODE): void {
localStorage.setItem('theme', theme) localStorage.setItem("theme", theme);
applyThemeToDocument(theme) applyThemeToDocument(theme);
} }
export function getStoredTheme(): LIGHT_DARK_MODE { export function getStoredTheme(): LIGHT_DARK_MODE {
return (localStorage.getItem('theme') as LIGHT_DARK_MODE) || DEFAULT_THEME return (localStorage.getItem("theme") as LIGHT_DARK_MODE) || DEFAULT_THEME;
} }

View File

@@ -1,35 +1,35 @@
import i18nKey from '@i18n/i18nKey' import i18nKey from "@i18n/i18nKey";
import { i18n } from '@i18n/translation' import { i18n } from "@i18n/translation";
export function pathsEqual(path1: string, path2: string) { export function pathsEqual(path1: string, path2: string) {
const normalizedPath1 = path1.replace(/^\/|\/$/g, '').toLowerCase() const normalizedPath1 = path1.replace(/^\/|\/$/g, "").toLowerCase();
const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase() const normalizedPath2 = path2.replace(/^\/|\/$/g, "").toLowerCase();
return normalizedPath1 === normalizedPath2 return normalizedPath1 === normalizedPath2;
} }
function joinUrl(...parts: string[]): string { function joinUrl(...parts: string[]): string {
const joined = parts.join('/') const joined = parts.join("/");
return joined.replace(/\/+/g, '/') return joined.replace(/\/+/g, "/");
} }
export function getPostUrlBySlug(slug: string): string { export function getPostUrlBySlug(slug: string): string {
return url(`/posts/${slug}/`) return url(`/posts/${slug}/`);
} }
export function getCategoryUrl(category: string): string { export function getCategoryUrl(category: string): string {
if (category === i18n(i18nKey.uncategorized)) if (category === i18n(i18nKey.uncategorized))
return url('/archive/category/uncategorized/') return url("/archive/category/uncategorized/");
return url(`/archive/category/${category}/`) return url(`/archive/category/${category}/`);
} }
export function getDir(path: string): string { export function getDir(path: string): string {
const lastSlashIndex = path.lastIndexOf('/') const lastSlashIndex = path.lastIndexOf("/");
if (lastSlashIndex < 0) { if (lastSlashIndex < 0) {
return '/' return "/";
} }
return path.substring(0, lastSlashIndex + 1) return path.substring(0, lastSlashIndex + 1);
} }
export function url(path: string) { export function url(path: string) {
return joinUrl('', import.meta.env.BASE_URL, path) return joinUrl("", import.meta.env.BASE_URL, path);
} }