feat: initial commit

(cherry picked from commit 44c4d7b9521fe449e61edc614446195861932f8c)
This commit is contained in:
saicaca 2023-09-26 14:27:38 +08:00
parent 02b0a65314
commit 124843848f
58 changed files with 13083 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
.vercel

54
README.md Normal file
View File

@ -0,0 +1,54 @@
# Astro Starter Kit: Basics
```
npm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card.astro
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:3000` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

49
astro.config.mjs Normal file
View File

@ -0,0 +1,49 @@
import { defineConfig } from 'astro/config';
import yaml from '@rollup/plugin-yaml';
import icon from "astro-icon";
import tailwind from "@astrojs/tailwind";
import {remarkReadingTime} from "./src/plugins/remark-reading-time.mjs";
import Color from 'colorjs.io';
// https://astro.build/config
const oklchToHex = function (str) {
const DEFAULT_HUE = 250;
const regex = /-?\d+(\.\d+)?/g;
const matches = str.string.match(regex);
const lch = [matches[0], matches[1], DEFAULT_HUE];
return new Color("oklch", lch).to("srgb").toString({format: "hex"});
}
export default defineConfig({
integrations: [
tailwind(),
icon({
include: {
'material-symbols': ['*'],
'fa6-brands': ['*']
}
})
],
markdown: {
remarkPlugins: [remarkReadingTime],
},
redirects: {
'/': '/page/1',
},
vite: {
plugins: [yaml()],
css: {
preprocessorOptions: {
stylus: {
define: {
oklchToHex: oklchToHex
}
}
}
}
},
});

6779
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.2.0",
"@astrojs/tailwind": "^4.0.0",
"@astrojs/ts-plugin": "^1.1.3",
"@fontsource/roboto": "^5.0.8",
"astro": "^3.0.10",
"astro-icon": "^1.0.0-next.2",
"colorjs.io": "^0.4.5",
"mdast-util-to-string": "^4.0.0",
"reading-time": "^1.5.0",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2"
},
"devDependencies": {
"@iconify-json/fa6-brands": "^1.1.13",
"@iconify-json/material-symbols": "^1.1.57",
"@rollup/plugin-yaml": "^4.1.1",
"@tailwindcss/typography": "^0.5.9",
"stylus": "^0.59.0"
}
}

4483
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

BIN
public/fonts/RobotoFlex.ttf Normal file

Binary file not shown.

View File

