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
|