refactor: code cleanup

This commit is contained in:
saicaca 2024-02-18 18:13:43 +08:00 committed by saica.go
parent 3cd21c2da9
commit af29b9160f
51 changed files with 872 additions and 2264 deletions

View File

@ -1 +0,0 @@
{}

View File

@ -1,14 +0,0 @@
{
"taxonomy": {
"tags": [
"Blogging",
"Customization",
"Demo",
"Example",
"Fuwari",
"Markdown",
"Video"
],
"categories": []
}
}

View File

@ -1,9 +1,6 @@
# Fuwari # Fuwari
> [!WARNING] Fuwari is a static blog template built with [Astro](https://astro.build), a refactored version of [hexo-theme-vivia](https://github.com/saicaca/hexo-theme-vivia).
> This project is still very unfinished and the code is quite messy. Features may be changed or removed in the future.
Fuwari (not the final name maybe) is a static blog template built with [Astro](https://astro.build), a refactored version of [hexo-theme-vivia](https://github.com/saicaca/hexo-theme-vivia).
[**🖥Live Demo (Vercel)**](https://fuwari.vercel.app) [**🖥Live Demo (Vercel)**](https://fuwari.vercel.app)

View File

@ -1,5 +1,4 @@
import tailwind from "@astrojs/tailwind" import tailwind from "@astrojs/tailwind"
import yaml from "@rollup/plugin-yaml"
import Compress from "astro-compress" import Compress from "astro-compress"
import icon from "astro-icon" import icon from "astro-icon"
import { defineConfig } from "astro/config" import { defineConfig } from "astro/config"
@ -9,7 +8,7 @@ import rehypeKatex from "rehype-katex"
import rehypeSlug from "rehype-slug" import rehypeSlug from "rehype-slug"
import remarkMath from "remark-math" import remarkMath from "remark-math"
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs" import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
import vue from "@astrojs/vue" import svelte from "@astrojs/svelte"
const oklchToHex = (str) => { const oklchToHex = (str) => {
const DEFAULT_HUE = 250 const DEFAULT_HUE = 250
@ -38,7 +37,7 @@ export default defineConfig({
Compress({ Compress({
Image: false, Image: false,
}), }),
vue() svelte(),
], ],
markdown: { markdown: {
remarkPlugins: [remarkMath, remarkReadingTime], remarkPlugins: [remarkMath, remarkReadingTime],
@ -71,7 +70,6 @@ export default defineConfig({
], ],
}, },
vite: { vite: {
plugins: [yaml()],
css: { css: {
preprocessorOptions: { preprocessorOptions: {
stylus: { stylus: {

View File

@ -14,11 +14,11 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.3.4", "@astrojs/check": "^0.3.4",
"@astrojs/svelte": "^5.0.3",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@astrojs/vue": "^4.0.8",
"@fontsource-variable/jetbrains-mono": "^5.0.19", "@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"astro": "^4.1.1", "astro": "^4.4.0",
"astro-icon": "1.0.2", "astro-icon": "1.0.2",
"colorjs.io": "^0.4.5", "colorjs.io": "^0.4.5",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
@ -30,10 +30,9 @@
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"svelte": "^4.2.9",
"tailwindcss": "^3.3.7", "tailwindcss": "^3.3.7",
"typescript": "^5.2.2", "typescript": "^5.2.2"
"valine": "^1.5.1",
"vue": "^3.4.15"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/ts-plugin": "^1.3.1", "@astrojs/ts-plugin": "^1.3.1",

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,10 +1,12 @@
/* This is a script to create a new post markdown file with front-matter */
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
function getDate() { function getDate() {
const today = new Date() const today = new Date()
const year = today.getFullYear() const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, "0") //月份从0开始所以要加1 const month = String(today.getMonth() + 1).padStart(2, "0")
const day = String(today.getDate()).padStart(2, "0") const day = String(today.getDate()).padStart(2, "0")
return `${year}-${month}-${day}` return `${year}-${month}-${day}`

View File

@ -1,4 +1,10 @@
--- ---
import {getSortedPosts} from "../utils/content-utils";
import {getPostUrlBySlug} from "../utils/url-utils";
import {i18n} from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
import {UNCATEGORIZED} from "@constants/constants";
interface Props { interface Props {
keyword: string; keyword: string;
tags: string[]; tags: string[];
@ -6,13 +12,6 @@ interface Props {
} }
const { keyword, tags, categories} = Astro.props; const { keyword, tags, categories} = Astro.props;
import Button from "./control/Button.astro";
import {getSortedPosts} from "../utils/content-utils";
import {getPostUrlBySlug} from "../utils/url-utils";
import {i18n} from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
import {UNCATEGORIZED} from "@constants/constants";
let posts = await getSortedPosts() let posts = await getSortedPosts()
if (Array.isArray(tags) && tags.length > 0) { if (Array.isArray(tags) && tags.length > 0) {
@ -66,18 +65,22 @@ function formatTag(tag: string[]) {
groups.map(group => ( groups.map(group => (
<div> <div>
<div class="flex flex-row w-full items-center h-[3.75rem]"> <div class="flex flex-row w-full items-center h-[3.75rem]">
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-black/75 dark:text-white/75">{group.year}</div> <div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">{group.year}</div>
<div class="w-[15%] md:w-[10%]"> <div class="w-[15%] md:w-[10%]">
<div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div> <div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div>
</div> </div>
<div class="w-[70%] md:w-[80%] transition text-left text-black/50 dark:text-white/50">{group.posts.length} {i18n(I18nKey.postsCount)}</div> <div class="w-[70%] md:w-[80%] transition text-left text-50">{group.posts.length} {i18n(I18nKey.postsCount)}</div>
</div> </div>
{group.posts.map(post => ( {group.posts.map(post => (
<a href={getPostUrlBySlug(post.slug)} aria-label={post.data.title} class="group"> <a href={getPostUrlBySlug(post.slug)}
<Button light height="40px" class="w-full rounded-lg hover:text-[initial]"> aria-label={post.data.title}
class="group btn-plain block h-10 w-full rounded-lg hover:text-[initial]"
>
<div class="flex flex-row justify-start items-center h-full"> <div class="flex flex-row justify-start items-center h-full">
<!-- date --> <!-- date -->
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-black/50 dark:text-white/50">{formatDate(post.data.published)}</div> <div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<!-- dot and line --> <!-- dot and line -->
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center"> <div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
<div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5 <div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
@ -92,17 +95,16 @@ function formatTag(tag: string[]) {
<!-- post title --> <!-- post title -->
<div class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold <div class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)] group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
text-black/80 dark:text-white/80 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden" text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
> >
{post.data.title} {post.data.title}
</div> </div>
<!-- tag list --> <!-- tag list -->
<div class="hidden md:block md:w-[15%] text-left text-sm transition <div class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden whitespace-nowrap overflow-ellipsis overflow-hidden
text-black/30 dark:text-white/30" text-30"
>{formatTag(post.data.tags)}</div> >{formatTag(post.data.tags)}</div>
</div> </div>
</Button>
</a> </a>
))} ))}
</div> </div>

View File

@ -1,6 +0,0 @@
---
---
<div class="rounded-2xl drop-shadow-2xl bg-white">
<slot />
</div>

View File

@ -1,7 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import ButtonLight from "./control/Button.astro";
---
<ButtonLight class="fill-black">
<Icon name="material-symbols:nightlight-badge-outline" class="w-6 h-6"/>
</ButtonLight>

View File

@ -1,60 +0,0 @@
---
interface Props {
title: string;
body: string;
href: string;
}
const { href, title, body } = Astro.props;
---
<li class="link-card">
<a href={href}>
<h2>
{title}
<span class="">&rarr;</span>
</h2>
<p>
{body}
</p>
</a>
</li>
<style>
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card > a {
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}
</style>

View File

@ -5,7 +5,7 @@ import {profileConfig} from "../config";
--- ---
<div class="card-base max-w-[var(--page-width)] min-h-[4.5rem] rounded-b-none mx-auto flex items-center px-6"> <div class="card-base max-w-[var(--page-width)] min-h-[4.5rem] rounded-b-none mx-auto flex items-center px-6">
<div class="text-black/50 dark:text-white/50 text-sm"> <div class="text-50 text-sm">
© 2023 {profileConfig.name}. All Rights Reserved. © 2023 {profileConfig.name}. All Rights Reserved.
<br> <br>
Powered by <a class="link text-[var(--primary)]" target="_blank" href="https://github.com/saicaca/fuwari">Fuwari</a> Powered by <a class="link text-[var(--primary)]" target="_blank" href="https://github.com/saicaca/fuwari">Fuwari</a>

View File

@ -199,11 +199,36 @@ color_set({
@apply bg-transparent text-[var(--primary)] @apply bg-transparent text-[var(--primary)]
} }
.text-deep { .btn-card {
@apply transition flex items-center justify-center bg-[var(--card-bg)] hover:bg-[var(--btn-card-bg-hover)]
active:bg-[var(--btn-card-bg-active)]
}
.btn-card.disabled {
@apply pointer-events-none text-black/10 dark:text-white/10
}
.btn-plain {
@apply transition flex items-center justify-center bg-none hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]
text-black/75 hover:text-[var(--primary)] dark:text-white/75 dark:hover:text-[var(--primary)]
}
.btn-regular {
@apply transition flex items-center justify-center bg-[var(--btn-regular-bg)] hover:bg-[var(--btn-regular-bg-hover)] active:bg-[var(--btn-regular-bg-active)]
text-[var(--btn-content)] dark:text-white/75
}
.text-90 {
@apply text-black/90 dark:text-white/90 @apply text-black/90 dark:text-white/90
} }
.text-sub { .text-75 {
@apply text-black/75 dark:text-white/75
}
.text-50 {
@apply text-black/50 dark:text-white/50 @apply text-black/50 dark:text-white/50
} }
.text-30 {
@apply text-black/30 dark:text-white/30
}
.text-25 {
@apply text-black/25 dark:text-white/25
}
} }
</style> </style>

View File

@ -1,85 +1,65 @@
--- ---
import Button from "./control/Button.astro";
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import DisplaySetting from "./widget/DisplaySetting.astro"; import DisplaySettings from "./widget/DisplaySettings.svelte";
import I18nKey from "../i18n/i18nKey";
import {i18n} from "../i18n/translation";
import {LinkPreset, NavBarLink} from "../types/config"; import {LinkPreset, NavBarLink} from "../types/config";
import {navBarConfig, siteConfig} from "../config"; import {navBarConfig, siteConfig} from "../config";
import NavMenuPanel from "./widget/NavMenuPanel.astro"; import NavMenuPanel from "./widget/NavMenuPanel.astro";
import SearchPanel from "./SearchPanel.vue" import Search from "./Search.svelte";
import {LinkPresets} from "../constants/link-presets";
const className = Astro.props.class; const className = Astro.props.class;
function isI18nKey(key: string): key is I18nKey { let links: NavBarLink[] = navBarConfig.links.map((item: NavBarLink | LinkPreset): NavBarLink => {
return Object.values(I18nKey).includes(key);
}
let links: NavBarLink[] = navBarConfig.links.map((item) => {
if (typeof item === "number") { if (typeof item === "number") {
return getLinkPresetInfo(item) return LinkPresets[item]
} }
return item; return item;
}); });
function getLinkPresetInfo(p: LinkPreset): NavBarLink {
switch (p) {
case LinkPreset.Home:
return {
name: i18n(I18nKey.home),
url: "/page/1"
};
case LinkPreset.Archive:
return {
name: i18n(I18nKey.archive),
url: "/archive"
};
case LinkPreset.About:
return {
name: i18n(I18nKey.about),
url: "/about"
};
}
}
--- ---
<div transition:animate="none" class:list={[ <div transition:animate="none" class:list={[
className, className,
"card-base sticky top-0 overflow-visible max-w-[var(--page-width)] h-[4.5rem] rounded-t-none mx-auto flex items-center justify-between px-4"]}> "card-base sticky top-0 overflow-visible max-w-[var(--page-width)] h-[4.5rem] rounded-t-none mx-auto flex items-center justify-between px-4"]}>
<a href="/page/1"><Button height="3.25rem" class="px-5 font-bold rounded-lg active:scale-95" light> <a href="/page/1" class="btn-plain h-[3.25rem] px-5 font-bold rounded-lg active:scale-95">
<div class="flex flex-row text-[var(--primary)] items-center text-md"> <div class="flex flex-row text-[var(--primary)] items-center text-md">
<Icon name="material-symbols:home-outline-rounded" size={"1.75rem"} class="mb-1 mr-2" /> <Icon name="material-symbols:home-outline-rounded" size={"1.75rem"} class="mb-1 mr-2" />
{siteConfig.title} {siteConfig.title}
</div> </div>
</Button></a> </a>
<div class="hidden md:block"> <div class="hidden md:flex">
{links.map((l) => { {links.map((l) => {
return <a aria-label={l.name} href={l.url} target={l.external ? "_blank" : null}> return <a aria-label={l.name} href={l.url} target={l.external ? "_blank" : null}
<Button light class="font-bold px-5 rounded-lg active:scale-95"> class="btn-plain h-11 font-bold px-5 rounded-lg active:scale-95"
>
<div class="flex items-center"> <div class="flex items-center">
{l.name} {l.name}
{l.external && <Icon size="14" name="fa6-solid:arrow-up-right-from-square" class="transition -translate-y-[1px] ml-1 text-black/[0.2] dark:text-white/[0.2]"></Icon>} {l.external && <Icon size="14" name="fa6-solid:arrow-up-right-from-square" class="transition -translate-y-[1px] ml-1 text-black/[0.2] dark:text-white/[0.2]"></Icon>}
</div> </div>
</Button>
</a>; </a>;
})} })}
</div> </div>
<div class="flex"> <div class="flex">
<SearchPanel client:load> <!--<SearchPanel client:load>-->
<Icon slot="search-icon" name="material-symbols:search" size={"1.25rem"} class="ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon> <Search client:load>
<Icon slot="search-icon" name="material-symbols:search" size={"1.25rem"} class="absolute pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<!--<Icon slot="arrow-icon" name="material-symbols:chevron-right-rounded" size={"1.25rem"} class="transition my-auto text-[var(&#45;&#45;primary)]"></Icon>--> <!--<Icon slot="arrow-icon" name="material-symbols:chevron-right-rounded" size={"1.25rem"} class="transition my-auto text-[var(&#45;&#45;primary)]"></Icon>-->
<Icon slot="arrow-icon" name="fa6-solid:chevron-right" size={"0.75rem"} class="transition translate-x-0.5 my-auto text-[var(--primary)]"></Icon> <Icon slot="arrow-icon" name="fa6-solid:chevron-right" size={"0.75rem"} class="transition translate-x-0.5 my-auto text-[var(--primary)]"></Icon>
<Button slot="search-switch" name="Search Panel" class="block lg:hidden rounded-lg active:scale-90" id="search-switch" iconName="material-symbols:search" iconSize={"1.25rem"} isIcon light></Button> <Icon slot="search-switch" name="material-symbols:search" size={"1.25rem"}></Icon>
</SearchPanel> </Search>
<Button name="Display Settings" class="rounded-lg active:scale-90" id="display-settings-switch" iconName="material-symbols:palette-outline" iconSize={"1.25rem"} isIcon light></Button> <button aria-label="Display Settings" class="btn-plain h-11 w-11 rounded-lg active:scale-90" id="display-settings-switch">
<Button name="Light/Dark Mode" class="rounded-lg flex items-center justify-center active:scale-90" id="scheme-switch" light height="2.75rem" width="2.75rem"> <Icon name="material-symbols:palette-outline" size={"1.25rem"}></Icon>
</button>
<button aria-label="Light/Dark Mode" class="btn-plain h-11 w-11 rounded-lg active:scale-90" id="scheme-switch">
<Icon name="material-symbols:wb-sunny-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-light-icon)]"></Icon> <Icon name="material-symbols:wb-sunny-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-light-icon)]"></Icon>
<Icon name="material-symbols:dark-mode-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-dark-icon)]"></Icon> <Icon name="material-symbols:dark-mode-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-dark-icon)]"></Icon>
</Button> </button>
<Button name="Nav Menu" class="rounded-lg active:scale-90 block md:hidden" id="nav-menu-switch" iconName="material-symbols:menu-rounded" iconSize={"1.25rem"} isIcon light></Button> <button name="Nav Menu" class="btn-plain w-11 h-11 rounded-lg active:scale-90 md:hidden" id="nav-menu-switch">
<Icon name="material-symbols:menu-rounded" size={"1.25rem"}></Icon>
</button>
</div> </div>
<DisplaySetting></DisplaySetting>
<NavMenuPanel links={links}></NavMenuPanel> <NavMenuPanel links={links}></NavMenuPanel>
<DisplaySettings client:only="svelte">
<Icon slot="restore-icon" name="fa6-solid:arrow-rotate-left" size={"0.875rem"} class=""></Icon>
</DisplaySettings>
</div> </div>
<style lang="stylus"> <style lang="stylus">
@ -125,12 +105,13 @@ document.addEventListener('astro:after-swap', () => {
{import.meta.env.PROD && <script is:raw> {import.meta.env.PROD && <script is:raw>
async function loadPagefind() { async function loadPagefind() {
const pagefind = await import("/pagefind/pagefind.js") const pagefind = await import('/pagefind/pagefind.js')
await pagefind.options({ await pagefind.options({
"excerptLength": 20 'excerptLength': 20
}) })
pagefind.init() pagefind.init()
window.pagefind = pagefind window.pagefind = pagefind
pagefind.search('') // speed up the first search
} }
loadPagefind() loadPagefind()
</script>} </script>}

View File

@ -1,6 +1,12 @@
--- ---
import path from "path"; import path from "path";
import PostMetadata from "./PostMetadata.astro"; import PostMetadata from "./PostMeta.astro";
import ImageWrapper from "./misc/ImageWrapper.astro";
import { Icon } from 'astro-icon/components';
import {i18n} from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
import {getDir} from "../utils/url-utils";
interface Props { interface Props {
class: string; class: string;
entry: any; entry: any;
@ -16,14 +22,6 @@ interface Props {
} }
const { entry, title, url, published, tags, category, image, description, words } = Astro.props; const { entry, title, url, published, tags, category, image, description, words } = Astro.props;
const className = Astro.props.class; const className = Astro.props.class;
// console.log(Astro.props);
import ImageBox from "./misc/ImageBox.astro";
import ButtonTag from "./control/ButtonTag.astro";
import { Icon } from 'astro-icon/components';
import Button from "./control/Button.astro";
import {i18n} from "../i18n/translation";
import I18nKey from "../i18n/i18nKey";
import {getDir} from "../utils/url-utils";
const hasCover = image !== undefined && image !== null && image !== ''; const hasCover = image !== undefined && image !== null && image !== '';
@ -35,8 +33,7 @@ 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]}> <div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]}>
<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}]}>
<a href={url} <a href={url}
class="transition w-full block font-bold mb-3 text-3xl class="transition w-full block font-bold mb-3 text-3xl text-90
text-black/90 dark:text-white/90
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
active:text-[var(--title-active)] dark:active:text-[var(--title-active)] active:text-[var(--title-active)] dark:active:text-[var(--title-active)]
before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)] before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
@ -48,10 +45,12 @@ const { remarkPluginFrontmatter } = await entry.render();
<!-- metadata --> <!-- metadata -->
<PostMetadata published={published} tags={tags} category={category} hideTagsForMobile={true} class:list={{"mb-4": description, "mb-6": !description}}></PostMetadata> <PostMetadata published={published} tags={tags} category={category} hideTagsForMobile={true} class:list={{"mb-4": description, "mb-6": !description}}></PostMetadata>
<div class="transition text-black/75 dark:text-white/75 mb-3.5"> <!-- description -->
<div class="transition text-75 mb-3.5">
{ description } { description }
</div> </div>
<!-- word count and read time -->
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition"> <div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
<div>{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div> <div>{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
<div>|</div> <div>|</div>
@ -71,26 +70,23 @@ const { remarkPluginFrontmatter } = await entry.render();
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl"> class="transition opacity-0 group-hover:opacity-100 text-white text-5xl">
</Icon> </Icon>
</div> </div>
<ImageBox src={image} basePath={path.join("content/posts/", getDir(entry.id))} alt="Cover Image of the Post" <ImageWrapper src={image} basePath={path.join("content/posts/", getDir(entry.id))} alt="Cover Image of the Post"
class="w-full h-full"> class="w-full h-full">
</ImageBox> </ImageWrapper>
</a>} </a>}
{!hasCover && {!hasCover &&
<a href={url} aria-label={title} class="hidden md:block"> <a href={url} aria-label={title} class="hidden md:flex btn-regular w-[3.25rem]
<Button name="Enter the Post" width="3.25rem" height="full" class="absolute right-3 top-3 bottom-3 rounded-xl bg-[var(--enter-btn-bg)] hover:bg-[var(--enter-btn-bg-hover)] active:bg-[var(--enter-btn-bg-active)] active:scale-95"> absolute right-3 top-3 bottom-3 rounded-xl bg-[var(--enter-btn-bg)]
hover:bg-[var(--enter-btn-bg-hover)] active:bg-[var(--enter-btn-bg-active)] active:scale-95
">
<Icon name="material-symbols:chevron-right-rounded" <Icon name="material-symbols:chevron-right-rounded"
class="transition text-[var(--primary)] text-4xl mx-auto"> class="transition text-[var(--primary)] text-4xl mx-auto">
</Icon> </Icon>
</Button>
</a> </a>
} }
</div> </div>
<div class="transition border-t-[1px] border-dashed mx-6 border-black/10 dark:border-white/[0.15] last:border-t-0 md:hidden"></div> <div class="transition border-t-[1px] border-dashed mx-6 border-black/10 dark:border-white/[0.15] last:border-t-0 md:hidden"></div>
<style lang="stylus" define:vars={{coverWidth}}> <style lang="stylus" define:vars={{coverWidth}}>
:root
--btn-enter-bg oklch(0.98 0.005 var(--hue))
--btn-enter-bg-dark oklch(0.2 0.02 var(--hue))
</style> </style>

View File

@ -22,7 +22,7 @@ const className = Astro.props.class;
> >
<Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon> <Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon>
</div> </div>
<span class="text-black/50 dark:text-white/50 text-sm font-medium">{formatDateToYYYYMMDD(published)}</span> <span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(published)}</span>
</div> </div>
<!-- categories --> <!-- categories -->
@ -33,7 +33,7 @@ const className = Astro.props.class;
</div> </div>
<div class="flex flex-row flex-nowrap"> <div class="flex flex-row flex-nowrap">
<div><a href=`/archive/category/${category || 'uncategorized'}` aria-label=`View all posts in the ${category} category` <div><a href=`/archive/category/${category || 'uncategorized'}` aria-label=`View all posts in the ${category} category`
class="link-lg transition text-black/50 dark:text-white/50 text-sm font-medium class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap"> hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{category || i18n(I18nKey.uncategorized)} {category || i18n(I18nKey.uncategorized)}
</a></div> </a></div>
@ -51,12 +51,12 @@ const className = Astro.props.class;
class="with-divider" class="with-divider"
> >
<a href=`/archive/tag/${tag}` aria-label=`View all posts with the ${tag} tag` <a href=`/archive/tag/${tag}` aria-label=`View all posts with the ${tag} tag`
class="link-lg transition text-black/50 dark:text-white/50 text-sm font-medium class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap"> hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{tag} {tag}
</a> </a>
</div>)} </div>)}
{!(tags && tags.length > 0) && <div class="transition text-black/50 dark:text-white/50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>} {!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}
</div> </div>
</div> </div>
</div> </div>
@ -70,7 +70,7 @@ const className = Astro.props.class;
text-[var(--btn-content)] mr-2 text-[var(--btn-content)] mr-2
} }
.with-divider { .with-divider {
@apply before:content-['/'] before:ml-1.5 before:mr-0.5 before:text-[var(--meta-divider)] before:text-sm @apply before:content-['/'] before:ml-1.5 before:mr-1.5 before:text-[var(--meta-divider)] before:text-sm
before:font-medium before:first-of-type:hidden before:transition before:font-medium before:first-of-type:hidden before:transition
} }
} }

View File

@ -0,0 +1,23 @@
---
import {getPostUrlBySlug} from "@utils/url-utils";
import PostCard from "./PostCard.astro";
const {page} = Astro.props;
---
<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; }; slug: string; }) => {
return (
<PostCard
entry={entry}
title={entry.data.title}
tags={entry.data.tags}
category={entry.data.category}
published={entry.data.published}
url={getPostUrlBySlug(entry.slug)}
image={entry.data.image}
description={entry.data.description}
draft={entry.data.draft}
></PostCard>
);
})}
</div>

View File

@ -1,83 +0,0 @@
---
import {formatDateToYYYYMMDD} from "../utils/date-utils";
interface Props {
title: string;
url: string;
published: Date;
tags: string[];
image: string;
description: string;
words: number;
}
const { title, url, published, tags, image, description, words } = Astro.props;
// console.log(Astro.props);
import ImageBox from "./misc/ImageBox.astro";
import ButtonTag from "./control/ButtonTag.astro";
import { Icon } from 'astro-icon/components';
// tags = ['Foo', 'Bar', 'Baz', 'Qux', 'Quux'];
// const cover = 'https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg';
// cover = null;
const hasCover = image !== undefined && image !== null && image !== '';
---
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative">
<div class:list={["card-base z-30 px-8 py-6 relative ",
{
'w-[calc(70%_+_var(--radius-large))]': hasCover,
'w-[calc(100%_-_76px_+_var(--radius-large))]': !hasCover,
}
]}>
<a href={url}
class="transition w-full block font-bold mb-1 text-3xl
text-neutral-900 dark:text-neutral-100
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
before:absolute before:top-8 before:left-4
">
This is a very long title
</a>
<div class="flex text-neutral-500 dark:text-neutral-400 items-center mb-1">
<div>{formatDateToYYYYMMDD(published)}</div>
<div class="transition h-1 w-1 rounded-sm bg-neutral-400 dark:bg-neutral-600 mx-3"></div>
<div>Uncategorized</div>
<div class="transition h-1 w-1 rounded-sm bg-neutral-400 dark:bg-neutral-600 mx-3"></div>
<div>{words} words</div>
</div>
<div class="flex gap-2 mb-4">
{tags.map(t => (
<ButtonTag dot>{t}</ButtonTag>
))}
</div>
<div class="transition text-neutral-700 dark:text-neutral-300">This is the description of the article</div>
</div>
{!hasCover && <a href={url}
class="transition w-[72px]
bg-[var(--btn-enter-bg)] dark:bg-[var(--btn-enter-bg-dark)]
hover:bg-[var(--btn-card-bg-hover)] active:bg-[var(--btn-card-bg-active)]
absolute top-0 bottom-0 right-0 flex items-center">
<Icon name="material-symbols:chevron-right-rounded"
class="transition text-4xl text-[var(--primary)] ml-[22px]"></Icon>
</a>}
{hasCover && <a href={url}
class="group w-[30%] absolute top-0 bottom-0 right-0">
<div class="absolute z-10 w-full h-full group-hover:bg-black/30 group-active:bg-black/50 transition"></div>
<div class="absolute z-20 w-full h-full flex items-center justify-center ">
<Icon name="material-symbols:chevron-right-rounded"
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl"></Icon>
</div>
<ImageBox src="https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg"
class="w-full h-full">
</ImageBox>
</a>}
</div>
<style lang="stylus">
:root
--btn-enter-bg oklch(0.98 0.005 var(--hue))
--btn-enter-bg-dark oklch(0.2 0.02 var(--hue))
</style>

View File

@ -1,16 +0,0 @@
---
interface Props {
name: string;
}
const {name} = Astro.props;
import BasicCard from "./BasicCard.astro";
---
<BasicCard >
<div class="p-4">
<div>{name}</div>
</div>
</BasicCard>

View File

@ -1,7 +0,0 @@
---
---
<div class="card-base w-full h-[200px]">
</div>

View File

@ -1,51 +1,50 @@
--- ---
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import Button from "./Button.astro";
--- ---
<!-- 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` -->
<div class="back-to-top-wrapper hidden lg:block" transition:persist> <div class="back-to-top-wrapper hidden lg:block" transition:persist>
<div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="topFunction()"> <div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="backToTop()">
<Button name="Back to Top" card height="60px" width="60px"> <button aria-label="Back to Top" class="btn-card h-[3.75rem] w-[3.75rem]">
<Icon name="material-symbols:keyboard-arrow-up-rounded" class="mx-auto"></Icon> <Icon name="material-symbols:keyboard-arrow-up-rounded" class="mx-auto"></Icon>
</Button> </button>
</div> </div>
</div> </div>
<style lang="stylus"> <style lang="stylus">
.back-to-top-wrapper .back-to-top-wrapper
width: 60px width: 3.75rem
height: 60px height: 3.75rem
position: absolute position: absolute
right: 0 right: 0
top: 0 top: 0
.back-to-top-btn .back-to-top-btn
color: var(--primary) color: var(--primary)
font-size: 36px font-size: 2.25rem
font-weight: bold font-weight: bold
border: none border: none
position: fixed position: fixed
bottom: 240px bottom: 15rem
opacity: 1 opacity: 1
cursor: pointer cursor: pointer
transform: translateX(80px) transform: translateX(5rem)
i i
font-size: 28px font-size: 1.75rem
&.hide &.hide
transform: translateX(80px) scale(0.9) transform: translateX(5rem) scale(0.9)
opacity: 0 opacity: 0
pointer-events: none pointer-events: none
&:active &:active
transform: translateX(80px) scale(0.9) transform: translateX(5rem) scale(0.9)
</style> </style>
<script is:raw> <script is:raw>
function backToTop() {
function topFunction() {
window.scroll({ top: 0, behavior: 'smooth' }); window.scroll({ top: 0, behavior: 'smooth' });
} }
function scrollFunction() { function scrollFunction() {
let btn = document.getElementById('back-to-top-btn'); let btn = document.getElementById('back-to-top-btn');
if (document.body.scrollTop > 600 || document.documentElement.scrollTop > 600) { if (document.body.scrollTop > 600 || document.documentElement.scrollTop > 600) {
@ -54,7 +53,5 @@ function scrollFunction() {
btn.classList.add('hide') btn.classList.add('hide')
} }
} }
window.onscroll = function() { window.onscroll = scrollFunction
scrollFunction();
}
</script> </script>

View File

@ -1,76 +0,0 @@
---
interface Props {
id?: string;
name?: string;
isIcon?: boolean;
iconName?: string;
width?: string;
height?: string;
regular?: boolean;
light?: boolean
card?: boolean;
iconSize?: number,
class?: string
disabled?: boolean
}
const props = Astro.props;
const {
id,
name,
isIcon = false,
iconName,
width,
height = '2.75rem',
regular,
light,
iconSize = 24,
card,
disabled = false,
} = Astro.props;
const className = Astro.props.class;
import { Icon } from 'astro-icon/components';
---
<button id={id}
disabled={disabled}
aria-label={name}
class:list={[
className,
`
transition
h-[var(--height)]
`,
{
'w-[var(--width)]': width,
'w-[var(--height)]': isIcon,
'bg-none': light,
'hover:bg-[var(--btn-plain-bg-hover)]': light,
'active:bg-[var(--btn-plain-bg-active)]': light,
'text-black/75': light,
'hover:text-[var(--primary)]': light,
'dark:text-white/75': light || regular,
'dark:hover:text-[var(--primary)]': light,
'bg-[var(--btn-regular-bg)]': regular,
'hover:bg-[var(--btn-regular-bg-hover)]': regular,
'active:bg-[var(--btn-regular-bg-active)]': regular,
'text-[var(--btn-content)]': regular,
'bg-[var(--card-bg)]': card,
'enabled:hover:bg-[var(--btn-card-bg-hover)]': card,
'enabled:active:bg-[var(--btn-card-bg-active)]': card,
'disabled:text-black/10': card,
'disabled:dark:text-white/10': card,
}
]}
>
{props.isIcon && <Icon class="mx-auto" name={props.iconName} size={iconSize}></Icon> }
<slot />
</button>
<style define:vars={{ height, width, iconSize }}>
</style>

View File

@ -1,5 +1,4 @@
--- ---
import Button from "./Button.astro";
interface Props { interface Props {
size?: string; size?: string;
dot?: boolean; dot?: boolean;
@ -8,9 +7,7 @@ interface Props {
} }
const { size, dot, href, label }: Props = Astro.props; const { size, dot, href, label }: Props = Astro.props;
--- ---
<a href={href} aria-label={label}> <a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg">
<Button regular height="2rem" class="text-sm px-3 flex flex-row items-center 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>}
<slot></slot> <slot></slot>
</Button>
</a> </a>

View File

@ -11,7 +11,6 @@ const {page} = Astro.props;
const HIDDEN = -1; const HIDDEN = -1;
const className = Astro.props.class; const className = Astro.props.class;
import Button from "./Button.astro";
const ADJ_DIST = 2; const ADJ_DIST = 2;
const VISIBLE = ADJ_DIST * 2 + 1; const VISIBLE = ADJ_DIST * 2 + 1;
@ -51,13 +50,22 @@ if (r < page.lastPage)
const parts: string[] = page.url.current.split('/'); const parts: string[] = page.url.current.split('/');
const commonUrl: string = parts.slice(0, -1).join('/') + '/'; const commonUrl: string = parts.slice(0, -1).join('/') + '/';
const getPageUrl = (p: number) => {
if (p == 1)
return commonUrl;
return commonUrl + p;
}
--- ---
<div class:list={[className, "flex flex-row gap-3 justify-center"]}> <div class:list={[className, "flex flex-row gap-3 justify-center"]}>
<a href={page.url.prev} aria-label={page.url.prev ? "Previous Page" : null}> <a href={page.url.prev} aria-label={page.url.prev ? "Previous Page" : null}
<Button isIcon card iconName="material-symbols:chevron-left-rounded" class:list={["text-[var(--primary)] rounded-lg", {"active:scale-90": page.url.prev != undefined}]} iconSize={28} class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
disabled = {page.url.prev == undefined} {"disabled": page.url.prev == undefined}
></Button> ]}
>
<Icon name="material-symbols:chevron-left-rounded" size="1.75rem"></Icon>
</a> </a>
<div class="bg-[var(--card-bg)] flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold"> <div class="bg-[var(--card-bg)] flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold">
{pages.map((p) => { {pages.map((p) => {
@ -69,16 +77,16 @@ const commonUrl: string = parts.slice(0, -1).join('/') + '/';
> >
{p} {p}
</div> </div>
return <a href={commonUrl + p} aria-label=`Page ${p}`> return <a href={getPageUrl(p)} aria-label=`Page ${p}`
<Button card iconName="material-symbols:chevron-left-rounded" class="rounded-lg active:scale-[0.85]" height="2.75rem" width="2.75rem"> class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
{p} >{p}</a>
</Button>
</a>
})} })}
</div> </div>
<a href={page.url.next} aria-label={page.url.next ? "Next Page" : null}> <a href={page.url.next} aria-label={page.url.next ? "Next Page" : null}
<Button isIcon card name="Next Page" iconName="material-symbols:chevron-right-rounded" class:list={["text-[var(--primary)] rounded-lg", {"active:scale-90": page.url.next != undefined}]} iconSize={28} class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
disabled = {page.url.next == undefined} {"disabled": page.url.next == undefined}
></Button> ]}
>
<Icon name="material-symbols:chevron-right-rounded" size="1.75rem"></Icon>
</a> </a>
</div> </div>

View File

@ -1,5 +1,10 @@
--- ---
import {formatDateToYYYYMMDD} from "../../utils/date-utils"; import {formatDateToYYYYMMDD} from "../../utils/date-utils";
import { Icon } from 'astro-icon/components';
import {licenseConfig, profileConfig} from "../../config";
import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey";
interface Props { interface Props {
title: string; title: string;
slug: string; slug: string;
@ -9,15 +14,8 @@ interface Props {
const { title, slug, pubDate } = Astro.props; const { title, slug, pubDate } = Astro.props;
const className = Astro.props.class; const className = Astro.props.class;
import { Icon } from 'astro-icon/components';
import {licenseConfig, profileConfig} from "../../config";
import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey";
const profileConf = profileConfig; const profileConf = profileConfig;
const licenseConf = licenseConfig; const licenseConf = licenseConfig;
const postUrl = decodeURIComponent(Astro.url.toString()); const postUrl = decodeURIComponent(Astro.url.toString());
--- ---

View File

@ -22,6 +22,12 @@ interface Props {
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}> <WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}>
{categories.map((c) => {categories.map((c) =>
<ButtonLink url={getCategoryUrl(c.name)} badge={c.count} label=`View all posts in the ${c.name} category`>{c.name}</ButtonLink> <ButtonLink
url={getCategoryUrl(c.name)}
badge={c.count}
label=`View all posts in the ${c.name} category`
>
{c.name}
</ButtonLink>
)} )}
</WidgetLayout> </WidgetLayout>

View File

@ -8,9 +8,6 @@ interface Props {
} }
const links = Astro.props.links; const links = Astro.props.links;
const enableBanner = siteConfig.banner.enable;
--- ---
<div id="nav-menu-panel" class:list={["float-panel closed absolute transition-all fixed right-4 px-2 py-2"]}> <div id="nav-menu-panel" class:list={["float-panel closed absolute transition-all fixed right-4 px-2 py-2"]}>
{links.map((link) => ( {links.map((link) => (

View File

@ -1,32 +1,29 @@
--- ---
import ImageBox from "../misc/ImageBox.astro"; import ImageWrapper from "../misc/ImageWrapper.astro";
import Button from "../control/Button.astro";
import {Icon} from "astro-icon/components"; import {Icon} from "astro-icon/components";
import {profileConfig} from "../../config"; import {profileConfig} from "../../config";
interface props {
}
const className = Astro.props
const config = profileConfig; const config = profileConfig;
--- ---
<div class="card-base"> <div class="card-base">
<a aria-label="Go to About Page" href="/about" class="group block relative mx-auto mt-4 lg:mx-3 lg:mt-3 mb-3 max-w-[240px] lg:max-w-none overflow-hidden rounded-xl active:scale-95"> <a aria-label="Go to About Page" href="/about"
<div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50 w-full h-full z-50 flex items-center justify-center"> class="group block relative mx-auto mt-4 lg:mx-3 lg:mt-3 mb-3
max-w-[240px] lg:max-w-none overflow-hidden rounded-xl active:scale-95">
<div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50
w-full h-full z-50 flex items-center justify-center">
<Icon name="fa6-regular:address-card" <Icon name="fa6-regular:address-card"
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl"> class="transition opacity-0 group-hover:opacity-100 text-white text-5xl">
</Icon> </Icon>
</div> </div>
<ImageBox src={config.avatar} alt="Profile Image of the Author" class="mx-auto lg:w-full h-full lg:mt-0 "></ImageBox> <ImageWrapper src={config.avatar} alt="Profile Image of the Author" class="mx-auto lg:w-full h-full lg:mt-0 "></ImageWrapper>
</a> </a>
<div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div> <div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div>
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div> <div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
<div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div> <div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div>
<div class="flex gap-2 mx-2 justify-center mb-4"> <div class="flex gap-2 mx-2 justify-center mb-4">
{config.links.map(item => {config.links.map(item =>
<a aria-label={item.name} href={item.url} target="_blank"> <a aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
<Button isIcon iconName={item.icon} regular height="40px" class="rounded-lg active:scale-90"></Button> <Icon name={item.icon} size="1.5rem"></Icon>
</a> </a>
)} )}
</div> </div>

View File

@ -1,20 +0,0 @@
---
import WidgetLayout from "./WidgetLayout.astro";
import ButtonLink from "../control/ButtonLink.astro";
import {getSortedPosts} from "../../utils/content-utils";
import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey";
import {getPostUrlBySlug} from "../../utils/url-utils";
let posts = await getSortedPosts()
const LIMIT = 3;
posts = posts.slice(0, LIMIT)
---
<WidgetLayout name={i18n(I18nKey.recentPosts)}>
{posts.map(post =>
<ButtonLink url={getPostUrlBySlug(post.slug)}>{post.data.title}</ButtonLink>
)}
</WidgetLayout>

View File

@ -1,6 +1,5 @@
--- ---
import Profile from "./Profile.astro"; import Profile from "./Profile.astro";
import RecentPost from "./RecentPost.astro";
import Tag from "./Tags.astro"; import Tag from "./Tags.astro";
import Categories from "./Categories.astro"; import Categories from "./Categories.astro";
@ -12,10 +11,6 @@ const className = Astro.props.class;
</div> </div>
<div class="flex flex-col w-full gap-4 top-4 sticky top-4" transition:animate="none"> <div class="flex flex-col w-full gap-4 top-4 sticky top-4" transition:animate="none">
<Categories></Categories> <Categories></Categories>
<!--<RecentPost></RecentPost>-->
<Tag></Tag> <Tag></Tag>
</div> </div>
</div> </div>
<style>
</style>

View File

@ -1,5 +1,4 @@
--- ---
import Button from "../control/Button.astro";
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import {i18n} from "../../i18n/translation"; import {i18n} from "../../i18n/translation";
import I18nKey from "../../i18n/i18nKey"; import I18nKey from "../../i18n/i18nKey";
@ -26,11 +25,11 @@ const {
<slot></slot> <slot></slot>
</div> </div>
{isCollapsed && <div class="expand-btn px-4 -mb-2"> {isCollapsed && <div class="expand-btn px-4 -mb-2">
<Button light class=" w-full rounded-lg" height="36px"> <button class="btn-plain w-full h-9 rounded-lg">
<div class="text-[var(--primary)] flex items-center justify-center gap-2 -translate-x-2"> <div class="text-[var(--primary)] flex items-center justify-center gap-2 -translate-x-2">
<Icon name="material-symbols:more-horiz" size={28}></Icon> {i18n(I18nKey.more)} <Icon name="material-symbols:more-horiz" size={28}></Icon> {i18n(I18nKey.more)}
</div> </div>
</Button> </button>
</div>} </div>}
</widget-layout> </widget-layout>

View File

@ -3,8 +3,8 @@ import type {
NavBarConfig, NavBarConfig,
ProfileConfig, ProfileConfig,
SiteConfig, SiteConfig,
} from './types/config.ts' } from './types/config'
import { LinkPreset } from './types/config.ts' import { LinkPreset } from './types/config'
export const siteConfig: SiteConfig = { export const siteConfig: SiteConfig = {
title: 'Fuwari', title: 'Fuwari',
@ -12,7 +12,7 @@ export const siteConfig: SiteConfig = {
lang: 'en', lang: 'en',
themeHue: 250, themeHue: 250,
banner: { banner: {
enable: true, enable: false,
src: 'assets/images/demo-banner.png', src: 'assets/images/demo-banner.png',
}, },
} }

View File

@ -1 +1,3 @@
export const UNCATEGORIZED = '__uncategorized__' export const UNCATEGORIZED = '__uncategorized__'
export const PAGE_SIZE = 8

View File

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

4
src/files.d.ts vendored
View File

@ -1,4 +0,0 @@
declare module '*.yml' {
const value: unknown
export default value
}

View File

@ -20,7 +20,7 @@ enum I18nKey {
postCount = 'postCount', postCount = 'postCount',
postsCount = 'postsCount', postsCount = 'postsCount',
primaryColor = 'primaryColor', themeColor = 'themeColor',
more = 'more', more = 'more',

View File

@ -1,5 +1,5 @@
import Key from '../i18nKey.ts' import Key from '../i18nKey'
import type { Translation } from '../translation.ts' import type { Translation } from '../translation'
export const en: Translation = { export const en: Translation = {
[Key.home]: 'Home', [Key.home]: 'Home',
@ -23,7 +23,7 @@ export const en: Translation = {
[Key.postCount]: 'post', [Key.postCount]: 'post',
[Key.postsCount]: 'posts', [Key.postsCount]: 'posts',
[Key.primaryColor]: 'Primary Color', [Key.themeColor]: 'Theme Color',
[Key.more]: 'More', [Key.more]: 'More',

View File

@ -1,5 +1,5 @@
import Key from '../i18nKey.ts' import Key from '../i18nKey'
import type { Translation } from '../translation.ts' import type { Translation } from '../translation'
export const ja: Translation = { export const ja: Translation = {
[Key.home]: 'Home', [Key.home]: 'Home',
@ -23,7 +23,7 @@ export const ja: Translation = {
[Key.postCount]: '件の投稿', [Key.postCount]: '件の投稿',
[Key.postsCount]: '件の投稿', [Key.postsCount]: '件の投稿',
[Key.primaryColor]: '原色', [Key.themeColor]: 'テーマカラー',
[Key.more]: 'もっと', [Key.more]: 'もっと',

View File

@ -1,5 +1,5 @@
import Key from '../i18nKey.ts' import Key from '../i18nKey'
import type { Translation } from '../translation.ts' import type { Translation } from '../translation'
export const zh_CN: Translation = { export const zh_CN: Translation = {
[Key.home]: '主页', [Key.home]: '主页',
@ -23,7 +23,7 @@ export const zh_CN: Translation = {
[Key.postCount]: '篇文章', [Key.postCount]: '篇文章',
[Key.postsCount]: '篇文章', [Key.postsCount]: '篇文章',
[Key.primaryColor]: '主题色', [Key.themeColor]: '主题色',
[Key.more]: '更多', [Key.more]: '更多',

View File

@ -1,5 +1,5 @@
import Key from '../i18nKey.ts' import Key from '../i18nKey'
import type { Translation } from '../translation.ts' import type { Translation } from '../translation'
export const zh_TW: Translation = { export const zh_TW: Translation = {
[Key.home]: '首頁', [Key.home]: '首頁',
@ -23,7 +23,7 @@ export const zh_TW: Translation = {
[Key.postCount]: '篇文章', [Key.postCount]: '篇文章',
[Key.postsCount]: '篇文章', [Key.postsCount]: '篇文章',
[Key.primaryColor]: '主題色', [Key.themeColor]: '主題色',
[Key.more]: '更多', [Key.more]: '更多',

View File

@ -1,9 +1,9 @@
import { siteConfig } from '../config.ts' import { siteConfig } from '../config'
import type I18nKey from './i18nKey.ts' import type I18nKey from './i18nKey'
import { en } from './languages/en.ts' import { en } from './languages/en'
import { ja } from './languages/ja.ts' import { ja } from './languages/ja'
import { zh_CN } from './languages/zh_CN.ts' import { zh_CN } from './languages/zh_CN'
import { zh_TW } from './languages/zh_TW.ts' import { zh_TW } from './languages/zh_TW'
export type Translation = { export type Translation = {
[K in I18nKey]: string [K in I18nKey]: string
@ -23,8 +23,7 @@ const map: { [key: string]: Translation } = {
} }
export function getTranslation(lang: string): Translation { export function getTranslation(lang: string): Translation {
lang = lang.toLowerCase() return map[lang.toLowerCase()] || defaultTranslation
return map[lang] || defaultTranslation
} }
export function i18n(key: I18nKey): string { export function i18n(key: I18nKey): string {

View File

@ -4,7 +4,7 @@ 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 { ViewTransitions } from 'astro:transitions'; import { ViewTransitions } from 'astro:transitions';
import ImageBox from "@components/misc/ImageBox.astro"; import ImageWrapper from "@components/misc/ImageWrapper.astro";
import { fade } from 'astro:transitions'; import { fade } from 'astro:transitions';
import {pathsEqual} from "@utils/url-utils"; import {pathsEqual} from "@utils/url-utils";
@ -95,10 +95,10 @@ if (title) {
class:list={{'banner-home': isHomePage, 'banner-else': !isHomePage}} class:list={{'banner-home': isHomePage, 'banner-else': !isHomePage}}
> >
<ImageBox id="boxtest" alt="Banner image of the blog" class:list={["object-center object-cover h-full", {"hidden": !siteConfig.banner.enable}]} <ImageWrapper id="boxtest" alt="Banner image of the blog" class:list={["object-center object-cover h-full", {"hidden": !siteConfig.banner.enable}]}
src={siteConfig.banner.src} transition:animate="fade" src={siteConfig.banner.src} transition:animate="fade"
> >
</ImageBox> </ImageWrapper>
</div> </div>
<slot /> <slot />
</GlobalStyles> </GlobalStyles>
@ -107,20 +107,7 @@ if (title) {
<style is:global> <style is:global>
:root { :root {
--hue: var(--configHue); --hue: var(--configHue);
--accent: 136, 58, 234; --page-width: 75rem;
--accent-light: 224, 204, 250;
--accent-dark: 49, 10, 101;
--accent-gradient: linear-gradient(45deg, rgb(var(--accent)), rgb(var(--accent-light)) 30%, white 60%);
--page-width: 1200px;
}
html {
background: #13151A;
background-size: 224px;
}
code {
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
} }
</style> </style>
<style> <style>
@ -163,6 +150,7 @@ import {
SizeObserverPlugin, SizeObserverPlugin,
ClickScrollPlugin ClickScrollPlugin
} from 'overlayscrollbars'; } from 'overlayscrollbars';
import {getHue, setHue} from "../utils/setting-utils";
/* Preload fonts */ /* Preload fonts */
// (async function() { // (async function() {
@ -206,41 +194,13 @@ function disableAnimation() {
} }
} }
function activateDisplaySettings() {
let output = document.getElementById("hueValue");
let slider = document.getElementById("colorSlider");
let configCarrier = document.getElementById("config-carrier");
output.innerHTML = slider.value; // Display the default slider value
let r = document.querySelector(':root');
function setHue(hue) {
localStorage.setItem('hue', hue);
output.innerHTML = hue;
slider.value = hue;
r.style.setProperty(`--hue`, hue);
}
let storedHue = localStorage.getItem('hue');
if (storedHue) {
setHue(storedHue);
} else {
setHue(configCarrier.dataset.hue);
}
slider.oninput = function() {
let hue = this.value;
output.innerHTML = this.value;
setHue(hue);
}
function setClickOutsideToClose(panel: string, ignores: string[]) { function setClickOutsideToClose(panel: string, ignores: string[]) {
document.addEventListener("click", event => { document.addEventListener("click", event => {
let panelDom = document.getElementById(panel); let panelDom = document.getElementById(panel);
let tDom = event.target; let tDom = event.target;
for (let ig of ignores) { for (let ig of ignores) {
let ie = document.getElementById(ig) let ie = document.getElementById(ig)
if (ie == tDom || ie.contains(tDom)) { if (ie == tDom || (ie?.contains(tDom))) {
return; return;
} }
} }
@ -250,7 +210,7 @@ function activateDisplaySettings() {
setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"]) setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"])
setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"]) setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"])
setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"]) setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"])
}
function loadTheme() { function loadTheme() {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && if (localStorage.theme === 'dark' || (!('theme' in localStorage) &&
@ -264,10 +224,7 @@ function loadTheme() {
} }
function loadHue() { function loadHue() {
const hue = localStorage.hue; setHue(getHue())
if (hue) {
document.documentElement.style.setProperty('--hue', hue);
}
} }
function setBannerHeight() { function setBannerHeight() {
@ -315,7 +272,6 @@ function init() {
setBannerHeight(); setBannerHeight();
loadTheme(); loadTheme();
loadHue(); loadHue();
activateDisplaySettings();
initCustomScrollbar(); initCustomScrollbar();
} }

View File

@ -5,7 +5,6 @@ import SideBar from "@components/widget/SideBar.astro";
import {pathsEqual} from "@utils/url-utils"; import {pathsEqual} from "@utils/url-utils";
import Footer from "@components/Footer.astro"; import Footer from "@components/Footer.astro";
import BackToTop from "@components/control/BackToTop.astro"; import BackToTop from "@components/control/BackToTop.astro";
import DisplaySetting from "@components/widget/DisplaySetting.astro";
import {siteConfig} from "@/config"; import {siteConfig} from "@/config";
interface Props { interface Props {
@ -13,17 +12,15 @@ interface Props {
banner?: string; banner?: string;
} }
const { title, banner } = Astro.props; const { title, banner } = Astro.props
const isHomePage = pathsEqual(Astro.url.pathname, '/') || pathsEqual(Astro.url.pathname, '/page/1')
const isHomePage = pathsEqual(Astro.url.pathname, '/') || pathsEqual(Astro.url.pathname, '/page/1'); const enableBanner = siteConfig.banner.enable
const enableBanner = siteConfig.banner.enable;
--- ---
<Layout title={title} banner={banner}> <Layout title={title} banner={banner}>
<div class=`max-w-[var(--page-width)] min-h-screen grid grid-cols-[280px_auto] grid-rows-[auto_auto_1fr_auto] lg:grid-rows-[auto_1fr_auto] <div class="max-w-[var(--page-width)] min-h-screen grid grid-cols-[17.5rem_auto] grid-rows-[auto_auto_1fr_auto] lg:grid-rows-[auto_1fr_auto]
mx-auto gap-4 relative px-0 md:px-4` mx-auto gap-4 relative px-0 md:px-4"
transition:animate="none" transition:animate="none"
> >
<div id="top-row" class="col-span-2 grid-rows-1 z-50" class:list={["transition-all", { <div id="top-row" class="col-span-2 grid-rows-1 z-50" class:list={["transition-all", {
@ -32,7 +29,7 @@ const enableBanner = siteConfig.banner.enable;
> >
<Navbar transition:animate="fade" transition:persist></Navbar> <Navbar transition:animate="fade" transition:persist></Navbar>
</div> </div>
<SideBar class="row-start-3 row-end-4 col-span-2 lg:row-start-2 lg:row-end-3 lg:col-span-1 lg:max-w-[280px] " transition:persist></SideBar> <SideBar class="row-start-3 row-end-4 col-span-2 lg:row-start-2 lg:row-end-3 lg:col-span-1 lg:max-w-[17.5rem]" transition:persist></SideBar>
<div class="row-start-2 row-end-3 col-span-2 lg:col-span-1 overflow-hidden" transition:animate="slide"> <div class="row-start-2 row-end-3 col-span-2 lg:col-span-1 overflow-hidden" transition:animate="slide">
<!-- the overflow-hidden here prevent long text break the layout--> <!-- the overflow-hidden here prevent long text break the layout-->

View File

@ -5,7 +5,6 @@ import ArchivePanel from "@components/ArchivePanel.astro";
import {i18n} from "@i18n/translation"; import {i18n} from "@i18n/translation";
import I18nKey from "@i18n/i18nKey"; import I18nKey from "@i18n/i18nKey";
export async function getStaticPaths() { export async function getStaticPaths() {
const categories = await getCategoryList(); const categories = await getCategoryList();
return categories.map(category => { return categories.map(category => {

View File

@ -1,3 +0,0 @@
---
---

View File

@ -1,10 +1,9 @@
--- ---
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import MainGridLayout from "@layouts/MainGridLayout.astro"; import MainGridLayout from "@layouts/MainGridLayout.astro";
import ImageBox from "@components/misc/ImageBox.astro"; import ImageWrapper from "../../components/misc/ImageWrapper.astro";
import {Icon} from "astro-icon/components"; import {Icon} from "astro-icon/components";
import PostMetadata from "@components/PostMetadata.astro"; import PostMetadata from "../../components/PostMeta.astro";
import Button from "@components/control/Button.astro";
import {i18n} from "@i18n/translation"; import {i18n} from "@i18n/translation";
import I18nKey from "@i18n/i18nKey"; import I18nKey from "@i18n/i18nKey";
import {getDir, getPostUrlBySlug} from "@utils/url-utils"; import {getDir, getPostUrlBySlug} from "@utils/url-utils";
@ -74,7 +73,7 @@ const { remarkPluginFrontmatter } = await entry.render();
<!-- always show cover as long as it has one --> <!-- always show cover as long as it has one -->
{entry.data.image && {entry.data.image &&
<ImageBox src={entry.data.image} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl"/> <ImageWrapper src={entry.data.image} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl"/>
} }
{!entry.data.image && <div class="border-[var(--line-divider)] border-dashed border-b-[1px] mb-5"></div>} {!entry.data.image && <div class="border-[var(--line-divider)] border-dashed border-b-[1px] mb-5"></div>}
@ -90,21 +89,21 @@ const { remarkPluginFrontmatter } = await entry.render();
<div class="flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full"> <div class="flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full">
<a href={getPostUrlBySlug(entry.data.nextSlug)} class="w-full font-bold overflow-hidden active:scale-95"> <a href={getPostUrlBySlug(entry.data.nextSlug)} class="w-full font-bold overflow-hidden active:scale-95">
{entry.data.nextSlug && <Button class="w-full max-w-full h-10 px-4 rounded-2xl flex items-center justify-start gap-4" card height="3.75rem"> {entry.data.nextSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center justify-start gap-4" >
<Icon name="material-symbols:chevron-left-rounded" size={32} class="text-[var(--primary)]" /> <Icon name="material-symbols:chevron-left-rounded" size={32} class="text-[var(--primary)]" />
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75"> <div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
{entry.data.nextTitle} {entry.data.nextTitle}
</div> </div>
</Button>} </div>}
</a> </a>
<a href={getPostUrlBySlug(entry.data.prevSlug)} class="w-full font-bold overflow-hidden active:scale-95"> <a href={getPostUrlBySlug(entry.data.prevSlug)} class="w-full font-bold overflow-hidden active:scale-95">
{entry.data.prevSlug && <Button class="w-full max-w-full h-10 px-4 rounded-2xl flex items-center justify-end gap-4" card height="3.75rem"> {entry.data.prevSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center justify-end gap-4">
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75"> <div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
{entry.data.prevTitle} {entry.data.prevTitle}
</div> </div>
<Icon name="material-symbols:chevron-right-rounded" size={32} class="text-[var(--primary)]" /> <Icon name="material-symbols:chevron-right-rounded" size={32} class="text-[var(--primary)]" />
</Button>} </div>}
</a> </a>
</div> </div>

View File

@ -1,11 +1,10 @@
import I18nKey from '@i18n/i18nKey'
import { i18n } from '@i18n/translation'
import { getCollection } from 'astro:content' import { getCollection } from 'astro:content'
import {UNCATEGORIZED} from "@constants/constants.ts";
import {i18n} from "@i18n/translation.ts";
import I18nKey from "@i18n/i18nKey.ts";
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((a, b) => { const sorted = allBlogPosts.sort((a, b) => {
const dateA = new Date(a.data.published) const dateA = new Date(a.data.published)
@ -32,7 +31,7 @@ export type Tag = {
export async function getTagList(): Promise<Tag[]> { export async function getTagList(): Promise<Tag[]> {
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 countMap: { [key: string]: number } = {} const countMap: { [key: string]: number } = {}
@ -58,16 +57,18 @@ export type Category = {
export async function getCategoryList(): Promise<Category[]> { export async function getCategoryList(): Promise<Category[]> {
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 count: { [key: string]: number } = {} const count: { [key: string]: number } = {}
allBlogPosts.map(post => { allBlogPosts.map(post => {
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] + 1 : 1 count[post.data.category] = count[post.data.category]
? count[post.data.category] + 1
: 1
}) })
const lst = Object.keys(count).sort((a, b) => { const lst = Object.keys(count).sort((a, b) => {

View File

@ -0,0 +1,19 @@
export function getDefaultHue(): number {
const fallback = '250'
const configCarrier = document.getElementById('config-carrier')
return parseInt(configCarrier?.dataset.hue || fallback)
}
export function getHue(): number {
const stored = localStorage.getItem('hue')
return stored ? parseInt(stored) : getDefaultHue()
}
export function setHue(hue: number): void {
localStorage.setItem('hue', String(hue))
const r = document.querySelector(':root')
if (!r) {
return
}
r.style.setProperty('--hue', hue)
}

View File

@ -1,5 +1,5 @@
import {i18n} from "@i18n/translation.ts"; import i18nKey from '@i18n/i18nKey'
import i18nKey from "@i18n/i18nKey.ts"; 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()
@ -18,8 +18,7 @@ export function getPostUrlBySlug(slug: string): string | null {
} }
export function getCategoryUrl(category: string): string | null { export function getCategoryUrl(category: string): string | null {
if (!category) if (!category) return null
return null
if (category === i18n(i18nKey.uncategorized)) if (category === i18n(i18nKey.uncategorized))
return '/archive/category/uncategorized' return '/archive/category/uncategorized'
return `/archive/category/${category}` return `/archive/category/${category}`