@ -0,0 +1,121 @@
---
interface Props {
keyword: string;
tags: string[];
categories: string[];
}
const { keyword, tags, categories} = Astro.props;
import Button from "./control/Button.astro";
import {getPostUrlBySlug, getSortedPosts} from "../utils/content-utils";
let posts = await getSortedPosts()
if (Array.isArray(tags) && tags.length > 0) {
posts = posts.filter(post =>
Array.isArray(post.data.tags) && post.data.tags.some(tag => tags.includes(tag))
);
}
if (Array.isArray(categories) && categories.length > 0) {
posts = posts.filter(post =>
Array.isArray(post.data.categories) && post.data.categories.some(category => categories.includes(category))
);
}
const groups = function () {
const groupedPosts = posts.reduce((grouped, post) => {
const year = post.data.pubDate.getFullYear()
if (!grouped[year]) {
grouped[year] = []
}
grouped[year].push(post)
return grouped
}, {})
// convert the object to an array
const groupedPostsArray = Object.keys(groupedPosts).map(key => ({
year: key,
posts: groupedPosts[key]
}))
// sort years by latest first
groupedPostsArray.sort((a, b) => b.year - a.year)
return groupedPostsArray;
}();
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${month}-${day}`;
}
// console.log(groups)
---
<div class="card-base px-8 py-6">
{
groups.map(group => (
<div>
<div class="flex flex-row w-full items-center h-[60px]">
<div class="w-[10%] transition text-2xl font-bold text-right text-black/75 dark:text-white/75">{group.year}</div>
<div class="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-[80%] transition text-left text-black/50 dark:text-white/50">{group.posts.length} Articles</div>
</div>
{group.posts.map(post => (
<a href={getPostUrlBySlug(post.slug)} class="group">
<Button light height="40px" class="w-full hover:text-[initial]">
<div class="flex flex-row justify-start items-center h-full">
<!-- date -->
<div class="w-[10%] transition text-sm text-right text-black/50 dark:text-white/50">{formatDate(post.data.pubDate)}</div>
<!-- dot and line -->
<div class="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
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
outline outline-4 z-50
outline-[var(--card-bg)]
group-hover:outline-[var(--btn-plain-bg-hover)]
group-active:outline-[var(--btn-plain-bg-active)]
"
></div>
</div>
<!-- post title -->
<div class="max-w-[65%] w-[65%] transition text-left font-bold">
<div class="group-hover:ml-1 transition-all group-hover:text-[var(--primary)]
text-black/80 dark:text-white/80 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden">
{post.data.title}
</div>
</div>
<!-- tag list -->
<div class="w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden
text-black/30 dark:text-white/30"
>#Test #Markdown</div>
</div>
</Button>
</a>
))}
</div>
))
}
</div>
<style>
@tailwind components;
@tailwind utilities;
@layer components {
.dash-line {
}
.dash-line::before {
content: "";
@apply w-[10%] h-full absolute -top-1/2 left-[calc(50%_-_1px)] -top-[50%] border-l-[2px]
border-dashed pointer-events-none border-[var(--line-color)] transition
}
}
</style>

View File

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

View File

@ -0,0 +1,7 @@
---
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>

60
src/components/Card.astro Normal file
View File

@ -0,0 +1,60 @@
---
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

@ -0,0 +1,92 @@
---
---
<div>
<slot/>
</div>
<style is:global lang="stylus">
/* utils */
white(a)
rgba(255, 255, 255, a)
black(a)
rgba(0, 0, 0, a)
isOklch(c)
return substr(c, 0, 5) == 'oklch'
oklch_fallback(c)
str = '' + c // convert color value to string
if isOklch(str)
return convert(oklchToHex(str))
return c
color_set(colors)
@supports (color: oklch(0 0 0))
:root
for key, value in colors
{key}: value[0]
:root.dark
for key, value in colors
if length(value) > 1
{key}: value[1]
/* provide fallback color for oklch */
@supports not (color: oklch(0 0 0))
:root
for key, value in colors
{key}: oklch_fallback(value[0])
:root.dark
for key, value in colors
if length(value) > 1
{key}: oklch_fallback(value[1])
:root
--radius-large 16px
--banner-height-home 60vh
--banner-height 50vh
color_set({
--primary: oklch(0.70 0.14 var(--hue))
--card-bg: white oklch(0.25 0.02 var(--hue))
--btn-content: oklch(0.55 0.12 var(--hue))
--btn-regular-bg: oklch(0.95 0.025 var(--hue)) oklch(0.38 0.04 var(--hue))
--btn-plain-bg-hover: oklch(0.95 0.025 var(--hue)) oklch(0.2 0.02 var(--hue))
--btn-plain-bg-active: oklch(0.98 0.01 var(--hue)) oklch(0.17 0.017 var(--hue))
--btn-card-bg-hover: oklch(0.96 0.015 var(--hue)) oklch(0.3 0.03 var(--hue))
--btn-card-bg-active: oklch(0.9 0.03 var(--hue)) oklch(0.35 0.035 var(--hue))
--deep-text: oklch(0.25 0.02 var(--hue))
--line-color: black(0.1) white(0.1)
--meta-divider: black(0.2) white(0.2)
--selection-bg: oklch(0.90 0.05 var(--hue)) oklch(0.40 0.08 var(--hue))
})
/* some global styles */
::selection
background-color: var(--selection-bg)
</style>
<style is:global>
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.card-base {
@apply rounded-[var(--radius-large)] overflow-hidden bg-[var(--card-bg)] transition;
}
h1, h2, h3, h4, h5, h6, p, a, span, li, ul, ol, blockquote, code, pre, table, th, td, strong {
@apply transition;
}
}
</style>

View File

@ -0,0 +1,60 @@
---
import Button from "./control/Button.astro";
import { Icon } from 'astro-icon/components';
const className = Astro.props.class;
---
<div class:list={[
className,
"card-base max-w-[var(--page-width)] h-[72px] rounded-t-none mx-auto flex items-center justify-between px-4"]}>
<a href="/"><Button height="52px" class="px-5 font-bold" light>
<div class="flex flex-row text-[var(--primary)] items-center text-md">
<Icon name="material-symbols:home-outline-rounded" size={28} class="mb-1 mr-2" />
<div class="top-2"></div>Vivia Preview
</div>
</Button></a>
<div>
<a href="/"><Button light class="font-bold px-5">Home</Button></a>
<a href="/archive"><Button light class="font-bold px-5">Archive</Button></a>
<Button light class="font-bold px-5">About</Button>
</div>
<div>
<Button id="scheme-switch" iconName="material-symbols:wb-sunny-outline-rounded" iconSize={20} isIcon light></Button>
</div>
</div>
<style lang="stylus">
</style>
<script>
function switchTheme() {
if (localStorage.theme === 'dark') {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
} else {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
}
}
function loadThemeSwitchScript() {
let switchBtn = document.getElementById("scheme-switch");
if (switchBtn === null) {
console.log("test")
}
switchBtn.addEventListener("click", function () {
console.log("test")
switchTheme()
});
}
loadThemeSwitchScript();
document.addEventListener('astro:after-swap', () => {
loadThemeSwitchScript();
}, { once: false });
</script>

View File

@ -0,0 +1,78 @@
---
import {formatDateToYYYYMMDD} from "../utils/date-utils";
import { Icon } from 'astro-icon/components';
interface Props {
class: string;
pubDate: Date;
tags: string[];
categories: string[];
}
const {pubDate, tags, categories} = Astro.props;
const className = Astro.props.class;
---
<div class:list={["flex flex-wrap text-neutral-500 dark:text-neutral-400 items-center gap-4 gap-x-4 gap-y-3", className]}>
<!-- publish date -->
<div class="flex items-center">
<div class="meta-icon"
>
<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(pubDate)}</span>
</div>
<!-- categories -->
<div class="flex items-center">
<div class="meta-icon"
>
<Icon name="material-symbols:menu-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row">
{categories && categories.map(category => <div
class="with-divider"
>
<a href=`/archive/category/${category}`
class="transition text-black/50 dark:text-white/50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]">
{category}
</a>
</div>)}
{!categories && <div class="transition text-black/50 dark:text-white/50 text-sm font-medium">Uncategorized</div>}
</div>
</div>
<!-- tags -->
<div class="flex items-center">
<div class="meta-icon"
>
<Icon name="material-symbols:tag-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row">
{tags.map(tag => <div
class="with-divider"
>
<a href=`/archive/tag/${tag}`
class="transition text-black/50 dark:text-white/50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]">
{tag}
</a>
</div>)}
</div>
</div>
</div>
<style>
@tailwind components;
@layer components {
.meta-icon {
@apply w-8 h-8 transition rounded-md flex items-center justify-center bg-[var(--btn-regular-bg)]
text-[var(--btn-content)] dark:text-[var(--primary)] mr-2
}
.with-divider {
@apply before:content-['/'] before:mx-[6px] before:text-[var(--meta-divider)] before:text-sm
before:font-medium before:first-of-type:hidden before:transition
}
}
</style>

View File

@ -0,0 +1,83 @@
---
import {formatDateToYYYYMMDD} from "../utils/date-utils";
interface Props {
title: string;
url: string;
pubDate: Date;
tags: string[];
cover: string;
description: string;
words: number;
}
const { title, url, pubDate, tags, cover, 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 = cover !== undefined && cover !== null && cover !== '';
---
<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(pubDate)}</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

@ -0,0 +1,78 @@
---
import PostMetadata from "./PostMetadata.astro";
interface Props {
class: string;
entry: any;
title: string;
url: string;
pubDate: Date;
tags: string[];
categories: string[];
cover: string;
description: string;
words: number;
}
const { entry, title, url, pubDate, tags, categories, cover, 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';
// tags = ['Foo', 'Bar', 'Baz', 'Qux', 'Quux'];
// const cover = 'https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg';
// cover = null;
const hasCover = cover !== undefined && cover !== null && cover !== '';
const coverWidth = "30%";
const { remarkPluginFrontmatter } = await entry.render();
---
<div class:list={["card-base flex w-full rounded-[var(--radius-large)] overflow-hidden relative", className]}>
<div class:list={[" px-10 pt-7 pb-6 relative", {'w-full': !hasCover, "w-[calc(100%_-_var(--coverWidth))]": hasCover}]}>
<a href={url}
class="transition w-full block font-bold mb-3 text-4xl
text-black/90 dark:text-white/90
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
before:absolute before:top-[38px] before:left-5
">
{title}
</a>
<!-- metadata -->
<PostMetadata pubDate={pubDate} tags={tags} categories={categories} class="mb-4"></PostMetadata>
<div class="transition text-black/75 dark:text-white/75 mb-4">
{ description }
</div>
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
<div>{remarkPluginFrontmatter.words} words</div>
<div>|</div>
<div>{remarkPluginFrontmatter.minutes} minutes</div>
</div>
</div>
{hasCover && <a href={url}
class=`group w-[var(--coverWidth)] absolute top-3 bottom-3 right-3 rounded-xl overflow-hidden`>
<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" 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>

View File

@ -0,0 +1,16 @@
---
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

@ -0,0 +1,77 @@
---
interface Props {
id?: 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,
isIcon = false,
iconName,
width,
height = '44px',
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}
class:list={[
className,
`
rounded-lg
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-neutral-900': light,
'hover:text-[var(--primary)]': light,
'dark:text-neutral-300': light || regular,
'dark:hover:text-[var(--primary)]': light,
'bg-[var(--btn-regular-bg)]': regular,
'hover:bg-[oklch(0.9_0.05_var(--hue))]': regular,
'active:bg-[oklch(0.85_0.08_var(--hue))]': regular,
'text-[var(--btn-content)]': regular,
'dark:bg-[oklch(0.38_0.04_var(--hue))]': regular,
'dark:hover:bg-[oklch(0.45_0.045_var(--hue))]': regular,
'dark:active:bg-[oklch(0.5_0.05_var(--hue))]': regular,
'card-base': 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

