Upgrade Astro and Astro Paper to v5

This commit is contained in:
tiff 2025-03-08 19:45:21 -05:00
parent 8ebf7d5996
commit e719f7ccca
105 changed files with 7099 additions and 1939 deletions

View file

@ -0,0 +1,37 @@
---
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
import LinkButton from "./LinkButton.astro";
import { SITE } from "@/config";
---
{
SITE.showBackButton && (
<div class="mx-auto flex w-full max-w-3xl items-center justify-start px-2">
<LinkButton
id="back-button"
href="/"
class="focus-outline mt-8 mb-2 flex hover:text-foreground/75"
>
<IconChevronLeft class="inline-block size-6" />
<span>Go back</span>
</LinkButton>
</div>
)
}
<script>
/* Update Search Praam */
function updateGoBackUrl() {
const backButton: HTMLAnchorElement | null =
document.querySelector("#back-button");
const backUrl = sessionStorage.getItem("backUrl");
if (backUrl && backButton) {
backButton.href = backUrl;
}
}
document.addEventListener("astro:page-load", updateGoBackUrl);
updateGoBackUrl();
</script>

View file

@ -8,34 +8,35 @@ const breadcrumbList = currentUrlPath.split("/").slice(1);
// if breadcrumb is Home > Posts > 1 <etc>
// replace Posts with Posts (page number)
breadcrumbList[0] === "posts" &&
if (breadcrumbList[0] === "posts") {
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
}
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
// replace [tag] > [page] with [tag] (page number)
breadcrumbList[0] === "tags" &&
!isNaN(Number(breadcrumbList[2])) &&
if (breadcrumbList[0] === "tags" && !isNaN(Number(breadcrumbList[2]))) {
breadcrumbList.splice(
1,
3,
`${breadcrumbList[1]} ${
Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"
}`
`${breadcrumbList[1]} ${Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"}`
);
}
---
<nav class="breadcrumb" aria-label="breadcrumb">
<ul>
<nav class="mx-auto mt-8 mb-1 w-full max-w-3xl px-4" aria-label="breadcrumb">
<ul
class="font-light [&>li]:inline [&>li:not(:last-child)>a]:hover:opacity-100"
>
<li>
<a href="/">Home</a>
<span aria-hidden="true">&raquo;</span>
<a href="/" class="opacity-80">Home</a>
<span aria-hidden="true" class="opacity-80">&raquo;</span>
</li>
{
breadcrumbList.map((breadcrumb, index) =>
index + 1 === breadcrumbList.length ? (
<li>
<span
class={`${index > 0 ? "lowercase" : "capitalize"}`}
class:list={["capitalize opacity-75", { lowercase: index > 0 }]}
aria-current="page"
>
{/* make the last part lowercase in Home > Tags > some-tag */}
@ -44,7 +45,9 @@ breadcrumbList[0] === "tags" &&
</li>
) : (
<li>
<a href={`/${breadcrumb}/`}>{breadcrumb}</a>
<a href={`/${breadcrumb}/`} class="capitalize opacity-70">
{breadcrumb}
</a>
<span aria-hidden="true">&raquo;</span>
</li>
)
@ -52,21 +55,3 @@ breadcrumbList[0] === "tags" &&
}
</ul>
</nav>
<style>
.breadcrumb {
@apply mx-auto mb-1 mt-8 w-full max-w-3xl px-4;
}
.breadcrumb ul li {
@apply inline;
}
.breadcrumb ul li a {
@apply capitalize opacity-70;
}
.breadcrumb ul li span {
@apply opacity-70;
}
.breadcrumb ul li:not(:last-child) a {
@apply hover:opacity-100;
}
</style>

37
src/components/Card.astro Normal file
View file

@ -0,0 +1,37 @@
---
import { slugifyStr } from "@/utils/slugify";
import type { CollectionEntry } from "astro:content";
import Datetime from "./Datetime.astro";
export interface Props {
href?: string;
frontmatter: CollectionEntry<"blog">["data"];
secHeading?: boolean;
}
const { href, frontmatter, secHeading = true } = Astro.props;
const { title, pubDatetime, modDatetime, description } = frontmatter;
const headerProps = {
style: { viewTransitionName: slugifyStr(title) },
class: "text-lg font-medium decoration-dashed hover:underline",
};
---
<li class="my-6">
<a
href={href}
class="inline-block text-lg font-medium text-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
>
{
secHeading ? (
<h2 {...headerProps}>{title}</h2>
) : (
<h3 {...headerProps}>{title}</h3>
)
}
</a>
<Datetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
<p>{description}</p>
</li>

View file

@ -1,35 +0,0 @@
import { slugifyStr } from "@utils/slugify";
import Datetime from "./Datetime";
import type { CollectionEntry } from "astro:content";
export interface Props {
href?: string;
frontmatter: CollectionEntry<"blog">["data"];
secHeading?: boolean;
}
export default function Card({ href, frontmatter, secHeading = true }: Props) {
const { title, pubDatetime, modDatetime, description } = frontmatter;
const headerProps = {
style: { viewTransitionName: slugifyStr(title) },
className: "text-lg font-medium decoration-dashed hover:underline",
};
return (
<li className="my-6">
<a
href={href}
className="inline-block text-lg font-medium text-skin-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
>
{secHeading ? (
<h2 {...headerProps}>{title}</h2>
) : (
<h3 {...headerProps}>{title}</h3>
)}
</a>
<Datetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
<p>{description}</p>
</li>
);
}

View file

@ -0,0 +1,66 @@
---
import { LOCALE } from "@/constants";
export interface Props {
pubDatetime: string | Date;
modDatetime: string | Date | undefined | null;
size?: "sm" | "lg";
class?: string;
}
const {
pubDatetime,
modDatetime,
size = "sm",
class: className = "",
} = Astro.props;
/* ========== Formatted Datetime ========== */
const myDatetime = new Date(
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime
);
const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
year: "numeric",
month: "short",
day: "numeric",
});
const time = myDatetime.toLocaleTimeString(LOCALE.langTag, {
hour: "2-digit",
minute: "2-digit",
});
---
<div class={`flex items-end space-x-2 opacity-80 ${className}`.trim()}>
<svg
xmlns="http://www.w3.org/2000/svg"
class={`${
size === "sm" ? "scale-90" : "scale-100"
} inline-block h-6 w-6 min-w-[1.375rem] fill-foreground`}
aria-hidden="true"
>
<path
d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"
></path>
<path
d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"
></path>
</svg>
{
modDatetime && modDatetime > pubDatetime ? (
<span
class={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}
>
Updated:
</span>
) : (
<span class="sr-only">Published:</span>
)
}
<span class={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}>
<time datetime={myDatetime.toISOString()}>{date}</time>
<span aria-hidden="true"> | </span>
<span class="sr-only">&nbsp;at&nbsp;</span>
<span class="text-nowrap">{time}</span>
</span>
</div>

