chore: init

This commit is contained in:
enscribe 2024-09-10 10:08:41 -07:00
commit f6dcc302d4
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
118 changed files with 13645 additions and 0 deletions

View file

@ -0,0 +1,42 @@
---
import type { CollectionEntry } from "astro:content";
type Props = {
entry: CollectionEntry<"blog">;
};
const { entry } = Astro.props as {
entry: CollectionEntry<"blog">;
};
---
<a
href={`/${entry.collection}/${entry.slug}`}
class="not-prose group relative flex flex-nowrap rounded-lg border border-black/15 px-4 py-3 pr-10 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<div class="flex flex-1 flex-col truncate">
<div class="font-semibold">
{entry.data.title}
</div>
<div class="text-sm">
{entry.data.description}
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute right-2 top-1/2 size-5 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 19 12 12 19"
class="-translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
></polyline>
</svg>
</a>

View file

@ -0,0 +1,33 @@
---
type Props = {
href: string;
};
const { href } = Astro.props;
---
<a
href={href}
class="not-prose group relative flex w-fit flex-nowrap rounded border border-black/15 py-1.5 pl-7 pr-3 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute left-2 top-1/2 size-4 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
></polyline>
</svg>
<div class="text-sm">
<slot />
</div>
</a>

View file

@ -0,0 +1,27 @@
---
---
<button
id="back-to-top"
class="group relative flex w-fit flex-nowrap rounded border border-black/15 py-1.5 pl-8 pr-3 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute left-2 top-1/2 size-4 -translate-y-1/2 rotate-90 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
></polyline>
</svg>
<div class="text-sm">Back to top</div>
</button>

View file

@ -0,0 +1,23 @@
---
export interface Props {
institution: string;
time: string;
job_title: string;
location: string;
description: string
}
const { institution, time, job_title, location, description } = Astro.props;
---
<div class="py-5 px-8">
<div class="flex justify-between text-sm font-bold md:text-lg">
<div>{institution}</div>
<div>{location}</div>
</div>
<div class="flex justify-between text-xs md:text-base">
<div class="italic">{job_title}</div>
<div>{time}</div>
</div>
<p class="text-xs mt-2">{description}</p>
</div>

View file

@ -0,0 +1,44 @@
---
interface Component {
type: "default" | "info" | "warning" | "error";
}
const { type = "default" } = Astro.props;
let emoji = "💡";
if (type === "info") {
emoji = "";
} else if (type === "warning") {
emoji = "⚠️";
} else if (type === "error") {
emoji = "🚨";
}
---
<div class={`not-prose callout callout-${type}`}>
<span class="emoji pointer-events-none select-none">{emoji}</span>
<slot />
</div>
<style>
.callout {
@apply relative my-4 flex rounded border border-orange-800 bg-orange-100 p-3 text-orange-950 dark:border-orange-200/20 dark:bg-orange-950/20 dark:text-orange-200;
}
.emoji {
@apply pr-3 text-xl;
}
.callout-info {
@apply border-blue-800 bg-blue-100 text-blue-950 dark:border-blue-200/20 dark:bg-blue-950/20 dark:text-blue-200;
}
.callout-warning {
@apply border-yellow-800 bg-yellow-100 text-yellow-950 dark:border-yellow-200/20 dark:bg-yellow-950/20 dark:text-yellow-200;
}
.callout-error {
@apply border-red-800 bg-red-100 text-red-950 dark:border-red-200/20 dark:bg-red-950/20 dark:text-red-200;
}
</style>

View file

@ -0,0 +1,5 @@
---
---
<div class="mx-auto max-w-screen-md px-3"><slot /></div>

View file