@ -0,0 +1,42 @@
---
interface Props {
badge?: string
url?: string
}
const { badge, url } = Astro.props
---
<a href={url}>
<button
class:list={`
w-full
h-10
rounded-lg
bg-none
hover:bg-[var(--btn-plain-bg-hover)]
active:bg-[var(--btn-plain-bg-active)]
transition-all
pl-2
hover:pl-3
text-neutral-700
hover:text-[var(--primary)]
dark:text-neutral-300
dark:hover:text-[var(--primary)]
`
}
>
<div class="flex items-center justify-between relative mr-2">
<div class="overflow-hidden text-left whitespace-nowrap overflow-ellipsis ">
<slot></slot>
</div>
{ badge !== undefined && badge !== null && badge !== '' &&
<div class="transition h-[28px] ml-4 min-w-[32px] rounded-lg text-sm font-bold
text-[var(--btn-content)] dark:text-[var(--deep-text)]
bg-[oklch(0.95_0.025_var(--hue))] dark:bg-[var(--primary)]
flex items-center justify-center">
{ badge }
</div>
}
</div>
</button>
</a>

View File

@ -0,0 +1,15 @@
---
import Button from "./Button.astro";
interface Props {
size?: string;
dot?: boolean;
href?: string;
}
const { size, dot, href }: Props = Astro.props;
---
<a href={href}>
<Button regular height="32px" class="text-[15px] px-3 flex flex-row items-center">
{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>

View File

@ -0,0 +1,84 @@
---
import type { Page } from "astro";
import { Icon } from 'astro-icon/components';
interface Props {
page: Page;
class?: string;
}
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;
// for test
let count = 1;
let l = page.currentPage, r = page.currentPage;
while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) {
count += 2;
l--;
r++;
}
while (0 < l - 1 && count < VISIBLE) {
count++;
l--;
}
while (r + 1 <= page.lastPage && count < VISIBLE) {
count++;
r++;
}
let pages: number[] = [];
if (l > 1)
pages.push(1);
if (l == 3)
pages.push(2);
if (l > 3)
pages.push(HIDDEN);
for (let i = l; i <= r; i++)
pages.push(i);
if (r < page.lastPage - 2)
pages.push(HIDDEN);
if (r == page.lastPage - 2)
pages.push(page.lastPage - 1);
if (r < page.lastPage)
pages.push(page.lastPage);
const parts: string[] = page.url.current.split('/');
const commonUrl: string = parts.slice(0, -1).join('/') + '/';
---
<div class:list={[className, "flex flex-row gap-3 justify-center"]}>
<a href={page.url.prev}>
<Button isIcon card iconName="material-symbols:chevron-left-rounded" class="text-[var(--primary)]" iconSize={28}
disabled = {page.url.prev == undefined}
></Button>
</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) => {
if (p == HIDDEN)
return <Icon name="material-symbols:more-horiz" class="mx-1"/>;
if (p == page.currentPage)
return <div class="h-[44px] w-[44px] rounded-lg bg-[var(--primary)] flex items-center justify-center
font-bold text-white dark:text-black/70"
>
{p}
</div>
return <a href={commonUrl + p}>
<Button card iconName="material-symbols:chevron-left-rounded" height="44px" width="44px">
{p}
</Button>
</a>
})}
</div>
<a href={page.url.next}>
<Button isIcon card iconName="material-symbols:chevron-right-rounded" class="text-[var(--primary)]" iconSize={28}
disabled = {page.url.next == undefined}
></Button>
</a>
</div>

