mirror of
https://github.com/saicaca/fuwari.git
synced 2026-01-11 23:02:53 +01:00
feat: initial commit
(cherry picked from commit 44c4d7b9521fe449e61edc614446195861932f8c)
This commit is contained in:
121
src/components/ArchivePanel.astro
Normal file
121
src/components/ArchivePanel.astro
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
interface Props {
|
||||
keyword: string;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
}
|
||||
const { keyword, tags, categories} = Astro.props;
|
||||
|
||||
import Button from "./control/Button.astro";
|
||||
import {getPostUrlBySlug, getSortedPosts} from "../utils/content-utils";
|
||||
|
||||
let posts = await getSortedPosts()
|
||||
|
||||
if (Array.isArray(tags) && tags.length > 0) {
|
||||
posts = posts.filter(post =>
|
||||
Array.isArray(post.data.tags) && post.data.tags.some(tag => tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(categories) && categories.length > 0) {
|
||||
posts = posts.filter(post =>
|
||||
Array.isArray(post.data.categories) && post.data.categories.some(category => categories.includes(category))
|
||||
);
|
||||
}
|
||||
|
||||
const groups = function () {
|
||||
const groupedPosts = posts.reduce((grouped, post) => {
|
||||
const year = post.data.pubDate.getFullYear()
|
||||
if (!grouped[year]) {
|
||||
grouped[year] = []
|
||||
}
|
||||
grouped[year].push(post)
|
||||
return grouped
|
||||
}, {})
|
||||
|
||||
// convert the object to an array
|
||||
const groupedPostsArray = Object.keys(groupedPosts).map(key => ({
|
||||
year: key,
|
||||
posts: groupedPosts[key]
|
||||
}))
|
||||
|
||||
// sort years by latest first
|
||||
groupedPostsArray.sort((a, b) => b.year - a.year)
|
||||
return groupedPostsArray;
|
||||
}();
|
||||
|
||||
function formatDate(date: Date) {
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
return `${month}-${day}`;
|
||||
}
|
||||
|
||||
|
||||
// console.log(groups)
|
||||
|
||||
|
||||
---
|
||||
|
||||
<div class="card-base px-8 py-6">
|
||||
{
|
||||
groups.map(group => (
|
||||
<div>
|
||||
<div class="flex flex-row w-full items-center h-[60px]">
|
||||
<div class="w-[10%] transition text-2xl font-bold text-right text-black/75 dark:text-white/75">{group.year}</div>
|
||||
<div class="w-[10%]">
|
||||
<div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div>
|
||||
</div>
|
||||
<div class="w-[80%] transition text-left text-black/50 dark:text-white/50">{group.posts.length} Articles</div>
|
||||
</div>
|
||||
{group.posts.map(post => (
|
||||
<a href={getPostUrlBySlug(post.slug)} class="group">
|
||||
<Button light height="40px" class="w-full hover:text-[initial]">
|
||||
<div class="flex flex-row justify-start items-center h-full">
|
||||
<!-- date -->
|
||||
<div class="w-[10%] transition text-sm text-right text-black/50 dark:text-white/50">{formatDate(post.data.pubDate)}</div>
|
||||
<!-- dot and line -->
|
||||
<div class="w-[10%] relative dash-line h-full flex items-center">
|
||||
<div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
|
||||
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
|
||||
outline outline-4 z-50
|
||||
outline-[var(--card-bg)]
|
||||
group-hover:outline-[var(--btn-plain-bg-hover)]
|
||||
group-active:outline-[var(--btn-plain-bg-active)]
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
<!-- post title -->
|
||||
<div class="max-w-[65%] w-[65%] transition text-left font-bold">
|
||||
<div class="group-hover:ml-1 transition-all group-hover:text-[var(--primary)]
|
||||
text-black/80 dark:text-white/80 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden">
|
||||
{post.data.title}
|
||||
</div>
|
||||
</div>
|
||||
<!-- tag list -->
|
||||
<div class="w-[15%] text-left text-sm transition
|
||||
whitespace-nowrap overflow-ellipsis overflow-hidden
|
||||
text-black/30 dark:text-white/30"
|
||||
>#Test #Markdown</div>
|
||||
</div>
|
||||
</Button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.dash-line {
|
||||
}
|
||||
.dash-line::before {
|
||||
content: "";
|
||||
@apply w-[10%] h-full absolute -top-1/2 left-[calc(50%_-_1px)] -top-[50%] border-l-[2px]
|
||||
border-dashed pointer-events-none border-[var(--line-color)] transition
|
||||
}
|
||||
}
|
||||
</style>
|
||||
6
src/components/BasicCard.astro
Normal file
6
src/components/BasicCard.astro
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
|
||||
---
|
||||
<div class="rounded-2xl drop-shadow-2xl bg-white">
|
||||
<slot />
|
||||
</div>
|
||||
7
src/components/BtnLightIcon.astro
Normal file
7
src/components/BtnLightIcon.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import ButtonLight from "./control/Button.astro";
|
||||
---
|
||||
<ButtonLight class="fill-black">
|
||||
<Icon name="material-symbols:nightlight-badge-outline" class="w-6 h-6"/>
|
||||
</ButtonLight>
|
||||
60
src/components/Card.astro
Normal file
60
src/components/Card.astro
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
body: string;
|
||||
href: string;
|
||||
}
|
||||
const { href, title, body } = Astro.props;
|
||||
---
|
||||
|
||||
<li class="link-card">
|
||||
<a href={href}>
|
||||
<h2>
|
||||
{title}
|
||||
<span class="">→</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>
|
||||
92
src/components/GlobalStyles.astro
Normal file
92
src/components/GlobalStyles.astro
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<div>
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<style is:global lang="stylus">
|
||||
|
||||
/* utils */
|
||||
white(a)
|
||||
rgba(255, 255, 255, a)
|
||||
|
||||
black(a)
|
||||
rgba(0, 0, 0, a)
|
||||
|
||||
isOklch(c)
|
||||
return substr(c, 0, 5) == 'oklch'
|
||||
|
||||
oklch_fallback(c)
|
||||
str = '' + c // convert color value to string
|
||||
if isOklch(str)
|
||||
return convert(oklchToHex(str))
|
||||
return c
|
||||
|
||||
color_set(colors)
|
||||
@supports (color: oklch(0 0 0))
|
||||
:root
|
||||
for key, value in colors
|
||||
{key}: value[0]
|
||||
:root.dark
|
||||
for key, value in colors
|
||||
if length(value) > 1
|
||||
{key}: value[1]
|
||||
/* provide fallback color for oklch */
|
||||
@supports not (color: oklch(0 0 0))
|
||||
:root
|
||||
for key, value in colors
|
||||
{key}: oklch_fallback(value[0])
|
||||
:root.dark
|
||||
for key, value in colors
|
||||
if length(value) > 1
|
||||
{key}: oklch_fallback(value[1])
|
||||
|
||||
:root
|
||||
--radius-large 16px
|
||||
|
||||
--banner-height-home 60vh
|
||||
--banner-height 50vh
|
||||
|
||||
color_set({
|
||||
--primary: oklch(0.70 0.14 var(--hue))
|
||||
--card-bg: white oklch(0.25 0.02 var(--hue))
|
||||
|
||||
--btn-content: oklch(0.55 0.12 var(--hue))
|
||||
|
||||
--btn-regular-bg: oklch(0.95 0.025 var(--hue)) oklch(0.38 0.04 var(--hue))
|
||||
|
||||
--btn-plain-bg-hover: oklch(0.95 0.025 var(--hue)) oklch(0.2 0.02 var(--hue))
|
||||
--btn-plain-bg-active: oklch(0.98 0.01 var(--hue)) oklch(0.17 0.017 var(--hue))
|
||||
|
||||
--btn-card-bg-hover: oklch(0.96 0.015 var(--hue)) oklch(0.3 0.03 var(--hue))
|
||||
--btn-card-bg-active: oklch(0.9 0.03 var(--hue)) oklch(0.35 0.035 var(--hue))
|
||||
|
||||
--deep-text: oklch(0.25 0.02 var(--hue))
|
||||
|
||||
--line-color: black(0.1) white(0.1)
|
||||
--meta-divider: black(0.2) white(0.2)
|
||||
--selection-bg: oklch(0.90 0.05 var(--hue)) oklch(0.40 0.08 var(--hue))
|
||||
})
|
||||
|
||||
|
||||
/* some global styles */
|
||||
::selection
|
||||
background-color: var(--selection-bg)
|
||||
|
||||
</style>
|
||||
<style is:global>
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.card-base {
|
||||
@apply rounded-[var(--radius-large)] overflow-hidden bg-[var(--card-bg)] transition;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6, p, a, span, li, ul, ol, blockquote, code, pre, table, th, td, strong {
|
||||
@apply transition;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
src/components/Navbar.astro
Normal file
60
src/components/Navbar.astro
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
import Button from "./control/Button.astro";
|
||||
import { Icon } from 'astro-icon/components';
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
<div class:list={[
|
||||
className,
|
||||
"card-base max-w-[var(--page-width)] h-[72px] rounded-t-none mx-auto flex items-center justify-between px-4"]}>
|
||||
<a href="/"><Button height="52px" class="px-5 font-bold" light>
|
||||
<div class="flex flex-row text-[var(--primary)] items-center text-md">
|
||||
<Icon name="material-symbols:home-outline-rounded" size={28} class="mb-1 mr-2" />
|
||||
<div class="top-2"></div>Vivia Preview
|
||||
</div>
|
||||
</Button></a>
|
||||
<div>
|
||||
<a href="/"><Button light class="font-bold px-5">Home</Button></a>
|
||||
<a href="/archive"><Button light class="font-bold px-5">Archive</Button></a>
|
||||
<Button light class="font-bold px-5">About</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button id="scheme-switch" iconName="material-symbols:wb-sunny-outline-rounded" iconSize={20} isIcon light></Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style lang="stylus">
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
function switchTheme() {
|
||||
if (localStorage.theme === 'dark') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
function loadThemeSwitchScript() {
|
||||
let switchBtn = document.getElementById("scheme-switch");
|
||||
if (switchBtn === null) {
|
||||
console.log("test")
|
||||
}
|
||||
switchBtn.addEventListener("click", function () {
|
||||
console.log("test")
|
||||
switchTheme()
|
||||
});
|
||||
}
|
||||
|
||||
loadThemeSwitchScript();
|
||||
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
loadThemeSwitchScript();
|
||||
}, { once: false });
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
78
src/components/PostMetadata.astro
Normal file
78
src/components/PostMetadata.astro
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
import {formatDateToYYYYMMDD} from "../utils/date-utils";
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
class: string;
|
||||
pubDate: Date;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
}
|
||||
const {pubDate, tags, categories} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
|
||||
<div class:list={["flex flex-wrap text-neutral-500 dark:text-neutral-400 items-center gap-4 gap-x-4 gap-y-3", className]}>
|
||||
<!-- publish date -->
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<span class="text-black/50 dark:text-white/50 text-sm font-medium">{formatDateToYYYYMMDD(pubDate)}</span>
|
||||
</div>
|
||||
|
||||
<!-- categories -->
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:menu-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
{categories && categories.map(category => <div
|
||||
class="with-divider"
|
||||
>
|
||||
<a href=`/archive/category/${category}`
|
||||
class="transition text-black/50 dark:text-white/50 text-sm font-medium
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]">
|
||||
{category}
|
||||
</a>
|
||||
</div>)}
|
||||
{!categories && <div class="transition text-black/50 dark:text-white/50 text-sm font-medium">Uncategorized</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tags -->
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:tag-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
{tags.map(tag => <div
|
||||
class="with-divider"
|
||||
>
|
||||
<a href=`/archive/tag/${tag}`
|
||||
class="transition text-black/50 dark:text-white/50 text-sm font-medium
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]">
|
||||
{tag}
|
||||
</a>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.meta-icon {
|
||||
@apply w-8 h-8 transition rounded-md flex items-center justify-center bg-[var(--btn-regular-bg)]
|
||||
text-[var(--btn-content)] dark:text-[var(--primary)] mr-2
|
||||
}
|
||||
.with-divider {
|
||||
@apply before:content-['/'] before:mx-[6px] before:text-[var(--meta-divider)] before:text-sm
|
||||
before:font-medium before:first-of-type:hidden before:transition
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
src/components/TitleCard.astro
Normal file
83
src/components/TitleCard.astro
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
import {formatDateToYYYYMMDD} from "../utils/date-utils";
|
||||
interface Props {
|
||||
title: string;
|
||||
url: string;
|
||||
pubDate: Date;
|
||||
tags: string[];
|
||||
cover: string;
|
||||
description: string;
|
||||
words: number;
|
||||
}
|
||||
const { title, url, pubDate, tags, cover, description, words } = Astro.props;
|
||||
// console.log(Astro.props);
|
||||
import ImageBox from "./misc/ImageBox.astro";
|
||||
import ButtonTag from "./control/ButtonTag.astro";
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
// tags = ['Foo', 'Bar', 'Baz', 'Qux', 'Quux'];
|
||||
|
||||
// const cover = 'https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg';
|
||||
// cover = null;
|
||||
const hasCover = cover !== undefined && cover !== null && cover !== '';
|
||||
|
||||
|
||||
---
|
||||
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative">
|
||||
<div class:list={["card-base z-30 px-8 py-6 relative ",
|
||||
{
|
||||
'w-[calc(70%_+_var(--radius-large))]': hasCover,
|
||||
'w-[calc(100%_-_76px_+_var(--radius-large))]': !hasCover,
|
||||
}
|
||||
]}>
|
||||
<a href={url}
|
||||
class="transition w-full block font-bold mb-1 text-3xl
|
||||
text-neutral-900 dark:text-neutral-100
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
|
||||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:top-8 before:left-4
|
||||
">
|
||||
This is a very long title
|
||||
</a>
|
||||
<div class="flex text-neutral-500 dark:text-neutral-400 items-center mb-1">
|
||||
<div>{formatDateToYYYYMMDD(pubDate)}</div>
|
||||
<div class="transition h-1 w-1 rounded-sm bg-neutral-400 dark:bg-neutral-600 mx-3"></div>
|
||||
<div>Uncategorized</div>
|
||||
<div class="transition h-1 w-1 rounded-sm bg-neutral-400 dark:bg-neutral-600 mx-3"></div>
|
||||
<div>{words} words</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-4">
|
||||
{tags.map(t => (
|
||||
<ButtonTag dot>{t}</ButtonTag>
|
||||
))}
|
||||
</div>
|
||||
<div class="transition text-neutral-700 dark:text-neutral-300">This is the description of the article</div>
|
||||
|
||||
</div>
|
||||
{!hasCover && <a href={url}
|
||||
class="transition w-[72px]
|
||||
bg-[var(--btn-enter-bg)] dark:bg-[var(--btn-enter-bg-dark)]
|
||||
hover:bg-[var(--btn-card-bg-hover)] active:bg-[var(--btn-card-bg-active)]
|
||||
absolute top-0 bottom-0 right-0 flex items-center">
|
||||
<Icon name="material-symbols:chevron-right-rounded"
|
||||
class="transition text-4xl text-[var(--primary)] ml-[22px]"></Icon>
|
||||
</a>}
|
||||
|
||||
{hasCover && <a href={url}
|
||||
class="group w-[30%] absolute top-0 bottom-0 right-0">
|
||||
<div class="absolute z-10 w-full h-full group-hover:bg-black/30 group-active:bg-black/50 transition"></div>
|
||||
<div class="absolute z-20 w-full h-full flex items-center justify-center ">
|
||||
<Icon name="material-symbols:chevron-right-rounded"
|
||||
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl"></Icon>
|
||||
</div>
|
||||
<ImageBox src="https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg"
|
||||
class="w-full h-full">
|
||||
</ImageBox>
|
||||
</a>}
|
||||
</div>
|
||||
|
||||
<style lang="stylus">
|
||||
:root
|
||||
--btn-enter-bg oklch(0.98 0.005 var(--hue))
|
||||
--btn-enter-bg-dark oklch(0.2 0.02 var(--hue))
|
||||
</style>
|
||||
78
src/components/TitleCardNew.astro
Normal file
78
src/components/TitleCardNew.astro
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
import PostMetadata from "./PostMetadata.astro";
|
||||
interface Props {
|
||||
class: string;
|
||||
entry: any;
|
||||
title: string;
|
||||
url: string;
|
||||
pubDate: Date;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
cover: string;
|
||||
description: string;
|
||||
words: number;
|
||||
}
|
||||
const { entry, title, url, pubDate, tags, categories, cover, description, words } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
// console.log(Astro.props);
|
||||
import ImageBox from "./misc/ImageBox.astro";
|
||||
import ButtonTag from "./control/ButtonTag.astro";
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
// tags = ['Foo', 'Bar', 'Baz', 'Qux', 'Quux'];
|
||||
|
||||
// const cover = 'https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg';
|
||||
// cover = null;
|
||||
const hasCover = cover !== undefined && cover !== null && cover !== '';
|
||||
|
||||
const coverWidth = "30%";
|
||||
|
||||
const { remarkPluginFrontmatter } = await entry.render();
|
||||
|
||||
---
|
||||
<div class:list={["card-base flex w-full rounded-[var(--radius-large)] overflow-hidden relative", className]}>
|
||||
<div class:list={[" px-10 pt-7 pb-6 relative", {'w-full': !hasCover, "w-[calc(100%_-_var(--coverWidth))]": hasCover}]}>
|
||||
<a href={url}
|
||||
class="transition w-full block font-bold mb-3 text-4xl
|
||||
text-black/90 dark:text-white/90
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)]
|
||||
before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:top-[38px] before:left-5
|
||||
">
|
||||
{title}
|
||||
</a>
|
||||
|
||||
<!-- metadata -->
|
||||
<PostMetadata pubDate={pubDate} tags={tags} categories={categories} class="mb-4"></PostMetadata>
|
||||
|
||||
<div class="transition text-black/75 dark:text-white/75 mb-4">
|
||||
{ description }
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
|
||||
<div>{remarkPluginFrontmatter.words} words</div>
|
||||
<div>|</div>
|
||||
<div>{remarkPluginFrontmatter.minutes} minutes</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{hasCover && <a href={url}
|
||||
class=`group w-[var(--coverWidth)] absolute top-3 bottom-3 right-3 rounded-xl overflow-hidden`>
|
||||
<div class="absolute z-10 w-full h-full group-hover:bg-black/30 group-active:bg-black/50 transition"></div>
|
||||
<div class="absolute z-20 w-full h-full flex items-center justify-center ">
|
||||
<Icon name="material-symbols:chevron-right-rounded"
|
||||
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl"></Icon>
|
||||
</div>
|
||||
<ImageBox src="https://saicaca.github.io/vivia-preview/assets/79905307_p0.jpg"
|
||||
class="w-full h-full">
|
||||
</ImageBox>
|
||||
</a>}
|
||||
</div>
|
||||
|
||||
<style lang="stylus" define:vars={{coverWidth}}>
|
||||
:root
|
||||
--btn-enter-bg oklch(0.98 0.005 var(--hue))
|
||||
--btn-enter-bg-dark oklch(0.2 0.02 var(--hue))
|
||||
|
||||
</style>
|
||||
16
src/components/WidgetCard.astro
Normal file
16
src/components/WidgetCard.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
interface Props {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const {name} = Astro.props;
|
||||
|
||||
import BasicCard from "./BasicCard.astro";
|
||||
|
||||
---
|
||||
|
||||
<BasicCard >
|
||||
<div class="p-4">
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
</BasicCard>
|
||||
77
src/components/control/Button.astro
Normal file
77
src/components/control/Button.astro
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
interface Props {
|
||||
id?: string;
|
||||
isIcon?: boolean;
|
||||
iconName?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
regular?: boolean;
|
||||
light?: boolean
|
||||
card?: boolean;
|
||||
iconSize?: number,
|
||||
class?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
const props = Astro.props;
|
||||
const {
|
||||
id,
|
||||
isIcon = false,
|
||||
iconName,
|
||||
width,
|
||||
height = '44px',
|
||||
regular,
|
||||
light,
|
||||
iconSize = 24,
|
||||
card,
|
||||
disabled = false,
|
||||
} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
import { Icon } from 'astro-icon/components';
|
||||
---
|
||||
|
||||
<button id={id}
|
||||
disabled={disabled}
|
||||
class:list={[
|
||||
className,
|
||||
`
|
||||
rounded-lg
|
||||
transition
|
||||
h-[var(--height)]
|
||||
`,
|
||||
{
|
||||
'w-[var(--width)]': width,
|
||||
'w-[var(--height)]': isIcon,
|
||||
|
||||
'bg-none': light,
|
||||
'hover:bg-[var(--btn-plain-bg-hover)]': light,
|
||||
'active:bg-[var(--btn-plain-bg-active)]': light,
|
||||
'text-neutral-900': light,
|
||||
'hover:text-[var(--primary)]': light,
|
||||
|
||||
'dark:text-neutral-300': light || regular,
|
||||
'dark:hover:text-[var(--primary)]': light,
|
||||
|
||||
'bg-[var(--btn-regular-bg)]': regular,
|
||||
'hover:bg-[oklch(0.9_0.05_var(--hue))]': regular,
|
||||
'active:bg-[oklch(0.85_0.08_var(--hue))]': regular,
|
||||
'text-[var(--btn-content)]': regular,
|
||||
|
||||
'dark:bg-[oklch(0.38_0.04_var(--hue))]': regular,
|
||||
'dark:hover:bg-[oklch(0.45_0.045_var(--hue))]': regular,
|
||||
'dark:active:bg-[oklch(0.5_0.05_var(--hue))]': regular,
|
||||
|
||||
'card-base': card,
|
||||
'enabled:hover:bg-[var(--btn-card-bg-hover)]': card,
|
||||
'enabled:active:bg-[var(--btn-card-bg-active)]': card,
|
||||
'disabled:text-black/10': card,
|
||||
'disabled:dark:text-white/10': card,
|
||||
}
|
||||
]}
|
||||
>
|
||||
{props.isIcon && <Icon class="mx-auto" name={props.iconName} size={iconSize}></Icon> }
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<style define:vars={{ height, width, iconSize }}>
|
||||
|
||||
</style>
|
||||
42
src/components/control/ButtonLink.astro
Normal file
42
src/components/control/ButtonLink.astro
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
interface Props {
|
||||
badge?: string
|
||||
url?: string
|
||||
}
|
||||
const { badge, url } = Astro.props
|
||||
---
|
||||
<a href={url}>
|
||||
<button
|
||||
class:list={`
|
||||
w-full
|
||||
h-10
|
||||
rounded-lg
|
||||
bg-none
|
||||
hover:bg-[var(--btn-plain-bg-hover)]
|
||||
active:bg-[var(--btn-plain-bg-active)]
|
||||
transition-all
|
||||
pl-2
|
||||
hover:pl-3
|
||||
|
||||
text-neutral-700
|
||||
hover:text-[var(--primary)]
|
||||
dark:text-neutral-300
|
||||
dark:hover:text-[var(--primary)]
|
||||
`
|
||||
}
|
||||
>
|
||||
<div class="flex items-center justify-between relative mr-2">
|
||||
<div class="overflow-hidden text-left whitespace-nowrap overflow-ellipsis ">
|
||||
<slot></slot>
|
||||
</div>
|
||||
{ badge !== undefined && badge !== null && badge !== '' &&
|
||||
<div class="transition h-[28px] ml-4 min-w-[32px] rounded-lg text-sm font-bold
|
||||
text-[var(--btn-content)] dark:text-[var(--deep-text)]
|
||||
bg-[oklch(0.95_0.025_var(--hue))] dark:bg-[var(--primary)]
|
||||
flex items-center justify-center">
|
||||
{ badge }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
15
src/components/control/ButtonTag.astro
Normal file
15
src/components/control/ButtonTag.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import Button from "./Button.astro";
|
||||
interface Props {
|
||||
size?: string;
|
||||
dot?: boolean;
|
||||
href?: string;
|
||||
}
|
||||
const { size, dot, href }: Props = Astro.props;
|
||||
---
|
||||
<a href={href}>
|
||||
<Button regular height="32px" class="text-[15px] px-3 flex flex-row items-center">
|
||||
{dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>}
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</a>
|
||||
84
src/components/control/Pagination.astro
Normal file
84
src/components/control/Pagination.astro
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
import type { Page } from "astro";
|
||||
import { Icon } from 'astro-icon/components';
|
||||
interface Props {
|
||||
page: Page;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {page} = Astro.props;
|
||||
|
||||
const HIDDEN = -1;
|
||||
|
||||
const className = Astro.props.class;
|
||||
import Button from "./Button.astro";
|
||||
|
||||
const ADJ_DIST = 2;
|
||||
const VISIBLE = ADJ_DIST * 2 + 1;
|
||||
|
||||
// for test
|
||||
let count = 1;
|
||||
let l = page.currentPage, r = page.currentPage;
|
||||
while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) {
|
||||
count += 2;
|
||||
l--;
|
||||
r++;
|
||||
}
|
||||
while (0 < l - 1 && count < VISIBLE) {
|
||||
count++;
|
||||
l--;
|
||||
}
|
||||
while (r + 1 <= page.lastPage && count < VISIBLE) {
|
||||
count++;
|
||||
r++;
|
||||
}
|
||||
|
||||
let pages: number[] = [];
|
||||
if (l > 1)
|
||||
pages.push(1);
|
||||
if (l == 3)
|
||||
pages.push(2);
|
||||
if (l > 3)
|
||||
pages.push(HIDDEN);
|
||||
for (let i = l; i <= r; i++)
|
||||
pages.push(i);
|
||||
if (r < page.lastPage - 2)
|
||||
pages.push(HIDDEN);
|
||||
if (r == page.lastPage - 2)
|
||||
pages.push(page.lastPage - 1);
|
||||
if (r < page.lastPage)
|
||||
pages.push(page.lastPage);
|
||||
|
||||
const parts: string[] = page.url.current.split('/');
|
||||
const commonUrl: string = parts.slice(0, -1).join('/') + '/';
|
||||
---
|
||||
|
||||
<div class:list={[className, "flex flex-row gap-3 justify-center"]}>
|
||||
<a href={page.url.prev}>
|
||||
<Button isIcon card iconName="material-symbols:chevron-left-rounded" class="text-[var(--primary)]" iconSize={28}
|
||||
disabled = {page.url.prev == undefined}
|
||||
></Button>
|
||||
</a>
|
||||
<div class="bg-[var(--card-bg)] flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold">
|
||||
{pages.map((p) => {
|
||||
if (p == HIDDEN)
|
||||
return <Icon name="material-symbols:more-horiz" class="mx-1"/>;
|
||||
if (p == page.currentPage)
|
||||
return <div class="h-[44px] w-[44px] rounded-lg bg-[var(--primary)] flex items-center justify-center
|
||||
font-bold text-white dark:text-black/70"
|
||||
>
|
||||
{p}
|
||||
</div>
|
||||
return <a href={commonUrl + p}>
|
||||
<Button card iconName="material-symbols:chevron-left-rounded" height="44px" width="44px">
|
||||
{p}
|
||||
</Button>
|
||||
</a>
|
||||
})}
|
||||
</div>
|
||||
<a href={page.url.next}>
|
||||
<Button isIcon card iconName="material-symbols:chevron-right-rounded" class="text-[var(--primary)]" iconSize={28}
|
||||
disabled = {page.url.next == undefined}
|
||||
></Button>
|
||||
</a>
|
||||
</div>
|
||||
15
src/components/misc/ImageBox.astro
Normal file
15
src/components/misc/ImageBox.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
interface Props {
|
||||
id?: string
|
||||
src: string;
|
||||
class?: string;
|
||||
alt?: string
|
||||
}
|
||||
const {id, src, alt} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
<div class:list={[className, 'overflow-hidden relative']}>
|
||||
<div class="transition absolute top-0 bottom-0 left-0 right-0 dark:bg-black/10 bg-opacity-50"></div>
|
||||
<img src={src} alt={alt} class="w-full h-full object-center object-cover">
|
||||
</div>
|
||||
|
||||
26
src/components/widget/Profile.astro
Normal file
26
src/components/widget/Profile.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import ImageBox from "../misc/ImageBox.astro";
|
||||
import ButtonLight from "../control/Button.astro";
|
||||
import {getConfig} from "../../utils/config-utils";
|
||||
interface props {
|
||||
|
||||
}
|
||||
const className = Astro.props
|
||||
|
||||
const vConf = getConfig();
|
||||
|
||||
---
|
||||
<div class="card-base" transition:persist>
|
||||
<ImageBox src={vConf.profile.avatar} class="w-full rounded-2xl mb-3"></ImageBox>
|
||||
<div class="font-bold text-lg text-center mb-1 dark:text-neutral-50 transition">{vConf.profile.author}</div>
|
||||
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-3 transition"></div>
|
||||
<div class="text-center text-neutral-400 mb-2 transition">{vConf.profile.subtitle}</div>
|
||||
<div class="flex gap-2 mx-2 justify-center mb-4">
|
||||
{vConf.profile.links.map(item =>
|
||||
<a href={item.url} target="_blank">
|
||||
<ButtonLight isIcon iconName={item.icon} regular height="40px"></ButtonLight>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
18
src/components/widget/RecentPost.astro
Normal file
18
src/components/widget/RecentPost.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import WidgetLayout from "./WidgetLayout.astro";
|
||||
import ButtonLink from "../control/ButtonLink.astro";
|
||||
import {getPostUrlBySlug, getSortedPosts} from "../../utils/content-utils";
|
||||
|
||||
let posts = await getSortedPosts()
|
||||
|
||||
const LIMIT = 5;
|
||||
|
||||
posts = posts.slice(0, LIMIT)
|
||||
|
||||
// console.log(posts)
|
||||
---
|
||||
<WidgetLayout name="Recent Posts">
|
||||
{posts.map(post =>
|
||||
<ButtonLink url={getPostUrlBySlug(post.slug)}>{post.data.title}</ButtonLink>
|
||||
)}
|
||||
</WidgetLayout>
|
||||
27
src/components/widget/SideBar.astro
Normal file
27
src/components/widget/SideBar.astro
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import Profile from "./Profile.astro";
|
||||
import RecentPost from "./RecentPost.astro";
|
||||
import Tag from "./Tag.astro";
|
||||
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
<div id="sidebar" class:list={[className, "flex flex-col w-full gap-4"]} transition:persist>
|
||||
<Profile></Profile>
|
||||
<RecentPost></RecentPost>
|
||||
<Tag></Tag>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#sidebar {
|
||||
view-transition-name: ssss;
|
||||
}
|
||||
/* TODO temporarily */
|
||||
html::view-transition-old(ssss) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
html::view-transition-new(ssss) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
</style>
|
||||
18
src/components/widget/Tag.astro
Normal file
18
src/components/widget/Tag.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
|
||||
import WidgetLayout from "./WidgetLayout.astro";
|
||||
import ButtonTag from "../control/ButtonTag.astro";
|
||||
import {getTagList} from "../../utils/content-utils";
|
||||
|
||||
const tags = await getTagList();
|
||||
|
||||
---
|
||||
<WidgetLayout name="Tags">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{tags.map(t => (
|
||||
<ButtonTag href={`/archive/tag/${t.name}`}>
|
||||
{t.name}
|
||||
</ButtonTag>
|
||||
))}
|
||||
</div>
|
||||
</WidgetLayout>
|
||||
18
src/components/widget/WidgetLayout.astro
Normal file
18
src/components/widget/WidgetLayout.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
interface Props {
|
||||
name?: string;
|
||||
}
|
||||
const props = Astro.props;
|
||||
const {
|
||||
name,
|
||||
} = Astro.props
|
||||
|
||||
---
|
||||
<div class="pb-4 card-base">
|
||||
<div class="font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-8 mt-4 mb-2
|
||||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:left-[-16px] before:top-[5.5px]">{name}</div>
|
||||
<div class="px-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user