feat: initial commit
(cherry picked from commit 44c4d7b9521fe449e61edc614446195861932f8c)
|
@ -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
|
|
@ -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).
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 426 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 554 B |
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
<div class="rounded-2xl drop-shadow-2xl bg-white">
|
||||||
|
<slot />
|
||||||
|
</div>
|
|
@ -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>
|
|
@ -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="">→</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||||
|
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
||||||
|
declare module "*.yml" {
|
||||||
|
const value: any;
|
||||||
|
export default value;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
|
@ -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]}));
|
||||||
|
}
|
|
@ -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}`;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "@astrojs/ts-plugin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"redirects": [
|
||||||
|
{
|
||||||
|
"source": "/",
|
||||||
|
"destination": "/page/1",
|
||||||
|
"statusCode": 307
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|