View File

@ -0,0 +1,15 @@
---
interface Props {
id?: string
src: string;
class?: string;
alt?: string
}
const {id, src, alt} = Astro.props;
const className = Astro.props.class;
---
<div class:list={[className, 'overflow-hidden relative']}>
<div class="transition absolute top-0 bottom-0 left-0 right-0 dark:bg-black/10 bg-opacity-50"></div>
<img src={src} alt={alt} class="w-full h-full object-center object-cover">
</div>

View File

@ -0,0 +1,26 @@
---
import ImageBox from "../misc/ImageBox.astro";
import ButtonLight from "../control/Button.astro";
import {getConfig} from "../../utils/config-utils";
interface props {
}
const className = Astro.props
const vConf = getConfig();
---
<div class="card-base" transition:persist>
<ImageBox src={vConf.profile.avatar} class="w-full rounded-2xl mb-3"></ImageBox>
<div class="font-bold text-lg text-center mb-1 dark:text-neutral-50 transition">{vConf.profile.author}</div>
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-3 transition"></div>
<div class="text-center text-neutral-400 mb-2 transition">{vConf.profile.subtitle}</div>
<div class="flex gap-2 mx-2 justify-center mb-4">
{vConf.profile.links.map(item =>
<a href={item.url} target="_blank">
<ButtonLight isIcon iconName={item.icon} regular height="40px"></ButtonLight>
</a>
)}
</div>
</div>

View File

@ -0,0 +1,18 @@
---
import WidgetLayout from "./WidgetLayout.astro";
import ButtonLink from "../control/ButtonLink.astro";
import {getPostUrlBySlug, getSortedPosts} from "../../utils/content-utils";
let posts = await getSortedPosts()
const LIMIT = 5;
posts = posts.slice(0, LIMIT)
// console.log(posts)
---
<WidgetLayout name="Recent Posts">
{posts.map(post =>
<ButtonLink url={getPostUrlBySlug(post.slug)}>{post.data.title}</ButtonLink>
)}
</WidgetLayout>

View File

@ -0,0 +1,27 @@
---
import Profile from "./Profile.astro";
import RecentPost from "./RecentPost.astro";
import Tag from "./Tag.astro";
const className = Astro.props.class;
---
<div id="sidebar" class:list={[className, "flex flex-col w-full gap-4"]} transition:persist>
<Profile></Profile>
<RecentPost></RecentPost>
<Tag></Tag>
</div>
<style>
#sidebar {
view-transition-name: ssss;
}
/* TODO temporarily */
html::view-transition-old(ssss) {
mix-blend-mode: normal;
animation: none;
}
html::view-transition-new(ssss) {
mix-blend-mode: normal;
animation: none;
}
</style>