@ -0,0 +1,92 @@
---
import Container from "@components/Container.astro";
import { SITE } from "@consts";
import BackToTop from "@components/BackToTop.astro";
import SocialIcons from "./SocialIcons.astro";
---
<footer class="animate">
<Container>
<div class="flex justify-between my-2">
<SocialIcons icon_size={'text-xl'} />
<BackToTop />
</div>
<div class="flex items-center justify-between">
<div>&copy; {new Date().getFullYear()} • {SITE.TITLE} 👀<br>
Built with Astro
</div>
<div class="flex flex-wrap items-center gap-1.5">
<button
id="light-theme-button"
aria-label="Light theme"
class="group flex size-9 items-center justify-center rounded border border-black/15 hover:bg-black/5 focus-visible:bg-black/5 dark:border-white/20 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-colors duration-300 ease-in-out group-hover:animate-pulse group-hover:stroke-black group-focus-visible:animate-pulse group-focus-visible:stroke-black group-hover:dark:stroke-white dark:group-focus-visible:stroke-white"
>
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<button
id="dark-theme-button"
aria-label="Dark theme"
class="group flex size-9 items-center justify-center rounded border border-black/15 hover:bg-black/5 focus-visible:bg-black/5 dark:border-white/20 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-colors duration-300 ease-in-out group-hover:animate-pulse group-hover:stroke-black group-focus-visible:animate-pulse group-focus-visible:stroke-black group-hover:dark:stroke-white dark:group-focus-visible:stroke-white"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<button
id="system-theme-button"
aria-label="System theme"
class="group flex size-9 items-center justify-center rounded border border-black/15 hover:bg-black/5 focus-visible:bg-black/5 dark:border-white/20 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-colors duration-300 ease-in-out group-hover:animate-pulse group-hover:stroke-black group-focus-visible:animate-pulse group-focus-visible:stroke-black group-hover:dark:stroke-white dark:group-focus-visible:stroke-white"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
</button>
</div>
</div>
</Container>
</footer>

View file

@ -0,0 +1,17 @@
---
interface Props {
date: Date;
}
const { date } = Astro.props;
---
<time datetime={date.toISOString()}>
{
date.toLocaleDateString("en-US", {
month: "long",
day: "2-digit",
year: "numeric",
})
}
</time>

View file

@ -0,0 +1,20 @@
<div class="giscus"></div>
<script
is:inline
data-astro-rerun
src="https://giscus.app/client.js"
data-repo="trevortylerlee/astro-micro"
data-repo-id="R_kgDOL_6l9Q"
data-category="Announcements"
data-category-id="DIC_kwDOL_6l9c4Cfk55"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme="preferred_color_scheme"
data-lang="en"
data-loading="lazy"
crossorigin="anonymous"
async></script>

251
src/components/Head.astro Normal file
View file

