refactor!: remove react dependency for UI interactions (#457)

Remove react as a dependency for UI interactions.
Update jsx components such as Search and Datetime
with astro components.
This commit is contained in:
Sat Naing
2025-03-02 21:05:36 +07:00
parent 846315a265
commit 35429176e0
20 changed files with 467 additions and 925 deletions
+37
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>
-35
View File
@@ -1,35 +0,0 @@
import type { CollectionEntry } from "astro:content";
import { slugifyStr } from "@/utils/slugify";
import { Datetime } from "./Datetime";
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-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>
);
}
+66
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>
-76
View File
@@ -1,76 +0,0 @@
import { LOCALE } from "@/constants";
interface DatetimesProps {
pubDatetime: string | Date;
modDatetime: string | Date | undefined | null;
}
interface Props extends DatetimesProps {
size?: "sm" | "lg";
className?: string;
}
export function Datetime({
pubDatetime,
modDatetime,
size = "sm",
className = "",
}: Props) {
return (
<div className={`flex items-end 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-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
className={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}
>
Updated:
</span>
) : (
<span className="sr-only">Published:</span>
)}
<span
className={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}
>
<FormattedDatetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
/>
</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>
</>
);
};
-135
View File
@@ -1,135 +0,0 @@
import Fuse from "fuse.js";
import type { CollectionEntry } from "astro:content";
import { useEffect, useRef, useState, useMemo, type FormEvent } from "react";
import Card from "@/components/Card";
export type SearchItem = {
title: string;
description: string;
data: CollectionEntry<"blog">["data"];
slug: string;
};
interface Props {
searchList: SearchItem[];
backUrl: string;
}
interface SearchResult {
item: SearchItem;
refIndex: number;
}
export default function SearchBar({ searchList, backUrl }: 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);
}, []);
let newRelativePathQuery = "";
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);
newRelativePathQuery =
window.location.pathname + "?" + searchParams.toString();
history.replaceState(history.state, "", newRelativePathQuery);
} else {
history.replaceState(history.state, "", window.location.pathname);
}
}, [inputVal]);
useEffect(() => {
// focus on text input when search bar is displayed
if (inputRef.current) {
inputRef.current.focus();
}
}, [inputVal]);
const fullBackUrl = `${backUrl}${inputVal.trim() !== "" ? `?q=${inputVal}` : "/"}`;
return (
<>
<label className="relative block">
<span className="sr-only">Search</span>
<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"
className="inline-block size-6 fill-foreground"
>
<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>
<input
className="border-skin-fill/40 bg-skin-fill focus:border-skin-accent block w-full rounded border py-3 pr-3 pl-10 placeholder:italic 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}${fullBackUrl}`}
frontmatter={item.data}
key={`${refIndex}-${item.slug}`}
/>
))}
</ul>
</>
);
}