refactor: code cleanup
This commit is contained in:
parent
3cd21c2da9
commit
af29b9160f
|
@ -1 +0,0 @@
|
|||
{}
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"taxonomy": {
|
||||
"tags": [
|
||||
"Blogging",
|
||||
"Customization",
|
||||
"Demo",
|
||||
"Example",
|
||||
"Fuwari",
|
||||
"Markdown",
|
||||
"Video"
|
||||
],
|
||||
"categories": []
|
||||
}
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
# Fuwari
|
||||
|
||||
> [!WARNING]
|
||||
> 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).
|
||||
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).
|
||||
|
||||
[**🖥️Live Demo (Vercel)**](https://fuwari.vercel.app)
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import tailwind from "@astrojs/tailwind"
|
||||
import yaml from "@rollup/plugin-yaml"
|
||||
import Compress from "astro-compress"
|
||||
import icon from "astro-icon"
|
||||
import { defineConfig } from "astro/config"
|
||||
|
@ -9,7 +8,7 @@ import rehypeKatex from "rehype-katex"
|
|||
import rehypeSlug from "rehype-slug"
|
||||
import remarkMath from "remark-math"
|
||||
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
|
||||
import vue from "@astrojs/vue"
|
||||
import svelte from "@astrojs/svelte"
|
||||
|
||||
const oklchToHex = (str) => {
|
||||
const DEFAULT_HUE = 250
|
||||
|
@ -38,7 +37,7 @@ export default defineConfig({
|
|||
Compress({
|
||||
Image: false,
|
||||
}),
|
||||
vue()
|
||||
svelte(),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [remarkMath, remarkReadingTime],
|
||||
|
@ -71,7 +70,6 @@ export default defineConfig({
|
|||
],
|
||||
},
|
||||
vite: {
|
||||
plugins: [yaml()],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
stylus: {
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.3.4",
|
||||
"@astrojs/svelte": "^5.0.3",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@astrojs/vue": "^4.0.8",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"astro": "^4.1.1",
|
||||
"astro": "^4.4.0",
|
||||
"astro-icon": "1.0.2",
|
||||
"colorjs.io": "^0.4.5",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
|
@ -30,10 +30,9 @@
|
|||
"rehype-slug": "^6.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.32.6",
|
||||
"svelte": "^4.2.9",
|
||||
"tailwindcss": "^3.3.7",
|
||||
"typescript": "^5.2.2",
|
||||
"valine": "^1.5.1",
|
||||
"vue": "^3.4.15"
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/ts-plugin": "^1.3.1",
|
||||
|
|
2153
pnpm-lock.yaml
2153
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -1,10 +1,12 @@
|
|||
/* This is a script to create a new post markdown file with front-matter */
|
||||
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
function getDate() {
|
||||
const today = new Date()
|
||||
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")
|
||||
|
||||
return `${year}-${month}-${day}`
|
||||
|
|
|
@ -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 {
|
||||
keyword: string;
|
||||
tags: string[];
|
||||
|
@ -6,13 +12,6 @@ interface 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()
|
||||
|
||||
if (Array.isArray(tags) && tags.length > 0) {
|
||||
|
@ -66,18 +65,22 @@ function formatTag(tag: string[]) {
|
|||
groups.map(group => (
|
||||
<div>
|
||||
<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="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></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>
|
||||
{group.posts.map(post => (
|
||||
<a href={getPostUrlBySlug(post.slug)} aria-label={post.data.title} class="group">
|
||||
<Button light height="40px" class="w-full rounded-lg hover:text-[initial]">
|
||||
<a href={getPostUrlBySlug(post.slug)}
|
||||
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">
|
||||
<!-- 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 -->
|
||||
<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
|
||||
|
@ -92,17 +95,16 @@ function formatTag(tag: string[]) {
|
|||
<!-- post title -->
|
||||
<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)]
|
||||
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}
|
||||
</div>
|
||||
<!-- tag list -->
|
||||
<div class="hidden md:block md:w-[15%] text-left text-sm transition
|
||||
whitespace-nowrap overflow-ellipsis overflow-hidden
|
||||
text-black/30 dark:text-white/30"
|
||||
text-30"
|
||||
>{formatTag(post.data.tags)}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
|
||||
---
|
||||
<div class="rounded-2xl drop-shadow-2xl bg-white">
|
||||
<slot />
|
||||
</div>
|
|
@ -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>
|
|
@ -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="">→</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>
|
|
@ -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="text-black/50 dark:text-white/50 text-sm">
|
||||
<div class="text-50 text-sm">
|
||||
© 2023 {profileConfig.name}. All Rights Reserved.
|
||||
<br>
|
||||
Powered by <a class="link text-[var(--primary)]" target="_blank" href="https://github.com/saicaca/fuwari">Fuwari</a>
|
||||
|
|
|
@ -199,11 +199,36 @@ color_set({
|
|||
@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
|
||||
}
|
||||
.text-sub {
|
||||
.text-75 {
|
||||
@apply text-black/75 dark:text-white/75
|
||||
}
|
||||
.text-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>
|
|
@ -1,85 +1,65 @@
|
|||
---
|
||||
import Button from "./control/Button.astro";
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import DisplaySetting from "./widget/DisplaySetting.astro";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import {i18n} from "../i18n/translation";
|
||||
import DisplaySettings from "./widget/DisplaySettings.svelte";
|
||||
import {LinkPreset, NavBarLink} from "../types/config";
|
||||
import {navBarConfig, siteConfig} from "../config";
|
||||
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;
|
||||
|
||||
function isI18nKey(key: string): key is I18nKey {
|
||||
return Object.values(I18nKey).includes(key);
|
||||
}
|
||||
|
||||
let links: NavBarLink[] = navBarConfig.links.map((item) => {
|
||||
let links: NavBarLink[] = navBarConfig.links.map((item: NavBarLink | LinkPreset): NavBarLink => {
|
||||
if (typeof item === "number") {
|
||||
return getLinkPresetInfo(item)
|
||||
return LinkPresets[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={[
|
||||
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"]}>
|
||||
<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">
|
||||
<Icon name="material-symbols:home-outline-rounded" size={"1.75rem"} class="mb-1 mr-2" />
|
||||
{siteConfig.title}
|
||||
</div>
|
||||
</Button></a>
|
||||
<div class="hidden md:block">
|
||||
</a>
|
||||
<div class="hidden md:flex">
|
||||
{links.map((l) => {
|
||||
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">
|
||||
return <a aria-label={l.name} href={l.url} target={l.external ? "_blank" : null}
|
||||
class="btn-plain h-11 font-bold px-5 rounded-lg active:scale-95"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
{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>}
|
||||
</div>
|
||||
</Button>
|
||||
</a>;
|
||||
})}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<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>
|
||||
<!--<SearchPanel client:load>-->
|
||||
<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(--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>
|
||||
</SearchPanel>
|
||||
<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 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 slot="search-switch" name="material-symbols:search" size={"1.25rem"}></Icon>
|
||||
</Search>
|
||||
<button aria-label="Display Settings" class="btn-plain h-11 w-11 rounded-lg active:scale-90" id="display-settings-switch">
|
||||
<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:dark-mode-outline-rounded" size={"1.25rem"} class="absolute opacity-[var(--display-dark-icon)]"></Icon>
|
||||
</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>
|
||||
<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>
|
||||
<DisplaySetting></DisplaySetting>
|
||||
<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>
|
||||
|
||||
<style lang="stylus">
|
||||
|
@ -125,12 +105,13 @@ document.addEventListener('astro:after-swap', () => {
|
|||
|
||||
{import.meta.env.PROD && <script is:raw>
|
||||
async function loadPagefind() {
|
||||
const pagefind = await import("/pagefind/pagefind.js")
|
||||
const pagefind = await import('/pagefind/pagefind.js')
|
||||
await pagefind.options({
|
||||
"excerptLength": 20
|
||||
'excerptLength': 20
|
||||
})
|
||||
pagefind.init()
|
||||
window.pagefind = pagefind
|
||||
pagefind.search('') // speed up the first search
|
||||
}
|
||||
loadPagefind()
|
||||
</script>}
|
|
@ -1,6 +1,12 @@
|
|||
---
|
||||
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 {
|
||||
class: string;
|
||||
entry: any;
|
||||
|
@ -16,14 +22,6 @@ interface Props {
|
|||
}
|
||||
const { entry, title, url, published, tags, category, image, description, words } = Astro.props;
|
||||
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 !== '';
|
||||
|
||||
|
@ -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={["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}
|
||||
class="transition w-full block font-bold mb-3 text-3xl
|
||||
text-black/90 dark:text-white/90
|
||||
class="transition w-full block font-bold mb-3 text-3xl text-90
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
|
||||
active:text-[var(--title-active)] dark:active:text-[var(--title-active)]
|
||||
before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
|
||||
|
@ -48,10 +45,12 @@ const { remarkPluginFrontmatter } = await entry.render();
|
|||
<!-- metadata -->
|
||||
<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 }
|
||||
</div>
|
||||
|
||||
<!-- word count and read time -->
|
||||
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
|
||||
<div>{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
|
||||
<div>|</div>
|
||||
|
@ -71,26 +70,23 @@ const { remarkPluginFrontmatter } = await entry.render();
|
|||
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl">
|
||||
</Icon>
|
||||
</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">
|
||||
</ImageBox>
|
||||
</ImageWrapper>
|
||||
</a>}
|
||||
|
||||
{!hasCover &&
|
||||
<a href={url} aria-label={title} class="hidden md:block">
|
||||
<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">
|
||||
<a href={url} aria-label={title} class="hidden md:flex btn-regular w-[3.25rem]
|
||||
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"
|
||||
class="transition text-[var(--primary)] text-4xl mx-auto">
|
||||
</Icon>
|
||||
</Button>
|
||||
</a>
|
||||
}
|
||||
</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}}>
|
||||
:root
|
||||
--btn-enter-bg oklch(0.98 0.005 var(--hue))
|
||||
--btn-enter-bg-dark oklch(0.2 0.02 var(--hue))
|
||||
|
||||
</style>
|
|
@ -22,7 +22,7 @@ const className = Astro.props.class;
|
|||
>
|
||||
<Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon>
|
||||
</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>
|
||||
|
||||
<!-- categories -->
|
||||
|
@ -33,7 +33,7 @@ const className = Astro.props.class;
|
|||
</div>
|
||||
<div class="flex flex-row flex-nowrap">
|
||||
<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">
|
||||
{category || i18n(I18nKey.uncategorized)}
|
||||
</a></div>
|
||||
|
@ -51,12 +51,12 @@ const className = Astro.props.class;
|
|||
class="with-divider"
|
||||
>
|
||||
<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">
|
||||
{tag}
|
||||
</a>
|
||||
</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>
|
||||
|
@ -70,7 +70,7 @@ const className = Astro.props.class;
|
|||
text-[var(--btn-content)] mr-2
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
|
||||
---
|
||||
|
||||
<div class="card-base w-full h-[200px]">
|
||||
|
||||
</div>
|
|
@ -1,51 +1,50 @@
|
|||
---
|
||||
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` -->
|
||||
<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()">
|
||||
<Button name="Back to Top" card height="60px" width="60px">
|
||||
<div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="backToTop()">
|
||||
<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>
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="stylus">
|
||||
.back-to-top-wrapper
|
||||
width: 60px
|
||||
height: 60px
|
||||
width: 3.75rem
|
||||
height: 3.75rem
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
|
||||
.back-to-top-btn
|
||||
color: var(--primary)
|
||||
font-size: 36px
|
||||
font-size: 2.25rem
|
||||
font-weight: bold
|
||||
border: none
|
||||
position: fixed
|
||||
bottom: 240px
|
||||
bottom: 15rem
|
||||
opacity: 1
|
||||
cursor: pointer
|
||||
transform: translateX(80px)
|
||||
transform: translateX(5rem)
|
||||
i
|
||||
font-size: 28px
|
||||
font-size: 1.75rem
|
||||
&.hide
|
||||
transform: translateX(80px) scale(0.9)
|
||||
transform: translateX(5rem) scale(0.9)
|
||||
opacity: 0
|
||||
pointer-events: none
|
||||
&:active
|
||||
transform: translateX(80px) scale(0.9)
|
||||
transform: translateX(5rem) scale(0.9)
|
||||
|
||||
</style>
|
||||
|
||||
<script is:raw>
|
||||
|
||||
function topFunction() {
|
||||
function backToTop() {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function scrollFunction() {
|
||||
let btn = document.getElementById('back-to-top-btn');
|
||||
if (document.body.scrollTop > 600 || document.documentElement.scrollTop > 600) {
|
||||
|
@ -54,7 +53,5 @@ function scrollFunction() {
|
|||
btn.classList.add('hide')
|
||||
}
|
||||
}
|
||||
window.onscroll = function() {
|
||||
scrollFunction();
|
||||
}
|
||||
window.onscroll = scrollFunction
|
||||
</script>
|
||||
|
|
|
@ -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>
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
import Button from "./Button.astro";
|
||||
interface Props {
|
||||
size?: string;
|
||||
dot?: boolean;
|
||||
|
@ -8,9 +7,7 @@ interface Props {
|
|||
}
|
||||
const { size, dot, href, label }: Props = Astro.props;
|
||||
---
|
||||
<a href={href} aria-label={label}>
|
||||
<Button regular height="2rem" class="text-sm px-3 flex flex-row items-center 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>}
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</a>
|
||||
|
|
|
@ -11,7 +11,6 @@ const {page} = Astro.props;
|
|||
const HIDDEN = -1;
|
||||
|
||||
const className = Astro.props.class;
|
||||
import Button from "./Button.astro";
|
||||
|
||||
const ADJ_DIST = 2;
|
||||
const VISIBLE = ADJ_DIST * 2 + 1;
|
||||
|
@ -51,13 +50,22 @@ if (r < page.lastPage)
|
|||
|
||||
const parts: string[] = page.url.current.split('/');
|
||||
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"]}>
|
||||
<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}
|
||||
disabled = {page.url.prev == undefined}
|
||||
></Button>
|
||||
<a href={page.url.prev} aria-label={page.url.prev ? "Previous Page" : null}
|
||||
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
|
||||
{"disabled": page.url.prev == undefined}
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-left-rounded" size="1.75rem"></Icon>
|
||||
</a>
|
||||
<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) => {
|
||||
|
@ -69,16 +77,16 @@ const commonUrl: string = parts.slice(0, -1).join('/') + '/';
|
|||
>
|
||||
{p}
|
||||
</div>
|
||||
return <a href={commonUrl + 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">
|
||||
{p}
|
||||
</Button>
|
||||
</a>
|
||||
return <a href={getPageUrl(p)} aria-label=`Page ${p}`
|
||||
class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
|
||||
>{p}</a>
|
||||
})}
|
||||
</div>
|
||||
<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}
|
||||
disabled = {page.url.next == undefined}
|
||||
></Button>
|
||||
<a href={page.url.next} aria-label={page.url.next ? "Next Page" : null}
|
||||
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
|
||||
{"disabled": page.url.next == undefined}
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-right-rounded" size="1.75rem"></Icon>
|
||||
</a>
|
||||
</div>
|
|
@ -1,5 +1,10 @@
|
|||
---
|
||||
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 {
|
||||
title: string;
|
||||
slug: string;
|
||||
|
@ -9,15 +14,8 @@ interface Props {
|
|||
|
||||
const { title, slug, pubDate } = Astro.props;
|
||||
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 licenseConf = licenseConfig;
|
||||
|
||||
const postUrl = decodeURIComponent(Astro.url.toString());
|
||||
|
||||
---
|
||||
|
|
|
@ -22,6 +22,12 @@ interface Props {
|
|||
|
||||
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}>
|
||||
{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>
|
|
@ -8,9 +8,6 @@ interface Props {
|
|||
}
|
||||
|
||||
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"]}>
|
||||
{links.map((link) => (
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
---
|
||||
import ImageBox from "../misc/ImageBox.astro";
|
||||
import Button from "../control/Button.astro";
|
||||
import ImageWrapper from "../misc/ImageWrapper.astro";
|
||||
import {Icon} from "astro-icon/components";
|
||||
import {profileConfig} from "../../config";
|
||||
interface props {
|
||||
|
||||
}
|
||||
const className = Astro.props
|
||||
|
||||
const config = profileConfig;
|
||||
|
||||
---
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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"
|
||||
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl">
|
||||
</Icon>
|
||||
</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>
|
||||
<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="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div>
|
||||
<div class="flex gap-2 mx-2 justify-center mb-4">
|
||||
{config.links.map(item =>
|
||||
<a aria-label={item.name} href={item.url} target="_blank">
|
||||
<Button isIcon iconName={item.icon} regular height="40px" class="rounded-lg active:scale-90"></Button>
|
||||
<a aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
|
||||
<Icon name={item.icon} size="1.5rem"></Icon>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
import Profile from "./Profile.astro";
|
||||
import RecentPost from "./RecentPost.astro";
|
||||
import Tag from "./Tags.astro";
|
||||
import Categories from "./Categories.astro";
|
||||
|
||||
|
@ -12,10 +11,6 @@ const className = Astro.props.class;
|
|||
</div>
|
||||
<div class="flex flex-col w-full gap-4 top-4 sticky top-4" transition:animate="none">
|
||||
<Categories></Categories>
|
||||
<!--<RecentPost></RecentPost>-->
|
||||
<Tag></Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
import Button from "../control/Button.astro";
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import {i18n} from "../../i18n/translation";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
|
@ -26,11 +25,11 @@ const {
|
|||
<slot></slot>
|
||||
</div>
|
||||
{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">
|
||||
<Icon name="material-symbols:more-horiz" size={28}></Icon> {i18n(I18nKey.more)}
|
||||
</div>
|
||||
</Button>
|
||||
</button>
|
||||
</div>}
|
||||
</widget-layout>
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import type {
|
|||
NavBarConfig,
|
||||
ProfileConfig,
|
||||
SiteConfig,
|
||||
} from './types/config.ts'
|
||||
import { LinkPreset } from './types/config.ts'
|
||||
} from './types/config'
|
||||
import { LinkPreset } from './types/config'
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
title: 'Fuwari',
|
||||
|
@ -12,7 +12,7 @@ export const siteConfig: SiteConfig = {
|
|||
lang: 'en',
|
||||
themeHue: 250,
|
||||
banner: {
|
||||
enable: true,
|
||||
enable: false,
|
||||
src: 'assets/images/demo-banner.png',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const UNCATEGORIZED = '__uncategorized__'
|
||||
|
||||
export const PAGE_SIZE = 8
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
declare module '*.yml' {
|
||||
const value: unknown
|
||||
export default value
|
||||
}
|
|
@ -20,7 +20,7 @@ enum I18nKey {
|
|||
postCount = 'postCount',
|
||||
postsCount = 'postsCount',
|
||||
|
||||
primaryColor = 'primaryColor',
|
||||
themeColor = 'themeColor',
|
||||
|
||||
more = 'more',
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
import Key from '../i18nKey'
|
||||
import type { Translation } from '../translation'
|
||||
|
||||
export const en: Translation = {
|
||||
[Key.home]: 'Home',
|
||||
|
@ -23,7 +23,7 @@ export const en: Translation = {
|
|||
[Key.postCount]: 'post',
|
||||
[Key.postsCount]: 'posts',
|
||||
|
||||
[Key.primaryColor]: 'Primary Color',
|
||||
[Key.themeColor]: 'Theme Color',
|
||||
|
||||
[Key.more]: 'More',
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
import Key from '../i18nKey'
|
||||
import type { Translation } from '../translation'
|
||||
|
||||
export const ja: Translation = {
|
||||
[Key.home]: 'Home',
|
||||
|
@ -23,7 +23,7 @@ export const ja: Translation = {
|
|||
[Key.postCount]: '件の投稿',
|
||||
[Key.postsCount]: '件の投稿',
|
||||
|
||||
[Key.primaryColor]: '原色',
|
||||
[Key.themeColor]: 'テーマカラー',
|
||||
|
||||
[Key.more]: 'もっと',
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
import Key from '../i18nKey'
|
||||
import type { Translation } from '../translation'
|
||||
|
||||
export const zh_CN: Translation = {
|
||||
[Key.home]: '主页',
|
||||
|
@ -23,7 +23,7 @@ export const zh_CN: Translation = {
|
|||
[Key.postCount]: '篇文章',
|
||||
[Key.postsCount]: '篇文章',
|
||||
|
||||
[Key.primaryColor]: '主题色',
|
||||
[Key.themeColor]: '主题色',
|
||||
|
||||
[Key.more]: '更多',
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
import Key from '../i18nKey'
|
||||
import type { Translation } from '../translation'
|
||||
|
||||
export const zh_TW: Translation = {
|
||||
[Key.home]: '首頁',
|
||||
|
@ -23,7 +23,7 @@ export const zh_TW: Translation = {
|
|||
[Key.postCount]: '篇文章',
|
||||
[Key.postsCount]: '篇文章',
|
||||
|
||||
[Key.primaryColor]: '主題色',
|
||||
[Key.themeColor]: '主題色',
|
||||
|
||||
[Key.more]: '更多',
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { siteConfig } from '../config.ts'
|
||||
import type I18nKey from './i18nKey.ts'
|
||||
import { en } from './languages/en.ts'
|
||||
import { ja } from './languages/ja.ts'
|
||||
import { zh_CN } from './languages/zh_CN.ts'
|
||||
import { zh_TW } from './languages/zh_TW.ts'
|
||||
import { siteConfig } from '../config'
|
||||
import type I18nKey from './i18nKey'
|
||||
import { en } from './languages/en'
|
||||
import { ja } from './languages/ja'
|
||||
import { zh_CN } from './languages/zh_CN'
|
||||
import { zh_TW } from './languages/zh_TW'
|
||||
|
||||
export type Translation = {
|
||||
[K in I18nKey]: string
|
||||
|
@ -23,8 +23,7 @@ const map: { [key: string]: Translation } = {
|
|||
}
|
||||
|
||||
export function getTranslation(lang: string): Translation {
|
||||
lang = lang.toLowerCase()
|
||||
return map[lang] || defaultTranslation
|
||||
return map[lang.toLowerCase()] || defaultTranslation
|
||||
}
|
||||
|
||||
export function i18n(key: I18nKey): string {
|
||||
|
|
|
@ -4,7 +4,7 @@ import '@fontsource/roboto/400.css';
|
|||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
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 {pathsEqual} from "@utils/url-utils";
|
||||
|
@ -95,10 +95,10 @@ if (title) {
|
|||
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"
|
||||
>
|
||||
</ImageBox>
|
||||
</ImageWrapper>
|
||||
</div>
|
||||
<slot />
|
||||
</GlobalStyles>
|
||||
|
@ -107,20 +107,7 @@ if (title) {
|
|||
<style is:global>
|
||||
:root {
|
||||
--hue: var(--configHue);
|
||||
--accent: 136, 58, 234;
|
||||
--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;
|
||||
--page-width: 75rem;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
|
@ -163,6 +150,7 @@ import {
|
|||
SizeObserverPlugin,
|
||||
ClickScrollPlugin
|
||||
} from 'overlayscrollbars';
|
||||
import {getHue, setHue} from "../utils/setting-utils";
|
||||
|
||||
/* Preload fonts */
|
||||
// (async function() {
|
||||
|
@ -206,51 +194,23 @@ 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 => {
|
||||
let panelDom = document.getElementById(panel);
|
||||
let tDom = event.target;
|
||||
for (let ig of ignores) {
|
||||
let ie = document.getElementById(ig)
|
||||
if (ie == tDom || ie.contains(tDom)) {
|
||||
if (ie == tDom || (ie?.contains(tDom))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panelDom.classList.add("closed");
|
||||
});
|
||||
}
|
||||
setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"])
|
||||
setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"])
|
||||
setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"])
|
||||
}
|
||||
setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"])
|
||||
setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"])
|
||||
setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"])
|
||||
|
||||
|
||||
function loadTheme() {
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) &&
|
||||
|
@ -264,10 +224,7 @@ function loadTheme() {
|
|||
}
|
||||
|
||||
function loadHue() {
|
||||
const hue = localStorage.hue;
|
||||
if (hue) {
|
||||
document.documentElement.style.setProperty('--hue', hue);
|
||||
}
|
||||
setHue(getHue())
|
||||
}
|
||||
|
||||
function setBannerHeight() {
|
||||
|
@ -315,7 +272,6 @@ function init() {
|
|||
setBannerHeight();
|
||||
loadTheme();
|
||||
loadHue();
|
||||
activateDisplaySettings();
|
||||
initCustomScrollbar();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import SideBar from "@components/widget/SideBar.astro";
|
|||
import {pathsEqual} from "@utils/url-utils";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import BackToTop from "@components/control/BackToTop.astro";
|
||||
import DisplaySetting from "@components/widget/DisplaySetting.astro";
|
||||
import {siteConfig} from "@/config";
|
||||
|
||||
interface Props {
|
||||
|
@ -13,17 +12,15 @@ interface Props {
|
|||
banner?: string;
|
||||
}
|
||||
|
||||
const { title, banner } = Astro.props;
|
||||
|
||||
const isHomePage = pathsEqual(Astro.url.pathname, '/') || pathsEqual(Astro.url.pathname, '/page/1');
|
||||
|
||||
const enableBanner = siteConfig.banner.enable;
|
||||
const { title, banner } = Astro.props
|
||||
const isHomePage = pathsEqual(Astro.url.pathname, '/') || pathsEqual(Astro.url.pathname, '/page/1')
|
||||
const enableBanner = siteConfig.banner.enable
|
||||
|
||||
---
|
||||
|
||||
<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]
|
||||
mx-auto gap-4 relative px-0 md:px-4`
|
||||
<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"
|
||||
transition:animate="none"
|
||||
>
|
||||
<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>
|
||||
</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">
|
||||
<!-- the overflow-hidden here prevent long text break the layout-->
|
||||
|
|
|
@ -5,7 +5,6 @@ import ArchivePanel from "@components/ArchivePanel.astro";
|
|||
import {i18n} from "@i18n/translation";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = await getCategoryList();
|
||||
return categories.map(category => {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
---
|
||||
|
||||
---
|
|
@ -1,10 +1,9 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
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 PostMetadata from "@components/PostMetadata.astro";
|
||||
import Button from "@components/control/Button.astro";
|
||||
import PostMetadata from "../../components/PostMeta.astro";
|
||||
import {i18n} from "@i18n/translation";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
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 -->
|
||||
|
||||
{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>}
|
||||
|
@ -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">
|
||||
<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)]" />
|
||||
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
||||
{entry.data.nextTitle}
|
||||
</div>
|
||||
</Button>}
|
||||
</div>}
|
||||
</a>
|
||||
|
||||
<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">
|
||||
{entry.data.prevTitle}
|
||||
</div>
|
||||
<Icon name="material-symbols:chevron-right-rounded" size={32} class="text-[var(--primary)]" />
|
||||
</Button>}
|
||||
</div>}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import I18nKey from '@i18n/i18nKey'
|
||||
import { i18n } from '@i18n/translation'
|
||||
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() {
|
||||
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 dateA = new Date(a.data.published)
|
||||
|
@ -32,7 +31,7 @@ export type Tag = {
|
|||
|
||||
export async function getTagList(): Promise<Tag[]> {
|
||||
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 } = {}
|
||||
|
@ -58,16 +57,18 @@ export type Category = {
|
|||
|
||||
export async function getCategoryList(): Promise<Category[]> {
|
||||
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 } = {}
|
||||
allBlogPosts.map(post => {
|
||||
if (!post.data.category) {
|
||||
const ucKey = i18n(I18nKey.uncategorized);
|
||||
const ucKey = i18n(I18nKey.uncategorized)
|
||||
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1
|
||||
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) => {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {i18n} from "@i18n/translation.ts";
|
||||
import i18nKey from "@i18n/i18nKey.ts";
|
||||
import i18nKey from '@i18n/i18nKey'
|
||||
import { i18n } from '@i18n/translation'
|
||||
|
||||
export function pathsEqual(path1: string, path2: string) {
|
||||
const normalizedPath1 = path1.replace(/^\/|\/$/g, '').toLowerCase()
|
||||
|
@ -18,8 +18,7 @@ export function getPostUrlBySlug(slug: string): string | null {
|
|||
}
|
||||
|
||||
export function getCategoryUrl(category: string): string | null {
|
||||
if (!category)
|
||||
return null
|
||||
if (!category) return null
|
||||
if (category === i18n(i18nKey.uncategorized))
|
||||
return '/archive/category/uncategorized'
|
||||
return `/archive/category/${category}`
|
||||
|
|
Loading…
Reference in New Issue