View file

@ -1,120 +0,0 @@
import { LOCALE, SITE } from "@config";
import type { CollectionEntry } from "astro:content";
interface DatetimesProps {
pubDatetime: string | Date;
modDatetime: string | Date | undefined | null;
}
interface EditPostProps {
editPost?: CollectionEntry<"blog">["data"]["editPost"];
postId?: CollectionEntry<"blog">["id"];
}
interface Props extends DatetimesProps, EditPostProps {
size?: "sm" | "lg";
className?: string;
}
export default function Datetime({
pubDatetime,
modDatetime,
size = "sm",
className = "",
editPost,
postId,
}: Props) {
return (
<div
className={`flex items-center space-x-2 opacity-80 ${className}`.trim()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`${
size === "sm" ? "scale-90" : "scale-100"
} inline-block h-6 w-6 min-w-[1.375rem] fill-skin-base`}
aria-hidden="true"
>
<path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path>
<path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path>
</svg>
{modDatetime && modDatetime > pubDatetime ? (
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
Updated:
</span>
) : (
<span className="sr-only">Published:</span>
)}
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
<FormattedDatetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
/>
{size === "lg" && <EditPost editPost={editPost} postId={postId} />}
</span>
</div>
);
}
const FormattedDatetime = ({ pubDatetime, modDatetime }: DatetimesProps) => {
const myDatetime = new Date(
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime
);
const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
year: "numeric",
month: "short",
day: "numeric",
});
const time = myDatetime.toLocaleTimeString(LOCALE.langTag, {
hour: "2-digit",
minute: "2-digit",
});
return (
<>
<time dateTime={myDatetime.toISOString()}>{date}</time>
<span aria-hidden="true"> | </span>
<span className="sr-only">&nbsp;at&nbsp;</span>
<span className="text-nowrap">{time}</span>
</>
);
};
const EditPost = ({ editPost, postId }: EditPostProps) => {
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
const appendFilePath =
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
if (appendFilePath && postId) {
editPostUrl += `/${postId}`;
}
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
return (
showEditPost && (
<>
<span aria-hidden="true"> | </span>
<a
className="space-x-1.5 hover:opacity-75"
href={editPostUrl}
rel="noopener noreferrer"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icons-tabler-outline icon-tabler-edit inline-block !scale-90 fill-skin-base"
aria-hidden="true"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
<path d="M16 5l3 3" />
</svg>
<span className="text-base italic">{editPostText}</span>
</a>
</>
)
);
};