@ -0,0 +1,251 @@
---
import "../styles/global.css";
import { ViewTransitions } from "astro:transitions";
import "@fontsource/geist-sans";
import "@fontsource/geist-mono";
interface Props {
title: string;
description: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = "/blog-placeholder-1.jpg" } = Astro.props;
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍄</text></svg>"
/>
<meta name="generator" content={Astro.generator} />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<!-- PageFind -->
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script is:inline src="/pagefind/pagefind-ui.js"></script>
<ViewTransitions />
<script is:inline>
function init() {
preloadTheme();
onScroll();
animate();
updateThemeButtons();
addCopyCodeButtons();
setGiscusTheme();
const backToTop = document.getElementById("back-to-top");
backToTop?.addEventListener("click", (event) => scrollToTop(event));
const backToPrev = document.getElementById("back-to-prev");
backToPrev?.addEventListener("click", () => window.history.back());
const lightThemeButton = document.getElementById("light-theme-button");
lightThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "light");
toggleTheme(false);
updateThemeButtons();
});
const darkThemeButton = document.getElementById("dark-theme-button");
darkThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "dark");
toggleTheme(true);
updateThemeButtons();
});
const systemThemeButton = document.getElementById("system-theme-button");
systemThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "system");
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
updateThemeButtons();
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
if (localStorage.theme === "system") {
toggleTheme(event.matches);
}
});
document.addEventListener("scroll", onScroll);
}
function updateThemeButtons() {
const theme = localStorage.getItem("theme");
const lightThemeButton = document.getElementById("light-theme-button");
const darkThemeButton = document.getElementById("dark-theme-button");
const systemThemeButton = document.getElementById("system-theme-button");
function removeActiveButtonTheme(button) {
button?.classList.remove("bg-black/5");
button?.classList.remove("dark:bg-white/5");
}
function addActiveButtonTheme(button) {
button?.classList.add("bg-black/5");
button?.classList.add("dark:bg-white/5");
}
removeActiveButtonTheme(lightThemeButton);
removeActiveButtonTheme(darkThemeButton);
removeActiveButtonTheme(systemThemeButton);
if (theme === "light") {
addActiveButtonTheme(lightThemeButton);
} else if (theme === "dark") {
addActiveButtonTheme(darkThemeButton);
} else {
addActiveButtonTheme(systemThemeButton);
}
}
function animate() {
const animateElements = document.querySelectorAll(".animate");
animateElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add("show");
}, index * 100);
});
}
function onScroll() {
if (window.scrollY > 0) {
document.documentElement.classList.add("scrolled");
} else {
document.documentElement.classList.remove("scrolled");
}
}
function scrollToTop(event) {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
function toggleTheme(dark) {
const css = document.createElement("style");
css.appendChild(
document.createTextNode(
`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}
`
)
);
document.head.appendChild(css);
if (dark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
window.getComputedStyle(css).opacity;
document.head.removeChild(css);
setGiscusTheme();
}
function preloadTheme() {
const userTheme = localStorage.theme;
if (userTheme === "light" || userTheme === "dark") {
toggleTheme(true); // set default to dark theme
} else {
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
}
}
function addCopyCodeButtons() {
let copyButtonLabel = "📋";
let codeBlocks = Array.from(document.querySelectorAll("pre"));
async function copyCode(codeBlock, copyButton) {
const codeText = codeBlock.innerText;
const buttonText = copyButton.innerText;
const textToCopy = codeText.replace(buttonText, "");
await navigator.clipboard.writeText(textToCopy);
copyButton.innerText = "✅";
setTimeout(() => {
copyButton.innerText = copyButtonLabel;
}, 2000);
}
for (let codeBlock of codeBlocks) {
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
const copyButton = document.createElement("button");
copyButton.innerText = copyButtonLabel;
copyButton.classList = "copy-code";
codeBlock.setAttribute("tabindex", "0");
codeBlock.appendChild(copyButton);
codeBlock.parentNode.insertBefore(wrapper, codeBlock);
wrapper.appendChild(codeBlock);
copyButton?.addEventListener("click", async () => {
await copyCode(codeBlock, copyButton);
});
}
}
const setGiscusTheme = () => {
const giscus = document.querySelector(".giscus-frame");
const isDark = document.documentElement.classList.contains("dark");
if (giscus) {
const url = new URL(giscus.src);
url.searchParams.set("theme", isDark ? "dark" : "light");
giscus.src = url.toString();
}
};
document.addEventListener("DOMContentLoaded", () => init());
document.addEventListener("astro:after-swap", () => init());
preloadTheme();
</script>

View file

@ -0,0 +1,59 @@
---
import Container from "@components/Container.astro";
import Link from "@components/Link.astro";
import { SITE } from "@consts";
import ProgressBar from "./ProgressBar.astro";
---
<header transition:persist>
<Container>
<div class="flex flex-wrap justify-between gap-y-2">
<Link href="/" underline={false}>
<div class="font-semibold">
{SITE.TITLE}&nbsp;🍄
</div>
</Link>
<nav class="flex items-center gap-1 text-sm">
<Link href="/blog">blog</Link>
<span>
{`/`}
</span>
<Link href="/publications">research</Link>
<span>
{`/`}
</span>
<Link href="/cv">cv</Link>
<span>
{`/`}
</span>
<Link href="/about">about</Link>
<span>
{`/`}
</span>
<Link href="/tags">tags</Link>
<span>
{`/`}
</span>
<button
id="magnifying-glass"
aria-label="Search"
class="flex items-center rounded border border-black/15 bg-neutral-100 px-2 py-1 text-xs transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:bg-neutral-900 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
height="16"
stroke-linejoin="round"
viewBox="0 0 16 16"
width="16"
style="color: currentcolor;"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 7C3.5 5.067 5.067 3.5 7 3.5C8.933 3.5 10.5 5.067 10.5 7C10.5 7.88461 10.1718 8.69256 9.63058 9.30876L9.30876 9.63058C8.69256 10.1718 7.88461 10.5 7 10.5C5.067 10.5 3.5 8.933 3.5 7ZM9.96544 11.0261C9.13578 11.6382 8.11014 12 7 12C4.23858 12 2 9.76142 2 7C2 4.23858 4.23858 2 7 2C9.76142 2 12 4.23858 12 7C12 8.11014 11.6382 9.13578 11.0261 9.96544L14.0303 12.9697L14.5607 13.5L13.5 14.5607L12.9697 14.0303L9.96544 11.0261Z"
fill="currentColor"></path></svg
>
&nbsp;Search
</button>
</nav>
</div>
</Container>
</header>

31
src/components/Link.astro Normal file
View file

@ -0,0 +1,31 @@
---
import { cn } from "@lib/utils";
type Props = {
href: string;
external?: boolean;
underline?: boolean;
group?: boolean;
};
const {
href,
external,
underline = true,
group = false,
...rest
} = Astro.props;
---
<a
href={href}
target={external ? "_blank" : "_self"}
class={cn(
"inline-block decoration-black/30 hover:decoration-black/50 focus-visible:decoration-black/50 dark:decoration-white/30 dark:hover:decoration-white/50 dark:focus-visible:decoration-white/50 hover:text-cyan-500 focus-visible:text-black dark:hover:text-orange-500 dark:focus-visible:text-white transition-colors duration-300 ease-in-out",
underline && "underline underline-offset-[3px]",
group && "group"
)}
{...rest}
>
<slot />
</a>

View file

@ -0,0 +1,132 @@
---
import Search from "astro-pagefind/components/Search";
---
<aside data-pagefind-ignore>
<div
transition:persist
id="backdrop"
class="bg-[rgba(0, 0, 0, 0.5] invisible fixed left-0 top-0 z-50 flex h-screen w-full justify-center p-6 backdrop-blur-sm"
>
<div
id="pagefind-container"
class="m-0 flex h-fit max-h-[80%] w-full max-w-screen-sm flex-col overflow-auto rounded border border-black/15 bg-neutral-100 p-2 px-4 py-3 shadow-lg dark:border-white/20 dark:bg-neutral-900"
>
<Search
id="search"
className="pagefind-ui"
uiOptions={{
showImages: false,
excerptLength: 15,
resetStyles: false,
}}
/>
<div class="mr-2 pb-1 pt-4 text-right text-xs dark:prose-invert">
Press <span class="prose text-xs dark:prose-invert"
><kbd class="">Esc</kbd></span
> or click anywhere to close
</div>
</div>
</div>
</aside>
<script is:inline>
const magnifyingGlass = document.getElementById("magnifying-glass");
const backdrop = document.getElementById("backdrop");
function openPagefind() {
const searchDiv = document.getElementById("search");
const search = searchDiv.querySelector("input");
setTimeout(() => {
search.focus();
}, 0);
backdrop?.classList.remove("invisible");
backdrop?.classList.add("visible");
}
function closePagefind() {
const search = document.getElementById("search");
search.value = "";
backdrop?.classList.remove("visible");
backdrop?.classList.add("invisible");
}
// open pagefind
magnifyingGlass?.addEventListener("click", () => {
openPagefind();
});
document.addEventListener("keydown", (e) => {
if (e.key === "/") {
e.preventDefault();
openPagefind();
} else if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
openPagefind();
}
});
// close pagefind
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" || e.keyCode === 27) {
closePagefind();
}
});
// close pagefind when searched result(link) clicked
document.addEventListener("click", (event) => {
if (event.target.classList.contains("pagefind-ui__result-link")) {
closePagefind();
}
});
backdrop?.addEventListener("click", (event) => {
if (!event.target.closest("#pagefind-container")) {
closePagefind();
}
});
// prevent form submission
const form = document.getElementById("form");
form?.addEventListener("submit", (event) => {
event.preventDefault();
});
</script>
<style is:global>
:root {
--pagefind-ui-scale: 0.75;
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: 3px;
--pagefind-ui-font: "Geist", sans-serif;
--pagefind-ui-primary: #3d3d3d;
--pagefind-ui-text: #3d3d3d;
--pagefind-ui-background: #ffffff;
--pagefind-ui-border: #d0d0d0;
--pagefind-ui-tag: #f5f5f5;
}
.dark {
--pagefind-ui-primary: #d4d4d4;
--pagefind-ui-text: #d4d4d4;
--pagefind-ui-background: #171717;
--pagefind-ui-border: #404040;
}
#search input {
font-weight: normal;
}
#search p {
font-weight: normal;
}
#search .pagefind-ui__result-title {
font-weight: 600;
}
#search .pagefind-ui__message {
padding: 0;
padding-bottom: 0.75rem;
}
</style>

