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 | ||||
| ``` | ||||
| 
 | ||||
| [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) | ||||
| [](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) | ||||
| [](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) | ||||
| 
 | ||||
| > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun! | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 🚀 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 | ||||