View file

@ -0,0 +1,41 @@
---
import type { CollectionEntry } from "astro:content";
import IconEdit from "@/assets/icons/IconEdit.svg";
import { SITE } from "@/config";
export interface Props {
editPost?: CollectionEntry<"blog">["data"]["editPost"];
postId?: CollectionEntry<"blog">["id"];
class?: string;
}
const { editPost, postId, class: className = "" } = Astro.props;
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
const appendFilePath =
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
if (appendFilePath && postId) {
editPostUrl += `/${postId}`;
}
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
---
{
showEditPost && (
<div class:list={["opacity-80", className]}>
<span aria-hidden="true" class="max-sm:hidden">
|
</span>
<a
class="space-x-1.5 hover:opacity-75"
href={editPostUrl}
rel="noopener noreferrer"
target="_blank"
>
<IconEdit class="inline-block size-6" />
<span class="italic max-sm:text-sm sm:inline">{editPostText}</span>
</a>
</div>
)
}

View file

@ -11,35 +11,16 @@ export interface Props {
const { noMarginTop = false } = Astro.props;
---
<footer class={`${noMarginTop ? "" : "mt-auto"}`}>
<footer class:list={["w-full", { "mt-auto": !noMarginTop }]}>
<Hr noPadding />
<div class="footer-wrapper">
<div
class="flex flex-col items-center justify-between py-6 sm:flex-row-reverse sm:py-4"
>
<Socials centered />
<div class="copyright-wrapper">
<div class="my-2 flex flex-col items-center whitespace-nowrap sm:flex-row">
<span>Copyright &#169; {currentYear}</span>
<span class="separator">&nbsp;|&nbsp;</span>
<span class="hidden sm:inline">&nbsp;|&nbsp;</span>
<span>All rights reserved.</span>
</div>
</div>
</footer>
<style>
footer {
@apply w-full;
}
.footer-wrapper {
@apply flex flex-col items-center justify-between py-6 sm:flex-row-reverse sm:py-4;
}
.link-button {
@apply my-1 p-2 hover:rotate-6;
}
.link-button svg {
@apply scale-125;
}
.copyright-wrapper {
@apply my-2 flex flex-col items-center whitespace-nowrap sm:flex-row;
}
.separator {
@apply hidden sm:inline;
}
</style>

View file

@ -1,145 +1,135 @@
---
import { LOGO_IMAGE, SITE } from "@config";
import Hr from "./Hr.astro";
import IconX from "@/assets/icons/IconX.svg";
import IconMoon from "@/assets/icons/IconMoon.svg";
import IconSearch from "@/assets/icons/IconSearch.svg";
import IconArchive from "@/assets/icons/IconArchive.svg";
import IconSunHigh from "@/assets/icons/IconSunHigh.svg";
import IconMenuDeep from "@/assets/icons/IconMenuDeep.svg";
import LinkButton from "./LinkButton.astro";
import Logo from "@/assets/logo.svg";
import { SITE } from "@/config";
export interface Props {
activeNav?: "posts" | "archives" | "tags" | "about" | "search";
}
const { pathname } = Astro.url;
const { activeNav } = Astro.props;
// Remove trailing slash from current pathname if exists
const currentPath =
pathname.endsWith("/") && pathname !== "/" ? pathname.slice(0, -1) : pathname;
const isActive = (path: string) => {
const currentPathArray = currentPath.split("/").filter(p => p.trim());
const pathArray = path.split("/").filter(p => p.trim());
return currentPath === path || currentPathArray[0] === pathArray[0];
};
---
<header>
<a id="skip-to-content" href="#main-content">Skip to content</a>
<div class="nav-container">
<div class="top-nav-wrap">
<a href="/" class="logo whitespace-nowrap">
{
LOGO_IMAGE.enable ? (
<img
src={`/assets/${LOGO_IMAGE.svg ? "logo.svg" : "logo.png"}`}
alt={SITE.title}
width={LOGO_IMAGE.width}
height={LOGO_IMAGE.height}
/>
) : (
SITE.title
)
}
<a
id="skip-to-content"
href="#main-content"
class="absolute -top-full left-16 z-50 bg-background px-3 py-2 text-accent backdrop-blur-lg transition-all focus:top-4"
>
Skip to content
</a>
<div
id="nav-container"
class="mx-auto flex max-w-3xl flex-col items-center justify-between sm:flex-row"
>
<div
id="top-nav-wrap"
class="relative flex w-full items-start justify-between bg-background p-4 sm:py-6"
>
<a
href="/"
class="absolute py-1 text-2xl leading-7 font-semibold whitespace-nowrap sm:static"
>
<Logo class="scale-75 dark:invert" />
</a>
<nav id="nav-menu">
<nav
id="nav-menu"
class="flex w-full flex-col items-center sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0"
>
<button
class="hamburger-menu focus-outline"
id="menu-btn"
class="focus-outline self-end p-2 sm:hidden"
aria-label="Open Menu"
aria-expanded="false"
aria-controls="menu-items"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="menu-icon"
>
<line x1="7" y1="12" x2="21" y2="12" class="line"></line>
<line x1="3" y1="6" x2="21" y2="6" class="line"></line>
<line x1="12" y1="18" x2="21" y2="18" class="line"></line>
<line x1="18" y1="6" x2="6" y2="18" class="close"></line>
<line x1="6" y1="6" x2="18" y2="18" class="close"></line>
</svg>
<IconX id="close-icon" class="hidden" />
<IconMenuDeep id="menu-icon" />
</button>
<ul id="menu-items" class="display-none sm:flex">
<li>
<a href="/posts/" class={activeNav === "posts" ? "active" : ""}>
<ul
id="menu-items"
class:list={[
"mt-4 grid w-44 grid-cols-2 place-content-center gap-2",
"[&>li>a]:block [&>li>a]:px-4 [&>li>a]:py-3 [&>li>a]:text-center [&>li>a]:font-medium [&>li>a]:hover:text-accent sm:[&>li>a]:px-2 sm:[&>li>a]:py-1",
"hidden",
"sm:mt-0 sm:ml-0 sm:flex sm:w-auto sm:gap-x-5 sm:gap-y-0",
]}
>
<li class="col-span-2">
<a href="/posts" class:list={{ "active-nav": isActive("/posts") }}>
Posts
</a>
</li>
<li>
<a href="/tags/" class={activeNav === "tags" ? "active" : ""}>
<li class="col-span-2">
<a href="/tags" class:list={{ "active-nav": isActive("/tags") }}>
Tags
</a>
</li>
<li>
<a href="/about/" class={activeNav === "about" ? "active" : ""}>
<li class="col-span-2">
<a href="/about" class:list={{ "active-nav": isActive("/about") }}>
About
</a>
</li>
{
SITE.showArchives && (
<li>
<li class="col-span-2">
<LinkButton
href="/archives/"
className={`focus-outline flex justify-center p-3 sm:p-1`}
href="/archives"
class:list={[
"focus-outline flex justify-center p-3 sm:p-1",
{
"active-nav [&>svg]:stroke-accent": isActive("/archives"),
},
]}
ariaLabel="archives"
title="Archives"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class:list={[
"icon icon-tabler icons-tabler-outline !hidden sm:!inline-block",
activeNav === "archives" && "!stroke-skin-accent",
]}
>
<>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
<path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" />
<path d="M10 12l4 0" />
</>
</svg>
<span
class:list={[
"sm:sr-only",
activeNav === "archives" && "active",
]}
>
Archives
</span>
<IconArchive class="hidden sm:inline-block" />
<span class="sm:sr-only">Archives</span>
</LinkButton>
</li>
)
}
<li>
<li class="col-span-1 flex items-center justify-center">
<LinkButton
href="/search/"
className={`focus-outline p-3 sm:p-1 ${
activeNav === "search" ? "active" : ""
} flex`}
href="/search"
class:list={[
"focus-outline flex p-3 sm:p-1",
{ "[&>svg]:stroke-accent": isActive("/search") },
]}
ariaLabel="search"
title="Search"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="scale-125 sm:scale-100"
><path
d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"
></path>
</svg>
<IconSearch />
<span class="sr-only">Search</span>
</LinkButton>
</li>
{
SITE.lightAndDarkMode && (
<li>
<li class="col-span-1 flex items-center justify-center">
<button
id="theme-btn"
class="focus-outline"
class="focus-outline relative size-12 p-4 sm:size-8 hover:[&>svg]:stroke-accent"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
<svg xmlns="http://www.w3.org/2000/svg" id="moon-svg">
<path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" id="sun-svg">
<path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z" />
</svg>
<IconMoon class="absolute top-[50%] left-[50%] -translate-[50%] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<IconSunHigh class="absolute top-[50%] left-[50%] -translate-[50%] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
</button>
</li>
)
@ -149,100 +139,26 @@ const { activeNav } = Astro.props;
</div>
</div>
<Hr />
<div style="display:none;">
<a rel="me" href="https://hachyderm.io/@tiff">Mastodon</a>
</div>
</header>
<style>
#skip-to-content {
@apply absolute -top-full left-16 z-50 bg-skin-accent px-3 py-2 text-skin-inverted transition-all focus:top-4;
}
.nav-container {
@apply mx-auto flex max-w-3xl flex-col items-center justify-between sm:flex-row;
}
.top-nav-wrap {
@apply relative flex w-full items-start justify-between p-4 sm:items-center sm:py-8;
}
.logo {
@apply absolute py-1 text-xl font-semibold sm:static sm:text-2xl;
}
.hamburger-menu {
@apply self-end p-2 sm:hidden;
}
.hamburger-menu svg {
@apply h-6 w-6 scale-125 fill-skin-base;
}
nav {
@apply flex w-full flex-col items-center sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0;
}
nav ul {
@apply mt-4 grid w-44 grid-cols-2 grid-rows-4 gap-x-2 gap-y-2 sm:ml-0 sm:mt-0 sm:w-auto sm:gap-x-5 sm:gap-y-0;
}
nav ul li {
@apply col-span-2 flex items-center justify-center;
}
nav ul li a {
@apply w-full px-4 py-3 text-center font-medium hover:text-skin-accent sm:my-0 sm:px-2 sm:py-1;
}
nav ul li:nth-last-child(2) a {
@apply w-auto;
}
nav ul li:nth-last-child(1),
nav ul li:nth-last-child(2) {
@apply col-span-1;
}
nav .active {
@apply underline decoration-wavy decoration-2 underline-offset-4;
}
nav a.active svg {
@apply fill-skin-accent;
}
nav button {
@apply p-1;
}
nav button svg {
@apply h-6 w-6 fill-skin-base hover:fill-skin-accent;
}
#theme-btn {
@apply p-3 sm:p-1;
}
#theme-btn svg {
@apply scale-125 hover:rotate-12 sm:scale-100;
}
.menu-icon line {
@apply transition-opacity duration-75 ease-in-out;
}
.menu-icon .close {
opacity: 0;
}
.menu-icon.is-active .line {
@apply opacity-0;
}
.menu-icon.is-active .close {
@apply opacity-100;
}
</style>
<script>
function toggleNav() {
// Toggle menu
const menuBtn = document.querySelector(".hamburger-menu");
const menuIcon = document.querySelector(".menu-icon");
const menuBtn = document.querySelector("#menu-btn");
const menuItems = document.querySelector("#menu-items");
const menuIcon = document.querySelector("#menu-icon");
const closeIcon = document.querySelector("#close-icon");
menuBtn?.addEventListener("click", () => {
const menuExpanded = menuBtn.getAttribute("aria-expanded") === "true";
menuIcon?.classList.toggle("is-active");
menuBtn.setAttribute("aria-expanded", menuExpanded ? "false" : "true");
menuBtn.setAttribute(
"aria-label",
menuExpanded ? "Open Menu" : "Close Menu"
);
menuItems?.classList.toggle("display-none");
if (!menuBtn || !menuItems || !menuIcon || !closeIcon) return;
menuBtn.addEventListener("click", () => {
const openMenu = menuBtn.getAttribute("aria-expanded") === "true";
menuBtn.setAttribute("aria-expanded", openMenu ? "false" : "true");
menuBtn.setAttribute("aria-label", openMenu ? "Open Menu" : "Close Menu");
menuItems.classList.toggle("hidden");
menuIcon.classList.toggle("hidden");
closeIcon.classList.toggle("hidden");
});
}

View file

@ -8,5 +8,5 @@ const { noPadding = false, ariaHidden = true } = Astro.props;
---
<div class={`max-w-3xl mx-auto ${noPadding ? "px-0" : "px-4"}`}>
<hr class="border-skin-line" aria-hidden={ariaHidden} />
<hr class="border-border" aria-hidden={ariaHidden} />
</div>

View file

@ -1,15 +1,17 @@
---
export interface Props {
id?: string;
href: string;
className?: string;
class?: string;
ariaLabel?: string;
title?: string;
disabled?: boolean;
}
const {
id,
href,
className = "",
class: className = "",
ariaLabel,
title,
disabled = false,
@ -19,6 +21,7 @@ const {
{
disabled ? (
<span
id={id}
class:list={["group inline-block", className]}
title={title}
aria-disabled={disabled}
@ -27,8 +30,9 @@ const {
</span>
) : (
<a
id={id}
{href}
class:list={["group inline-block hover:text-skin-accent", className]}
class:list={["group inline-block hover:text-accent", className]}
aria-label={ariaLabel}
title={title}
>

View file

@ -1,7 +1,9 @@
---
import type { Page } from "astro";
import LinkButton from "./LinkButton.astro";
import type { CollectionEntry } from "astro:content";
import IconArrowLeft from "@/assets/icons/IconArrowLeft.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import LinkButton from "./LinkButton.astro";
export interface Props {
page: Page<CollectionEntry<"blog">>;
@ -12,48 +14,26 @@ const { page } = Astro.props;
{
page.lastPage > 1 && (
<nav class="pagination-wrapper" aria-label="Pagination">
<nav class="mt-auto mb-8 flex justify-center" aria-label="Pagination">
<LinkButton
disabled={!page.url.prev}
href={page.url.prev as string}
className={`mr-4 select-none ${page.url.prev ? "" : "disabled"}`}
class:list={["mr-4 select-none", { "opacity-50": !page.url.prev }]}
ariaLabel="Previous"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class:list={[{ "disabled-svg": !page.url.prev }]}
>
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
</svg>
<IconArrowLeft class="inline-block" />
Prev
</LinkButton>
{page.currentPage} / {page.lastPage}
<LinkButton
disabled={!page.url.next}
href={page.url.next as string}
className={`mx-4 select-none ${page.url.next ? "" : "disabled"}`}
class:list={["ml-4 select-none", { "opacity-50": !page.url.next }]}
ariaLabel="Next"
>
Next
<svg
xmlns="http://www.w3.org/2000/svg"
class:list={[{ "disabled-svg": !page.url.next }]}
>
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
</svg>
<IconArrowRight class="inline-block" />
</LinkButton>
</nav>
)
}
<style>
.pagination-wrapper {
@apply mb-8 mt-auto flex justify-center;
}
.disabled {
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
}
.disabled-svg {
@apply group-hover:!fill-skin-base;
}
</style>

View file

@ -1,120 +0,0 @@
import Fuse from "fuse.js";
import { useEffect, useRef, useState, useMemo, type FormEvent } from "react";
import Card from "@components/Card";
import type { CollectionEntry } from "astro:content";
export type SearchItem = {
title: string;
description: string;
data: CollectionEntry<"blog">["data"];
slug: string;
};
interface Props {
searchList: SearchItem[];
}
interface SearchResult {
item: SearchItem;
refIndex: number;
}
export default function SearchBar({ searchList }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputVal, setInputVal] = useState("");
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(
null
);
const handleChange = (e: FormEvent<HTMLInputElement>) => {
setInputVal(e.currentTarget.value);
};
const fuse = useMemo(
() =>
new Fuse(searchList, {
keys: ["title", "description"],
includeMatches: true,
minMatchCharLength: 2,
threshold: 0.5,
}),
[searchList]
);
useEffect(() => {
// if URL has search query,
// insert that search query in input field
const searchUrl = new URLSearchParams(window.location.search);
const searchStr = searchUrl.get("q");
if (searchStr) setInputVal(searchStr);
// put focus cursor at the end of the string
setTimeout(function () {
inputRef.current!.selectionStart = inputRef.current!.selectionEnd =
searchStr?.length || 0;
}, 50);
}, []);
useEffect(() => {
// Add search result only if
// input value is more than one character
const inputResult = inputVal.length > 1 ? fuse.search(inputVal) : [];
setSearchResults(inputResult);
// Update search string in URL
if (inputVal.length > 0) {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("q", inputVal);
const newRelativePathQuery =
window.location.pathname + "?" + searchParams.toString();
history.replaceState(history.state, "", newRelativePathQuery);
} else {
history.replaceState(history.state, "", window.location.pathname);
}
}, [inputVal]);
return (
<>
<label className="relative block">
<span className="absolute inset-y-0 left-0 flex items-center pl-2 opacity-75">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path>
</svg>
<span className="sr-only">Search</span>
</span>
<input
className="block w-full rounded border border-skin-fill/40 bg-skin-fill py-3 pl-10 pr-3 placeholder:italic focus:border-skin-accent focus:outline-none"
placeholder="Search for anything..."
type="text"
name="search"
value={inputVal}
onChange={handleChange}
autoComplete="off"
// autoFocus
ref={inputRef}
/>
</label>
{inputVal.length > 1 && (
<div className="mt-8">
Found {searchResults?.length}
{searchResults?.length && searchResults?.length === 1
? " result"
: " results"}{" "}
for '{inputVal}'
</div>
)}
<ul>
{searchResults &&
searchResults.map(({ item, refIndex }) => (
<Card
href={`/posts/${item.slug}/`}
frontmatter={item.data}
key={`${refIndex}-${item.slug}`}
/>
))}
</ul>
</>
);
}

View file

@ -1,71 +1,26 @@
---
import { SHARE_LINKS } from "@/constants";
import LinkButton from "./LinkButton.astro";
import socialIcons from "@assets/socialIcons";
const URL = Astro.url;
const shareLinks = [
{
Bluesky: "Bluesky",
href: "https://bsky.app/intent/compose?text=",
linkTitle: `Share this post on Bluesky`,
},
{
LinkedIn: "LinkedIn",
href: "https://www.linkedin.com/sharing/share-offsite/?url=",
linkTitle: `Share this post on LinkedIn`,
},
{
name: "Facebook",
href: "https://www.facebook.com/sharer.php?u=",
linkTitle: `Share this post on Facebook`,
},
{
name: "X",
href: "https://x.com/intent/post?url=",
linkTitle: `Share this post on X`,
},
{
name: "Telegram",
href: "https://t.me/share/url?url=",
linkTitle: `Share this post via Telegram`,
},
{
name: "Pinterest",
href: "https://pinterest.com/pin/create/button/?url=",
linkTitle: `Share this post on Pinterest`,
},
{
name: "Mail",
href: "mailto:?subject=See%20this%20post&body=",
linkTitle: `Share this post via email`,
},
] as const;
---
<div class={`social-icons`}>
<div
class="flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start"
>
<span class="italic">Share this post on:</span>
<div class="text-center">
{
shareLinks.map(social => (
SHARE_LINKS.map(social => (
<LinkButton
href={`${social.href + URL}`}
className="link-button"
class="scale-90 p-2 hover:rotate-6 sm:p-1"
title={social.linkTitle}
>
<Fragment set:html={socialIcons[social.name]} />
<social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
<span class="sr-only">{social.linkTitle}</span>
</LinkButton>
))
}
</div>
</div>
<style>
.social-icons {
@apply flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start;
}
.link-button {
@apply scale-90 p-2 hover:rotate-6 sm:p-1;
}
</style>

20
src/components/Socials.astro Executable file → Normal file
View file

@ -1,7 +1,6 @@
---
import { SOCIALS } from "@config";
import { SOCIALS } from "@/constants";
import LinkButton from "./LinkButton.astro";
import socialIcons from "@assets/socialIcons";
export interface Props {
centered?: boolean;
@ -10,26 +9,17 @@ export interface Props {
const { centered = false } = Astro.props;
---
<div class={`social-icons ${centered ? "flex" : ""}`}>
<div class:list={["flex-wrap justify-center gap-1", { flex: centered }]}>
{
SOCIALS.filter(social => social.active).map(social => (
SOCIALS.map(social => (
<LinkButton
href={social.href}
className="link-button"
class="p-2 hover:rotate-6 sm:p-1"
title={social.linkTitle}
>
<Fragment set:html={socialIcons[social.name]} />
<social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
<span class="sr-only">{social.linkTitle}</span>
</LinkButton>
))
}
</div>
<style>
.social-icons {
@apply flex-wrap justify-center gap-1;
}
.link-button {
@apply p-2 hover:rotate-6 sm:p-1;
}
</style>

View file

@ -1,38 +1,36 @@
---
import IconHash from "@/assets/icons/IconHash.svg";
export interface Props {
tag: string;
tagName: string;
size?: "sm" | "lg";
}
const { tag, size = "sm" } = Astro.props;
const { tag, tagName, size = "sm" } = Astro.props;
---
<li
class={`inline-block ${
size === "sm" ? "my-1 underline-offset-4" : "my-3 mx-1 underline-offset-8"
}`}
class:list={[
"group inline-block group-hover:cursor-pointer",
size === "sm" ? "my-1 underline-offset-4" : "mx-1 my-3 underline-offset-8",
]}
>
<a
href={`/tags/${tag}/`}
transition:name={tag}
class={`${size === "sm" ? "text-sm" : "text-lg"} pr-2 group`}
class:list={[
"relative pr-2 text-lg underline decoration-dashed group-hover:-top-0.5 group-hover:text-accent focus-visible:p-1",
{ "text-sm": size === "sm" },
]}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class={`${size === "sm" ? " scale-75" : "scale-110"}`}
><path
d="M16.018 3.815 15.232 8h-4.966l.716-3.815-1.964-.37L8.232 8H4v2h3.857l-.751 4H3v2h3.731l-.714 3.805 1.965.369L8.766 16h4.966l-.714 3.805 1.965.369.783-4.174H20v-2h-3.859l.751-4H21V8h-3.733l.716-3.815-1.965-.37zM14.106 14H9.141l.751-4h4.966l-.752 4z"
></path>
</svg>
&nbsp;<span>{tag}</span>
<IconHash
class:list={[
"inline-block opacity-80",
{ "-mr-3.5 size-4": size === "sm" },
{ "-mr-5 size-6": size === "lg" },
]}
/>
&nbsp;<span>{tagName}</span>
</a>
</li>
<style>
a {
@apply relative underline decoration-dashed hover:-top-0.5 hover:text-skin-accent focus-visible:p-1;
}
a svg {
@apply -mr-5 h-6 w-6 scale-95 text-skin-base opacity-80 group-hover:fill-skin-accent;
}
</style>