View file

@ -0,0 +1,65 @@
---
const { prevPost, nextPost } = Astro.props;
---
<div class="grid grid-cols-2 gap-1.5 sm:gap-3">
{
prevPost?.slug ? (
<a
href={`/blog/${prevPost?.slug}`}
class="group relative flex flex-nowrap rounded-lg border border-black/15 px-4 py-3 pl-10 no-underline transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute left-2 top-1/2 size-5 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
/>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
/>
</svg>
<div class="flex items-center text-sm">{prevPost?.data.title}</div>
</a>
) : (
<div class="invisible" />
)
}
{
nextPost?.slug ? (
<a
href={`/blog/${nextPost?.slug}`}
class="group relative flex flex-grow flex-row-reverse flex-nowrap rounded-lg border border-black/15 px-4 py-4 pr-10 no-underline transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute right-2 top-1/2 size-5 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
/>
<polyline
points="12 5 19 12 12 19"
class="-translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
/>
</svg>
<div class="flex items-center text-sm">{nextPost?.data.title}</div>
</a>
) : (
<div class="invisible" />
)
}
</div>

View file

@ -0,0 +1,9 @@
<div class="fixed w-full h-[2px] bg-neutral-300 dark:bg-slate-900 z-50">
<div class="bg-cyan-500 dark:bg-orange-500 h-full"></div>
</div>
<script>
import { initializeProgressBar } from '../scripts/progress-bar.js';
initializeProgressBar();
</script>