View File

@ -0,0 +1,18 @@
---
import WidgetLayout from "./WidgetLayout.astro";
import ButtonTag from "../control/ButtonTag.astro";
import {getTagList} from "../../utils/content-utils";
const tags = await getTagList();
---
<WidgetLayout name="Tags">
<div class="flex gap-2 flex-wrap">
{tags.map(t => (
<ButtonTag href={`/archive/tag/${t.name}`}>
{t.name}
</ButtonTag>
))}
</div>
</WidgetLayout>

View File

@ -0,0 +1,18 @@
---
interface Props {
name?: string;
}
const props = Astro.props;
const {
name,
} = Astro.props
---
<div class="pb-4 card-base">
<div class="font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-8 mt-4 mb-2
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
before:absolute before:left-[-16px] before:top-[5.5px]">{name}</div>
<div class="px-4">
<slot></slot>
</div>
</div>

12
src/content/config.ts Normal file
View File

@ -0,0 +1,12 @@
import { z, defineCollection } from "astro:content";
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
cover: z.string().optional(),
})
})
export const collections = {
'blog': blogCollection,
}

View File

@ -0,0 +1,28 @@
---
title: 'My First Blog PostMy First Blog PostMy First Blog PostMy First Blog PostMy First Blog PostMy First Blog PostMy First Blog PostMy First Blog PostMy First Blog Post'
pubDate: 2022-10-01
description: 'This is the first post of my new Astro blog.'
author: 'Astro Learner'
image:
url: 'https://docs.astro.build/assets/full-logo-light.png'
alt: 'The full Astro logo.'
tags: ["astro", "blogging", "learning in public"]
categories: ['Foo', 'Bar']
---
# My First Blog Post
Published on: 2022-07-01
Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
## What I've accomplished
1. **Installing Astro**: First, I created a new Astro project and set up my online accounts.
2. **Making Pages**: I then learned how to make pages by creating new `.astro` files and placing them in the `src/pages/` folder.
3. **Making Blog Posts**: This is my first blog post! I now have Astro pages and Markdown posts!
## What's next
I will finish the Astro tutorial, and then keep adding more posts. Watch this space for more to come.

View File

@ -0,0 +1,28 @@
---
title: 'My Second Blog Post'
pubDate: 2021-07-01
description: 'This is the first post of my new Astro blog.'
author: 'Astro Learner'
image:
url: 'https://docs.astro.build/assets/full-logo-light.png'
alt: 'The full Astro logo.'
tags: ["astro", "blogging", "learning in public"]
cover: 'https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg'
---
# My First Blog Post
Published on: 2022-07-01
Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
## What I've accomplished
1. **Installing Astro**: First, I created a new Astro project and set up my online accounts.
2. **Making Pages**: I then learned how to make pages by creating new `.astro` files and placing them in the `src/pages/` folder.
3. **Making Blog Posts**: This is my first blog post! I now have Astro pages and Markdown posts!
## What's next
I will finish the Astro tutorial, and then keep adding more posts. Watch this space for more to come.

View File

@ -0,0 +1,27 @@
---
title: 'My Third Blog Post'
pubDate: 2020-07-01
description: 'This is the first post of my new Astro blog.'
author: 'Astro Learner'
image:
url: 'https://docs.astro.build/assets/full-logo-light.png'
alt: 'The full Astro logo.'
tags: ["astro", "blogging", "learning in public"]
---
# My First Blog Post
Published on: 2022-07-01
Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
## What I've accomplished
1. **Installing Astro**: First, I created a new Astro project and set up my online accounts.
2. **Making Pages**: I then learned how to make pages by creating new `.astro` files and placing them in the `src/pages/` folder.
3. **Making Blog Posts**: This is my first blog post! I now have Astro pages and Markdown posts!
## What's next
I will finish the Astro tutorial, and then keep adding more posts. Watch this space for more to come.

View File

@ -0,0 +1,27 @@
---
title: 'My Fourth Blog Post'
pubDate: 2022-07-01
description: 'This is the first post of my new Astro blog.'
author: 'Astro Learner'
image:
url: 'https://docs.astro.build/assets/full-logo-light.png'
alt: 'The full Astro logo.'
tags: ["astro", "blogging", "learning in public"]
---
# My First Blog Post
Published on: 2022-07-01
Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
## What I've accomplished
1. **Installing Astro**: First, I created a new Astro project and set up my online accounts.
2. **Making Pages**: I then learned how to make pages by creating new `.astro` files and placing them in the `src/pages/` folder.
3. **Making Blog Posts**: This is my first blog post! I now have Astro pages and Markdown posts!
## What's next
I will finish the Astro tutorial, and then keep adding more posts. Watch this space for more to come.

4
src/files.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.yml" {
const value: any;
export default value;
}

191
src/layouts/Layout.astro Normal file
View File

