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 | ||||||