View file

@ -0,0 +1,74 @@
---
import type { CollectionEntry } from "astro:content";
import { Image } from "astro:assets";
import { HIGHLIGHTAUTHOR } from "@consts";
type Props = {
entry: CollectionEntry<"publications">;
};
const { entry } = Astro.props as {
entry: CollectionEntry<"publications">;
};
const splitStr = (authors: string | undefined, targetAuthor: string) => {
if (!authors) return [];
const parts = authors.split(new RegExp(`(${targetAuthor})`, 'g'));
return parts;
};
const decomposeURL = (URL: string | undefined) => {
if (!URL) return { text: '', url: '' };
const parts = URL.split(": ");
return { text: parts[0], url: parts[1] };
};
const dataLink = decomposeURL(entry.data.dataURL);
const paperLink = decomposeURL(entry.data.paperURL);
const codeLink = decomposeURL(entry.data.codeURL);
const webLink = decomposeURL(entry.data.webURL);
const authorsParts = splitStr(entry.data.authors, HIGHLIGHTAUTHOR);
---
<div
class="w-full not-prose group relative grid grid-cols-auto md:grid-cols-[204px_auto] gap-4 rounded-lg items-center border border-black/15 px-4 py-3 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<Image
src={entry.data.img ?? ''}
alt={entry.data.imgAlt ?? ''}
width={640}
height={480}
class="sm:w-[240px] sm:h-[135px] shadow-sm rounded-md sm:mr-6 hover:opacity-80 transition hidden md:flex"
loading="eager"
/>
<div class="flex items-center h-full">
<div class="flex flex-col">
<div class="w-full">
<div class="text-md font-semibold w-full">
{entry.data.title}
</div>
<div class="text-sm w-full">
{authorsParts.map((part:any) =>
part === HIGHLIGHTAUTHOR ? <u><strong>{part}</strong></u> : part
)}
</div>
<div class="text-sm w-full">
{paperLink.url!="" && <a class="underline hover:text-cyan-500 text-orange-500 dark:hover:text-cyan-500 transition-colors duration-300 ease-in-out visited:text-indigo-400" target="_blank" href={paperLink.url}>{paperLink.text}</a>}
{codeLink.url!="" && <a class="underline hover:text-cyan-500 text-orange-500 dark:hover:text-cyan-500 transition-colors duration-300 ease-in-out visited:text-indigo-400" target="_blank" href={codeLink.url}>{codeLink.text}</a>}
{webLink.url!="" && <a class="underline hover:text-cyan-500 text-orange-500 dark:hover:text-cyan-500 transition-colors duration-300 ease-in-out visited:text-indigo-400" target="_blank" href={webLink.url}>{webLink.text}</a>}
{dataLink.url!="" && <a class="underline hover:text-cyan-500 text-orange-500 dark:hover:text-cyan-500 transition-colors duration-300 ease-in-out visited:text-indigo-400" target="_blank" href={dataLink.url}>{dataLink.text}</a>}
</div>
<div class="text-sm">
In <div class="inline italic">{entry.data.pub}</div>
</div>
</div>
<p class="text-sm mt-2 break-words">
{entry.data.description}
</p>
</div>
</div>
</div>