@ -0,0 +1,191 @@
---
import GlobalStyles from "../components/GlobalStyles.astro";
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 { fade } from 'astro:transitions';
import {getConfig} from "../utils/config-utils";
import {pathsEqual} from "../utils/url-utils";
interface Props {
title: string;
banner: string;
}
let { title, banner } = Astro.props;
const isHomePage = pathsEqual(Astro.url.pathname, '/') || pathsEqual(Astro.url.pathname, '/page/1');
const testPathName = Astro.url.pathname;
const anim = {
old: {
name: 'fadeIn',
duration: '4s',
easing: 'linear',
fillMode: 'forwards',
mixBlendMode: 'normal',
},
new: {
name: 'fadeOut',
duration: '4s',
easing: 'linear',
fillMode: 'backwards',
mixBlendMode: 'normal',
}
};
const myFade = {
forwards: anim,
backwards: anim,
};
// defines global css variables
// why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757
const viConf = getConfig();
const hue = viConf.appearance.hue;
if (!banner || typeof banner !== 'string' || banner.trim() === '') {
banner = viConf.banner.url;
}
---
<!DOCTYPE html>
<html lang="en" isHome={isHomePage} pathname={testPathName}>
<head>
<ViewTransitions />
<meta charset="UTF-8" />
<meta name="description" content="Astro description">
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<link rel="icon" media="(prefers-color-scheme: light)" href="/favicon/favicon-light-32.png" sizes="32x32">
<link rel="icon" media="(prefers-color-scheme: light)" href="/favicon/favicon-light-128.png" sizes="128x128">
<link rel="icon" media="(prefers-color-scheme: light)" href="/favicon/favicon-light-180.png" sizes="180x180">
<link rel="icon" media="(prefers-color-scheme: light)" href="/favicon/favicon-light-192.png" sizes="192x192">
<link rel="icon" media="(prefers-color-scheme: dark)" href="/favicon/favicon-dark-32.png" sizes="32x32">
<link rel="icon" media="(prefers-color-scheme: dark)" href="/favicon/favicon-dark-128.png" sizes="128x128">
<link rel="icon" media="(prefers-color-scheme: dark)" href="/favicon/favicon-dark-180.png" sizes="180x180">
<link rel="icon" media="(prefers-color-scheme: dark)" href="/favicon/favicon-dark-192.png" sizes="192x192">
<style define:vars={{ hue }}></style> <!-- defines global css variables -->
<title>{title}</title>
</head>
<body class="bg-[oklch(0.95_0.01_var(--hue))] dark:bg-[oklch(0.16_0.014_var(--hue))] min-h-screen transition">
<GlobalStyles>
<div class="absolute w-full"
class:list={{'banner-home': isHomePage, 'banner-else': !isHomePage}}
id="banner-wrapper"
>
<!-- TODO the transition here is not correct -->
<ImageBox id="boxtest" class="object-center object-cover h-full"
src={banner} transition:animate="fade"
>
</ImageBox>
</div>
<slot />
</GlobalStyles>
</body>
</html>
<style is:global>
:root {
--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;
}
</style>
<style>
@tailwind components;
@tailwind utilities;
@layer components {
.banner-home {
@apply h-[var(--banner-height-home)]
}
.banner-else {
@apply h-[var(--banner-height)]
}
}
#banner-wrapper {
view-transition-name: banner-ani;
}
/* i don't know how this work*/
html::view-transition-old(banner-ani) {
mix-blend-mode: normal;
animation: none;
height: 100%;
overflow: clip;
object-fit: none;
}
html::view-transition-new(banner-ani) {
mix-blend-mode: normal;
animation: none;
height: 100%;
overflow: clip;
object-fit: none;
}
.banner-home {
}
</style>
<script>
/* Preload fonts */
// (async function() {
// try {
// await Promise.all([
// document.fonts.load("400 1em Roboto"),
// document.fonts.load("700 1em Roboto"),
// ]);
// document.body.classList.remove("hidden");
// } catch (error) {
// console.log("Failed to load fonts:", error);
// }
// })();
function loadTheme() {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
loadTheme();
function setBannerHeight() {
const banner = document.getElementById('banner-wrapper');
if (document.documentElement.hasAttribute('isHome')) {
banner.classList.remove('banner-else');
banner.classList.add('banner-home');
} else {
banner.classList.remove('banner-home');
banner.classList.add('banner-else');
}
}
/* Load light/dark mode setting */
/* astro:after-swap event happened before swap animation */
document.addEventListener('astro:after-swap', () => {
setBannerHeight();
loadTheme();
}, { once: false });
</script>

View File

@ -0,0 +1,60 @@
---
import Layout from "./Layout.astro";
import Navbar from "../components/Navbar.astro";
import SideBar from "../components/widget/SideBar.astro";
import {pathsEqual} from "../utils/url-utils";
interface Props {
title: string;
banner: string;
}
const { title, banner } = Astro.props;
const isHomePage = pathsEqual(Astro.url.pathname, '/') || pathsEqual(Astro.url.pathname, '/page/1');
const pageWidth = "1200px";
const sidebarWidth = "280px";
---
<Layout title={title} banner={banner}>
<div class=`max-w-[1200px] grid grid-cols-[280px_auto] grid-auto-rows-[auto] mx-auto gap-4 relative`
transition:animate="none"
>
<div id="top-row" class="col-span-2 grid-rows-1" class:list={{
'min-h-[calc(var(--banner-height-home)_-_72px)]': isHomePage,
'min-h-[calc(var(--banner-height)_-_72px)]': !isHomePage}}
>
<Navbar transition:animate="fade" transition:persist></Navbar>
</div>
<SideBar class="max-w-[280px] col-span-1 grid-rows-2" transition:persist></SideBar>
<div class="grid-rows-2 grid-cols-2 overflow-hidden" transition:animate="slide">
<!-- the overflow-hidden here prevent long text break the layout-->
<slot></slot>
</div>
</div>
</Layout>
<style>
#top-row {
view-transition-name: rrrr;
}
/* i don't know how this work*/
html::view-transition-old(rrrr) {
mix-blend-mode: normal;
animation: none;
height: auto;
overflow: clip;
object-fit: none;
}
html::view-transition-new(rrrr) {
mix-blend-mode: normal;
animation: none;
height: auto;
overflow: clip;
object-fit: none;
}
</style>

View File

@ -0,0 +1,35 @@
---
import {getSortedPosts} from "../../../utils/content-utils";
import MainGridLayout from "../../../layouts/MainGridLayout.astro";
import ArchivePanel from "../../../components/ArchivePanel.astro";
export async function getStaticPaths() {
let posts = await getSortedPosts()
const allCategories = posts.reduce((acc, post) => {
if (!Array.isArray(post.data.categories))
return acc;
post.data.categories.forEach(category => acc.add(category));
return acc;
}, new Set());
const allCategoriesArray = Array.from(allCategories);
return allCategoriesArray.map(category => {
return {
params: {
category: category
}
}
});
}
const { category } = Astro.params;
---
<MainGridLayout>
<ArchivePanel categories={[category]}></ArchivePanel>
</MainGridLayout>

View File

@ -0,0 +1,10 @@
---
import { getCollection, getEntry } from "astro:content";
import MainGridLayout from "../../layouts/MainGridLayout.astro";
import ArchivePanel from "../../components/ArchivePanel.astro";
---
<MainGridLayout>
<ArchivePanel></ArchivePanel>
</MainGridLayout>

View File

@ -0,0 +1,34 @@
---
import {getSortedPosts} from "../../../utils/content-utils";
import MainGridLayout from "../../../layouts/MainGridLayout.astro";
import ArchivePanel from "../../../components/ArchivePanel.astro";
export async function getStaticPaths() {
let posts = await getSortedPosts()
const allTags = posts.reduce((acc, post) => {
post.data.tags.forEach(tag => acc.add(tag));
return acc;
}, new Set());
const allTagsArray = Array.from(allTags);
return allTagsArray.map(tag => {
return {
params: {
tag: tag
}
}
});
}
const { tag } = Astro.params;
---
<MainGridLayout>
<ArchivePanel tags={[tag]}></ArchivePanel>
</MainGridLayout>

3
src/pages/index.astro Normal file
View File

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

View File

@ -0,0 +1,41 @@
---
import { getCollection, getEntry } from "astro:content";
import MainGridLayout from "../../layouts/MainGridLayout.astro";
import TitleCard from "../../components/TitleCardNew.astro";
import Pagination from "../../components/control/Pagination.astro";
import {getPostUrlBySlug, getSortedPosts} from "../../utils/content-utils";
import {getConfig} from "../../utils/config-utils";
export async function getStaticPaths({ paginate }) {
// const allBlogPosts = await getCollection("posts");
const allBlogPosts = await getSortedPosts();
return paginate(allBlogPosts, { pageSize: 6 });
}
const {page} = Astro.props;
// page.data.map(entry => console.log(entry));
// console.log(page)
// console.log(getConfig());
---
<!-- 显示当前页面。也可以使用 Astro.params.page -->
<MainGridLayout>
<div class="flex flex-col gap-4 mb-4">
{page.data.map(entry =>
<TitleCard
entry={entry}
title={entry.data.title}
tags={entry.data.tags}
categories={entry.data.categories}
pubDate={entry.data.pubDate}
url={getPostUrlBySlug(entry.slug)}
cover={entry.data.cover}
description={entry.data.description}
></TitleCard>
)}
</div>
<Pagination class="mx-auto" page={page}></Pagination>
</MainGridLayout>

View File