View file

@ -0,0 +1,18 @@
---
import 'bootstrap-icons/font/bootstrap-icons.css';
export interface Props {
URL: string;
icon: string;
icon_size: string
}
const { URL, icon, icon_size } = Astro.props;
---
<a
href={URL}
target={"_blank"}
class={
`inline-block ${icon_size} decoration-black/30 dark:decoration-white/30 hover:decoration-black/50 focus-visible:decoration-black/50 dark:hover:decoration-white/50 dark:focus-visible:decoration-white/50 text-current hover:text-cyan-500 focus-visible:text-black dark:hover:text-orange-500 dark:focus-visible:text-white transition-colors duration-300 ease-in-out`}>
<i class={`bi bi-${icon}`}></i>
</a>

View file

@ -0,0 +1,18 @@
---
import SocialIcon from "@components/SocialIcon.astro";
import { SITE } from "@consts";
export interface Props {
icon_size: string
}
const { icon_size } = Astro.props;
---
<ul class="not-prose flex flex-wrap gap-2">
<SocialIcon icon_size={icon_size} URL="#" icon="twitter-x"/>
<SocialIcon icon_size={icon_size} URL="#" icon="github"/>
<SocialIcon icon_size={icon_size} URL="#" icon="linkedin"/>
<SocialIcon icon_size={icon_size} URL="#" icon="envelope-fill"/>
<SocialIcon icon_size={icon_size} URL="#" icon="mortarboard-fill"/>
<SocialIcon icon_size={icon_size} URL={`${SITE.SITEURL}/rss.xml`} icon="rss-fill"/>
</ul>

View file

@ -0,0 +1,54 @@
---
import TableOfContentsHeading from "./TableOfContentsHeading.astro";
// https://kld.dev/building-table-of-contents/
const { headings } = Astro.props;
const toc = buildToc(headings);
export interface Heading {
depth: number;
slug: string;
text: string;
}
function buildToc(headings: Heading[]) {
const toc: Heading[] = [];
const parentHeadings = new Map();
headings.forEach((h) => {
const heading = { ...h, subheadings: [] };
parentHeadings.set(heading.depth, heading);
if (heading.depth === 2) {
toc.push(heading);
} else {
parentHeadings.get(heading.depth - 1).subheadings.push(heading);
}
});
return toc;
}
---
<details
open
class="animate rounded-lg border border-black/15 dark:border-white/20"
>
<summary>Table of Contents</summary>
<nav class="">
<ul class="py-3">
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul>
</nav>
</details>
<style>
summary {
@apply cursor-pointer rounded-t-lg px-3 py-1.5 font-medium transition-colors;
}
summary:hover {
@apply bg-black/5 dark:bg-white/5;
}
details[open] summary {
@apply bg-black/5 dark:bg-white/5;
}
</style>

View file

@ -0,0 +1,23 @@
---
import type { Heading } from "./TableOfContents.astro";
import Link from "./Link.astro";
// https://kld.dev/building-table-of-contents/
const { heading } = Astro.props;
---
<li class="list-inside list-disc px-6 py-1.5 text-sm">
<Link href={"#" + heading.slug} underline>
{heading.text}
</Link>
{
heading.subheadings.length > 0 && (
<ul class="translate-x-3">
{heading.subheadings.map((subheading: Heading) => (
<Astro.self heading={subheading} />
))}
</ul>
)
}
</li>