@ -0,0 +1,73 @@
---
import { getCollection } from 'astro:content';
import MainGridLayout from "../../layouts/MainGridLayout.astro";
import ButtonTag from "../../components/control/ButtonTag.astro";
import ImageBox from "../../components/misc/ImageBox.astro";
import {Icon} from "astro-icon/components";
import {formatDateToYYYYMMDD} from "../../utils/date-utils";
import PostMetadata from "../../components/PostMetadata.astro";
// 1. 为每个集合条目生成一个新路径
export async function getStaticPaths() {
const blogEntries = await getCollection('posts');
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}
// 2. 当渲染的时候,你可以直接从属性中得到条目
const { entry } = Astro.props;
const { Content } = await entry.render();
const { remarkPluginFrontmatter } = await entry.render();
---
<MainGridLayout banner={entry.data.cover}>
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative">
<div class:list={["card-base z-10 px-9 py-6 relative w-full ",
{}
]}>
<div class="flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3 transition">
<div class="flex flex-row items-center">
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/5 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
<Icon name="material-symbols:notes-rounded"></Icon>
</div>
<div class="text-sm">{remarkPluginFrontmatter.words} words</div>
</div>
<div class="flex flex-row items-center">
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/5 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
<Icon name="material-symbols:schedule-outline-rounded"></Icon>
</div>
<div class="text-sm">{remarkPluginFrontmatter.minutes} minutes</div>
</div>
</div>
<div class="relative">
<div
class="transition w-full block font-bold mb-3 text-4xl
text-black/90 dark:text-white/90
before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
before:absolute before:top-[10px] before:left-[-18px]
">
{entry.data.title}
</div>
</div>
<PostMetadata
class="mb-5"
pubDate={entry.data.pubDate}
tags={entry.data.tags}
categories={entry.data.categories}
></PostMetadata>
<div class="border-b-black/8 dark:border-b-white/8 border-dashed border-b-[1px] mb-5"></div>
<div class="prose dark:prose-invert max-w-none prose-h1:text-3xl">
<Content />
</div>
</div>
</div>
</MainGridLayout>

View File

@ -0,0 +1,11 @@
import getReadingTime from 'reading-time';
import { toString } from 'mdast-util-to-string';
export function remarkReadingTime() {
return function (tree, { data }) {
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
data.astro.frontmatter.minutes = Math.max(1, Math.round(readingTime.minutes));
data.astro.frontmatter.words = readingTime.words;
};
}

37
src/utils/config-utils.ts Normal file
View File

@ -0,0 +1,37 @@
import _config from '../../vivia.config.yml';
interface ViviaConfig {
menu: {
[key: string]: string;
};
appearance: {
hue: number;
};
favicon: string;
banner: {
enable: boolean;
url: string;
position: string;
onAllPages: boolean;
};
sidebar: {
widgets: {
normal: string | string[];
sticky: string | string[];
};
};
profile: {
avatar: string;
author: string;
subtitle: string;
links: {
name: string;
icon: string;
url: string;
}[];
}
}
const config: ViviaConfig = _config;
export const getConfig = () => config;

View File

@ -0,0 +1,32 @@
import {getCollection} from "astro:content";
export async function getSortedPosts() {
const allBlogPosts = await getCollection("posts");
return allBlogPosts.sort((a, b) => {
const dateA = new Date(a.data.pubDate);
const dateB = new Date(b.data.pubDate);
return dateA > dateB ? -1 : 1;
});
}
export function getPostUrlBySlug(slug: string): string {
return `/posts/${slug}`;
}
export async function getTagList(): Promise<{ name: string; count: number }[]> {
const allBlogPosts = await getCollection("posts");
const countMap: { [key: string]: number } = {};
allBlogPosts.map((post) => {
post.data.tags.map((tag: string) => {
if (!countMap[tag]) countMap[tag] = 0;
countMap[tag]++;
})
});
// 获取对象的所有键并按字典顺序排序
const keys: string[] = Object.keys(countMap).sort();
// 使用排序后的键构建包含 key 和 value 的数组
return keys.map((key) => ({name: key, count: countMap[key]}));
}

7
src/utils/date-utils.ts Normal file
View File

@ -0,0 +1,7 @@
export function formatDateToYYYYMMDD(date: Date): string {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}

5
src/utils/url-utils.ts Normal file
View File

@ -0,0 +1,5 @@
export function pathsEqual(path1: string, path2: string) {
const normalizedPath1 = path1.replace(/^\/|\/$/g, '').toLowerCase();
const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase();
return normalizedPath1 === normalizedPath2;
}

16
tailwind.config.cjs Normal file
View File

@ -0,0 +1,16 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
darkMode: 'class', // allows toggling dark mode manually
theme: {
extend: {
fontFamily: {
sans: ['Roboto', 'sans-serif', ...defaultTheme.fontFamily.sans],
}
},
},
plugins: [
require('@tailwindcss/typography'),
],
}

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"strictNullChecks": true,
"allowJs": true,
"plugins": [
{
"name": "@astrojs/ts-plugin"
}
]
}
}

9
vercel.json Normal file
View File

@ -0,0 +1,9 @@
{
"redirects": [
{
"source": "/",
"destination": "/page/1",
"statusCode": 307
}
]
}

20
vivia.config.yml Normal file
View File

@ -0,0 +1,20 @@
appearance:
hue: 290
banner:
url: https://saicaca.github.io/vivia-preview/assets/banner.jpg
profile:
avatar: https://saicaca.github.io/vivia-preview/assets/avatar.jpg
author: Your Name
subtitle: This is the subtitle
links:
- name: Twitter
icon: fa6-brands:twitter
url: https://twitter.com
- name: Steam
icon: fa6-brands:steam
url: https://store.steampowered.com
- name: GitHub
icon: fa6-brands:github
url: https://github.com