mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-09 21:07:49 +00:00
enable tsconfig strict mode & fix type errors
This commit is contained in:
@@ -36,19 +36,24 @@ export default defineConfig({
|
||||
lastmod: new Date(),
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: Object.keys(languages).reduce((prev, key) => {
|
||||
prev[key] = fileToOpenGraphConverter(key as keyof typeof languages);
|
||||
return prev;
|
||||
}, {}),
|
||||
locales: Object.keys(languages).reduce(
|
||||
(prev, key) => {
|
||||
prev[key] = fileToOpenGraphConverter(key as keyof typeof languages);
|
||||
return prev;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
),
|
||||
},
|
||||
filter(page) {
|
||||
// return true, unless lart part of the URL ends with "_noindex"
|
||||
// in which case it should not be in the sitemap
|
||||
return !page
|
||||
.split("/")
|
||||
.filter((part) => !!part.length)
|
||||
.at(-1)
|
||||
.endsWith("_noindex");
|
||||
return !(
|
||||
page
|
||||
.split("/")
|
||||
.filter((part) => !!part.length)
|
||||
.at(-1)
|
||||
?.endsWith("_noindex") ?? false
|
||||
);
|
||||
},
|
||||
serialize({ url, ...rest }) {
|
||||
return {
|
||||
|
||||
@@ -58,9 +58,9 @@ async function generateCollectionEPub(
|
||||
collectionPosts: PostInfo[],
|
||||
fileLocation: string,
|
||||
) {
|
||||
const authors = collection.authors.map((id) => {
|
||||
return getUnicornById(id, collection.locale).name;
|
||||
});
|
||||
const authors = collection.authors
|
||||
.map((id) => getUnicornById(id, collection.locale)?.name)
|
||||
.filter((name): name is string => !!name);
|
||||
|
||||
const epub = new EPub(
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ const createPostIndex = () => {
|
||||
getFn: (post) => {
|
||||
return post.authors
|
||||
.map((id) => api.getUnicornById(id, post.locale))
|
||||
.flatMap((author) => Object.values(author.socials))
|
||||
.flatMap((author) => Object.values(author!.socials))
|
||||
.join(", ");
|
||||
},
|
||||
weight: 1.2,
|
||||
@@ -60,7 +60,7 @@ const createCollectionIndex = () => {
|
||||
name: "authorName",
|
||||
getFn: (post) => {
|
||||
return post.authors
|
||||
.map((id) => api.getUnicornById(id, post.locale).name)
|
||||
.map((id) => api.getUnicornById(id, post.locale)!.name)
|
||||
.join(", ");
|
||||
},
|
||||
weight: 1.8,
|
||||
@@ -70,7 +70,7 @@ const createCollectionIndex = () => {
|
||||
getFn: (post) => {
|
||||
return post.authors
|
||||
.map((id) => api.getUnicornById(id, post.locale))
|
||||
.flatMap((author) => Object.values(author.socials))
|
||||
.flatMap((author) => Object.values(author!.socials))
|
||||
.join(", ");
|
||||
},
|
||||
weight: 1.2,
|
||||
@@ -87,12 +87,13 @@ const createCollectionIndex = () => {
|
||||
const postIndex = createPostIndex();
|
||||
const collectionIndex = createCollectionIndex();
|
||||
|
||||
const unicorns: Record<string, UnicornInfo> = api
|
||||
.getUnicornsByLang("en")
|
||||
.reduce((obj, unicorn) => {
|
||||
const unicorns = api.getUnicornsByLang("en").reduce(
|
||||
(obj, unicorn) => {
|
||||
obj[unicorn.id] = unicorn;
|
||||
return obj;
|
||||
}, {});
|
||||
},
|
||||
{} as Record<string, UnicornInfo>,
|
||||
);
|
||||
|
||||
const json = JSON.stringify({
|
||||
postIndex,
|
||||
|
||||
@@ -4,8 +4,10 @@ import style from "./banner-css";
|
||||
import classnames from "classnames";
|
||||
import tags from "../../../content/data/tags.json";
|
||||
import fs from "fs";
|
||||
import { TagInfo } from "types/TagInfo";
|
||||
|
||||
const TAG_SVG_DEFAULT = fs.readFileSync("public/stickers/role_devops.svg", "utf-8");
|
||||
const tagsMap = new Map(Object.entries(tags));
|
||||
|
||||
function BannerCodeScreen({
|
||||
post,
|
||||
@@ -19,8 +21,9 @@ function BannerCodeScreen({
|
||||
const rotX = (post.description.length % 20) - 10;
|
||||
const rotY = (post.title.length * 3) % 20;
|
||||
|
||||
const tagInfo = post.tags.map(tag => tags[tag])
|
||||
.filter(t => t?.emoji || (t?.image && t?.shownWithBranding))[0];
|
||||
const tagInfo = post.tags.map(tag => tagsMap.get(tag))
|
||||
.filter((t): t is TagInfo => !!t)
|
||||
.filter(t => t.emoji || (t.image && t.shownWithBranding))[0];
|
||||
|
||||
const tagSvg = tagInfo?.image
|
||||
? fs.readFileSync("public" + tagInfo.image, "utf-8")
|
||||
|
||||
@@ -72,7 +72,7 @@ const TwitterLargeCard = ({
|
||||
</div>
|
||||
<div class="postInfo">
|
||||
<span class="authors">
|
||||
{post.authors.map((id) => getUnicornById(id, post.locale).name).join(", ")}
|
||||
{post.authors.map((id) => getUnicornById(id, post.locale)!.name).join(", ")}
|
||||
</span>
|
||||
<span class="date">
|
||||
{post.publishedMeta} · {post.wordCount.toLocaleString("en")} words
|
||||
|
||||
@@ -13,7 +13,7 @@ export const layouts: Layout[] = [banner, twitterPreview];
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const post = getPostBySlug("async-pipe-is-not-pure", "en");
|
||||
const post = getPostBySlug("async-pipe-is-not-pure", "en")!;
|
||||
|
||||
const rebuild = async () => {
|
||||
console.log("rebuilding...");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PostInfo } from "types/index";
|
||||
import { render } from "preact-render-to-string";
|
||||
import { createElement } from "preact";
|
||||
import { VNode, createElement } from "preact";
|
||||
import sharp from "sharp";
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
@@ -68,7 +68,7 @@ export const renderPostPreviewToString = async (
|
||||
const authorImageMap = Object.fromEntries(
|
||||
await Promise.all(
|
||||
post.authors.map(async (authorId) => {
|
||||
const author = getUnicornById(authorId, post.locale);
|
||||
const author = getUnicornById(authorId, post.locale)!;
|
||||
|
||||
if (authorImageCache.has(author.id))
|
||||
return [author.id, authorImageCache.get(author.id)];
|
||||
@@ -113,7 +113,7 @@ export const renderPostPreviewToString = async (
|
||||
width: PAGE_WIDTH,
|
||||
height: PAGE_HEIGHT,
|
||||
authorImageMap,
|
||||
}),
|
||||
}) as VNode<{}>,
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/json5": "^2.2.0",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/probe-image-size": "^7.2.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
@@ -7273,6 +7274,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
|
||||
},
|
||||
"node_modules/@types/needle": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/needle/-/needle-3.2.2.tgz",
|
||||
"integrity": "sha512-xUKAjFjDcucpgfyTvnbaqN+WBKyM9UehBuVRI/1AoPbaXrCScADqeTgTM1ZBYnS3Ovs9IEQt813IcJNyac7dNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nlcst": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.1.tgz",
|
||||
@@ -7309,6 +7319,16 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/probe-image-size": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.2.2.tgz",
|
||||
"integrity": "sha512-rNWbJLj3XdCfiHsaDVTkPgvTtxlVMgNEpGpJYU/d2pVwzIpjumZguQzlnOQPWF7ajPOpX00rmPQGerRlB3tL+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/needle": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/json5": "^2.2.0",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/probe-image-size": "^7.2.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useRadioGroupState,
|
||||
} from "react-stately";
|
||||
|
||||
const RadioContext = createContext<RadioGroupState>(null);
|
||||
const RadioContext = createContext<RadioGroupState | null>(null);
|
||||
|
||||
interface RadioButtonGroupProps extends PropsWithChildren<RadioGroupProps> {
|
||||
class?: string;
|
||||
@@ -51,6 +51,10 @@ export function RadioButtonGroup(props: RadioButtonGroupProps) {
|
||||
export function RadioButton(props: AriaRadioProps) {
|
||||
const { children } = props;
|
||||
const state = useContext(RadioContext);
|
||||
if (!state) {
|
||||
throw new Error("<RadioButton> must only be used within a <RadioButtonGroup>!");
|
||||
}
|
||||
|
||||
const ref = useRef(null);
|
||||
const { inputProps, isSelected } = useRadio(props, state, ref);
|
||||
const { isFocusVisible, focusProps } = useFocusRing();
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { JSXNode, PropsWithChildren } from "../types";
|
||||
import { createElement, Ref, VNode } from "preact";
|
||||
import { JSX } from "preact";
|
||||
import { forwardRef } from "preact/compat";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { ForwardedRef, forwardRef } from "preact/compat";
|
||||
|
||||
type AllowedTags = "a" | "button" | "span" | "div";
|
||||
|
||||
type AllowedElements<Tag extends AllowedTags> = (
|
||||
Tag extends "a"
|
||||
? HTMLAnchorElement
|
||||
: Tag extends "div"
|
||||
? HTMLDivElement
|
||||
: Tag extends "span"
|
||||
? HTMLSpanElement
|
||||
: HTMLButtonElement
|
||||
);
|
||||
|
||||
type ButtonProps<Tag extends AllowedTags> = PropsWithChildren<
|
||||
{
|
||||
tag?: Tag;
|
||||
@@ -19,19 +27,11 @@ type ButtonProps<Tag extends AllowedTags> = PropsWithChildren<
|
||||
| "secondary-emphasized"
|
||||
| "primary"
|
||||
| "secondary";
|
||||
} & JSX.HTMLAttributes<
|
||||
Tag extends "a"
|
||||
? HTMLAnchorElement
|
||||
: Tag extends "div"
|
||||
? HTMLDivElement
|
||||
: Tag extends "span"
|
||||
? HTMLSpanElement
|
||||
: HTMLButtonElement
|
||||
>
|
||||
} & JSX.HTMLAttributes<AllowedElements<Tag>>
|
||||
>;
|
||||
|
||||
const ButtonWrapper = forwardRef(
|
||||
<T extends AllowedTags = "a">(
|
||||
const ButtonWrapper = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
|
||||
(
|
||||
{
|
||||
tag = "a" as never,
|
||||
class: className,
|
||||
@@ -41,16 +41,8 @@ const ButtonWrapper = forwardRef(
|
||||
rightIcon,
|
||||
isFocusVisible,
|
||||
...props
|
||||
}: ButtonProps<T>,
|
||||
ref: Ref<
|
||||
T extends "a"
|
||||
? HTMLAnchorElement
|
||||
: T extends "div"
|
||||
? HTMLDivElement
|
||||
: T extends "span"
|
||||
? HTMLSpanElement
|
||||
: HTMLButtonElement
|
||||
>,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Wrapper: any = tag;
|
||||
@@ -85,10 +77,10 @@ const ButtonWrapper = forwardRef(
|
||||
},
|
||||
);
|
||||
|
||||
export const Button = forwardRef(
|
||||
<T extends AllowedTags = "a">(
|
||||
{ class: className = "", ...props }: ButtonProps<T>,
|
||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
||||
export const Button = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
|
||||
(
|
||||
{ class: className = "", ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<ButtonWrapper
|
||||
@@ -100,10 +92,10 @@ export const Button = forwardRef(
|
||||
},
|
||||
);
|
||||
|
||||
export const LargeButton = forwardRef(
|
||||
<T extends AllowedTags = "a">(
|
||||
{ class: className = "", ...props }: ButtonProps<T>,
|
||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
||||
export const LargeButton = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
|
||||
(
|
||||
{ class: className = "", ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<ButtonWrapper
|
||||
@@ -120,10 +112,10 @@ type IconOnlyButtonProps<T extends AllowedTags = "a"> = Omit<
|
||||
"leftIcon" | "rightIcon"
|
||||
>;
|
||||
|
||||
export const IconOnlyButton = forwardRef(
|
||||
<T extends AllowedTags = "a">(
|
||||
{ class: className = "", children, ...props }: IconOnlyButtonProps<T>,
|
||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
||||
export const IconOnlyButton = forwardRef<AllowedElements<AllowedTags> | null, IconOnlyButtonProps<AllowedTags>>(
|
||||
(
|
||||
{ class: className = "", children, ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<ButtonWrapper
|
||||
@@ -139,10 +131,10 @@ export const IconOnlyButton = forwardRef(
|
||||
},
|
||||
);
|
||||
|
||||
export const LargeIconOnlyButton = forwardRef(
|
||||
<T extends AllowedTags = "a">(
|
||||
{ class: className = "", children, ...props }: IconOnlyButtonProps<T>,
|
||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
||||
export const LargeIconOnlyButton = forwardRef<AllowedElements<AllowedTags> | null, IconOnlyButtonProps<AllowedTags>>(
|
||||
(
|
||||
{ class: className = "", children, ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<ButtonWrapper {...props} class={`iconOnly large ${className}`} ref={ref}>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const CollectionCard = ({
|
||||
className={`text-style-button-regular ${style.authorListItem}`}
|
||||
>
|
||||
<UUPicture
|
||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)}
|
||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
|
||||
alt=""
|
||||
class={style.authorImage}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Dialog(props: DialogProps) {
|
||||
if (dialogRef.current) {
|
||||
if (props.open && !dialogRef.current.open) {
|
||||
// reset the return value when re-opening the dialog
|
||||
dialogRef.current.returnValue = undefined;
|
||||
dialogRef.current.returnValue = "";
|
||||
dialogRef.current.showModal();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const onSoftNavClick =
|
||||
(softNavigate: (href: string) => void) => (e: MouseEvent) => {
|
||||
let link = e.target as HTMLElement;
|
||||
let link = e.target as HTMLElement | null;
|
||||
// Could click on a child element of an anchor
|
||||
while (link && !(link instanceof HTMLAnchorElement)) {
|
||||
link = link.parentElement;
|
||||
|
||||
@@ -21,7 +21,7 @@ import { DOMProps } from "@react-types/shared";
|
||||
|
||||
function PopupContents(
|
||||
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> & {
|
||||
titleId: string;
|
||||
titleId?: string;
|
||||
close: () => void;
|
||||
},
|
||||
) {
|
||||
@@ -32,6 +32,8 @@ function PopupContents(
|
||||
class={style.popupInner}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!props.getPageHref) return;
|
||||
|
||||
if (props.softNavigate) {
|
||||
props.softNavigate(props.getPageHref(count));
|
||||
props.close();
|
||||
|
||||
@@ -23,7 +23,7 @@ export function PostCardGrid({
|
||||
return (
|
||||
<ul {...props} class={style.list} role="list" id="post-list-container">
|
||||
{postsToDisplay.map((post, i) => {
|
||||
const authors = post.authors.map(id => postAuthors.get(id))
|
||||
const authors = post.authors.map(id => postAuthors.get(id)!)
|
||||
.filter(u => !!u);
|
||||
|
||||
return expanded && post.bannerImg ? (
|
||||
|
||||
@@ -213,8 +213,10 @@ function ListBox(props: ListBoxProps) {
|
||||
|
||||
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
|
||||
// ...but nothing should be using these IDs anyway.
|
||||
listBoxProps["id"] = undefined;
|
||||
listBoxProps["aria-labelledby"] = undefined;
|
||||
Object.assign(listBoxProps, {
|
||||
["id"]: undefined,
|
||||
["aria-labelledby"]: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<ul {...listBoxProps} ref={listBoxRef} class={styles.optionsList}>
|
||||
@@ -242,8 +244,10 @@ export function Option({ item, state }: OptionProps) {
|
||||
|
||||
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
|
||||
// ...but nothing should be using these IDs anyway.
|
||||
optionProps["aria-labelledby"] = undefined;
|
||||
optionProps["aria-describedby"] = undefined;
|
||||
Object.assign(optionProps, {
|
||||
["aria-labelledby"]: undefined,
|
||||
["aria-describedby"]: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<li
|
||||
|
||||
@@ -7,12 +7,13 @@ interface UseElementSizeProps {
|
||||
export const useElementSize = ({
|
||||
includeMargin = true,
|
||||
}: UseElementSizeProps = {}) => {
|
||||
const [el, setEl] = useState<HTMLElement>(null);
|
||||
const [el, setEl] = useState<HTMLElement|null>(null);
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!el) return;
|
||||
function getElementSize() {
|
||||
if (!el) return;
|
||||
const style = window.getComputedStyle(el);
|
||||
let calculatedHeight = el.offsetHeight;
|
||||
let calculatedWidth = el.offsetWidth;
|
||||
|
||||
@@ -32,8 +32,8 @@ export const get = () => {
|
||||
.map((id) => getUnicornById(id, post.locale))
|
||||
.map((author) => {
|
||||
return {
|
||||
name: author.name,
|
||||
link: `${siteUrl}/unicorns/${author.id}`,
|
||||
name: author!.name,
|
||||
link: `${siteUrl}/unicorns/${author!.id}`,
|
||||
};
|
||||
}),
|
||||
date: new Date(post.published),
|
||||
|
||||
@@ -25,7 +25,7 @@ const userLogins = getUnicornsByLang("en")
|
||||
.filter((unicorn) => !!unicorn.socials.github)
|
||||
.map((unicorn) => unicorn.socials.github);
|
||||
|
||||
const userResult: Record<string, { id: string }> = await octokit?.graphql(`
|
||||
const userResult = (await octokit?.graphql(`
|
||||
query {
|
||||
${userLogins.map(
|
||||
(login, i) => `
|
||||
@@ -35,12 +35,12 @@ query {
|
||||
`,
|
||||
)}
|
||||
}
|
||||
`);
|
||||
`)) as Record<string, { id: string }>;
|
||||
|
||||
const userIds: Record<string, string> = {};
|
||||
if (userResult) {
|
||||
userLogins.forEach((login, i) => {
|
||||
userIds[login] = userResult[`user${i}`].id;
|
||||
if (login !== undefined) userIds[login] = userResult[`user${i}`].id;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -80,44 +80,44 @@ export async function* getAchievements(
|
||||
};
|
||||
}
|
||||
|
||||
if (data?.issueCount >= 25) {
|
||||
if (data && data.issueCount >= 25) {
|
||||
yield {
|
||||
name: "Insect infestation!",
|
||||
body: `Open 25 issues in our GitHub repo`,
|
||||
};
|
||||
} else if (data?.issueCount >= 10) {
|
||||
} else if (data && data.issueCount >= 10) {
|
||||
yield {
|
||||
name: "Creepy crawlies!",
|
||||
body: "Open 10 issues in our GitHub repo",
|
||||
};
|
||||
} else if (data?.issueCount > 0) {
|
||||
} else if (data && data.issueCount > 0) {
|
||||
yield {
|
||||
name: "Bug!",
|
||||
body: "Open an issue in our GitHub repo",
|
||||
};
|
||||
}
|
||||
|
||||
if (data?.pullRequestCount >= 30) {
|
||||
if (data && data.pullRequestCount >= 30) {
|
||||
yield {
|
||||
name: "Rabid Requester",
|
||||
body: `Open 30 pull requests in our GitHub repo`,
|
||||
};
|
||||
} else if (data?.pullRequestCount >= 10) {
|
||||
} else if (data && data.pullRequestCount >= 10) {
|
||||
yield {
|
||||
name: "Request Rampage",
|
||||
body: "Open 10 pull requests in our GitHub repo",
|
||||
};
|
||||
} else if (data?.pullRequestCount >= 5) {
|
||||
} else if (data && data.pullRequestCount >= 5) {
|
||||
yield {
|
||||
name: "Request Robot",
|
||||
body: "Open 5 pull requests in our GitHub repo",
|
||||
};
|
||||
} else if (data?.pullRequestCount >= 3) {
|
||||
} else if (data && data.pullRequestCount >= 3) {
|
||||
yield {
|
||||
name: "Request Racer",
|
||||
body: "Open 3 pull requests in our GitHub repo",
|
||||
};
|
||||
} else if (data?.pullRequestCount > 0) {
|
||||
} else if (data && data.pullRequestCount > 0) {
|
||||
yield {
|
||||
name: "Request Ranger",
|
||||
body: "Open a pull request in our GitHub repo",
|
||||
@@ -142,7 +142,7 @@ export async function* getAchievements(
|
||||
}
|
||||
|
||||
for (const year of contributorYears) {
|
||||
if (data?.commitsInYear?.includes(year)) {
|
||||
if (data && data.commitsInYear?.includes(year)) {
|
||||
yield {
|
||||
name: `${year} Contributor`,
|
||||
body: `Make a commit to the site in ${year}!`,
|
||||
|
||||
@@ -18,6 +18,7 @@ export function getUnicornById(
|
||||
language: Languages,
|
||||
): UnicornInfo | undefined {
|
||||
const locales = unicorns.get(id);
|
||||
if (!locales) return undefined;
|
||||
return locales.find((u) => u.locale === language) || locales[0];
|
||||
}
|
||||
|
||||
@@ -51,7 +52,9 @@ export function getPostsByCollection(
|
||||
.map((locales) => locales.find((p) => p.locale === language) || locales[0])
|
||||
.filter((p) => p?.collection === collectionSlug)
|
||||
.filter((p) => !p.noindex)
|
||||
.sort((postA, postB) => (postA.order > postB.order ? 1 : -1));
|
||||
.sort((postA, postB) =>
|
||||
Number(postA.order) > Number(postB.order) ? 1 : -1,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPostsByUnicorn(
|
||||
|
||||
@@ -49,7 +49,7 @@ const tagExplainerParser = unified()
|
||||
|
||||
for (const [key, tag] of Object.entries(tagsRaw)) {
|
||||
let explainer = undefined;
|
||||
let explainerType = undefined;
|
||||
let explainerType: TagInfo["explainerType"] | undefined = undefined;
|
||||
|
||||
if ("image" in tag && tag.image.endsWith(".svg")) {
|
||||
const license = await fs
|
||||
@@ -100,6 +100,9 @@ async function readUnicorn(unicornPath: string): Promise<UnicornInfo[]> {
|
||||
const frontmatter = matter(fileContents).data as RawUnicornInfo;
|
||||
|
||||
const profileImgSize = getImageSize(frontmatter.profileImg, unicornPath);
|
||||
if (!profileImgSize) {
|
||||
throw new Error(`${unicornPath}: Unable to parse profile image size`);
|
||||
}
|
||||
|
||||
const unicorn: UnicornInfo = {
|
||||
pronouns: "",
|
||||
@@ -115,7 +118,7 @@ async function readUnicorn(unicornPath: string): Promise<UnicornInfo[]> {
|
||||
profileImgMeta: {
|
||||
height: profileImgSize.height as number,
|
||||
width: profileImgSize.width as number,
|
||||
...resolvePath(frontmatter.profileImg, unicornPath),
|
||||
...resolvePath(frontmatter.profileImg, unicornPath)!,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -178,14 +181,17 @@ async function readCollection(
|
||||
const frontmatter = matter(fileContents).data as RawCollectionInfo;
|
||||
|
||||
const coverImgSize = getImageSize(frontmatter.coverImg, collectionPath);
|
||||
if (!coverImgSize) {
|
||||
throw new Error(`${collectionPath}: Unable to parse cover image size`);
|
||||
}
|
||||
|
||||
const coverImgMeta = {
|
||||
height: coverImgSize.height as number,
|
||||
width: coverImgSize.width as number,
|
||||
...resolvePath(frontmatter.coverImg, collectionPath),
|
||||
...resolvePath(frontmatter.coverImg, collectionPath)!,
|
||||
};
|
||||
|
||||
const frontmatterTags = [...frontmatter.tags].filter((tag) => {
|
||||
const frontmatterTags = (frontmatter.tags || []).filter((tag) => {
|
||||
if (tags.has(tag)) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -267,7 +273,7 @@ async function readPost(
|
||||
// get an excerpt of the post markdown no longer than 150 chars
|
||||
const excerpt = getExcerpt(fileMatter.content, 150);
|
||||
|
||||
const frontmatterTags = [...frontmatter.tags].filter((tag) => {
|
||||
const frontmatterTags = (frontmatter.tags || []).filter((tag) => {
|
||||
if (tags.has(tag)) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
const sizeStringRegex = / ([0-9.]+)([xw])$/;
|
||||
|
||||
const parseSourceString = (source: string) => {
|
||||
const [fullMatch, sizeStr, sizeType] = sizeStringRegex.exec(source);
|
||||
const [fullMatch, sizeStr, sizeType] = sizeStringRegex.exec(
|
||||
source,
|
||||
) as string[];
|
||||
const srcStr = source.replace(fullMatch, "");
|
||||
return {
|
||||
src: srcStr,
|
||||
|
||||
@@ -46,17 +46,17 @@ const getOrderRange = (arr: PostInfo[]) => {
|
||||
smallest: curr,
|
||||
};
|
||||
}
|
||||
if (curr.order! < prev.smallest.order!) {
|
||||
if (curr.order! < prev.smallest!.order!) {
|
||||
prev.smallest = curr;
|
||||
}
|
||||
if (curr.order! > prev.largest.order!) {
|
||||
if (curr.order! > prev.largest!.order!) {
|
||||
prev.largest = curr;
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
{
|
||||
largest: null as PostInfo,
|
||||
smallest: null as PostInfo,
|
||||
largest: undefined as PostInfo | undefined,
|
||||
smallest: undefined as PostInfo | undefined,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -104,8 +104,8 @@ export const getSuggestedArticles = (postNode: PostInfo) => {
|
||||
const { largest, smallest } = getOrderRange(suggestedArticles) || {};
|
||||
for (const suggestedPost of extraSuggestedArticles) {
|
||||
if (
|
||||
suggestedPost.order === smallest.order! - 1 ||
|
||||
suggestedPost.order === largest.order! + 1
|
||||
suggestedPost.order === smallest!.order! - 1 ||
|
||||
suggestedPost.order === largest!.order! + 1
|
||||
) {
|
||||
suggestedArticles.push(suggestedPost);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,19 @@ function isNestedElement(e: MouseEvent) {
|
||||
if (target.getAttribute("data-dont-bind-navigate-click") !== null)
|
||||
return true;
|
||||
|
||||
target = target.parentElement;
|
||||
if (target.parentElement) target = target.parentElement;
|
||||
else break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
function handleHrefContainerMouseDown(e: MouseEvent): void;
|
||||
function handleHrefContainerMouseUp(e: MouseEvent): void;
|
||||
function handleHrefContainerAuxClick(e: MouseEvent): void;
|
||||
function handleHrefContainerClick(e: MouseEvent): void;
|
||||
}
|
||||
|
||||
globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
|
||||
const isMiddleClick = e.button === 1;
|
||||
if (!isMiddleClick) return;
|
||||
@@ -46,7 +54,7 @@ globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
|
||||
// implement the AuxClick event using MouseUp (only on browsers that don't support auxclick; i.e. safari)
|
||||
globalThis.handleHrefContainerMouseUp = (e: MouseEvent) => {
|
||||
// if auxclick is supported, do nothing
|
||||
if ("onauxclick" in e.currentTarget) return;
|
||||
if (e.currentTarget && "onauxclick" in e.currentTarget) return;
|
||||
// otherwise, pass mouseup events to auxclick
|
||||
globalThis.handleHrefContainerAuxClick(e);
|
||||
};
|
||||
@@ -95,7 +103,7 @@ globalThis.handleHrefContainerClick = (e: MouseEvent) => {
|
||||
)
|
||||
return;
|
||||
|
||||
window.location.href = href;
|
||||
window.location.href = String(href);
|
||||
};
|
||||
|
||||
export function getHrefContainerProps(href: string) {
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
*/
|
||||
import { toString } from "hast-util-to-string";
|
||||
import type { Child as HChild } from "hastscript";
|
||||
import { Element } from "hast";
|
||||
import { Data, Element, Parent, Node } from "hast";
|
||||
import { visit } from "unist-util-visit";
|
||||
import replaceAllBetween from "unist-util-replace-all-between";
|
||||
import { Node } from "unist";
|
||||
import JSON5 from "json5";
|
||||
import { FileList, Directory, File } from "./file-list";
|
||||
import { Root } from "postcss";
|
||||
|
||||
interface DirectoryMetadata {
|
||||
open?: boolean;
|
||||
@@ -34,19 +34,24 @@ interface DirectoryMetadata {
|
||||
interface FileMetadata {}
|
||||
|
||||
export const rehypeFileTree = () => {
|
||||
return (tree) => {
|
||||
return (tree: Root) => {
|
||||
function replaceFiletreeNodes(nodes: Node[]) {
|
||||
const items: Array<Directory | File> = [];
|
||||
|
||||
const isNodeElement = (node: unknown): node is Element =>
|
||||
typeof node === "object" && node["type"] === "element";
|
||||
(typeof node === "object" &&
|
||||
node &&
|
||||
"type" in node &&
|
||||
node["type"] === "element") ??
|
||||
false;
|
||||
|
||||
function traverseUl(listNode: Element, listItems: typeof items) {
|
||||
if (listNode.children.length === 0) return;
|
||||
|
||||
for (const listItem of listNode.children) {
|
||||
// Filter out `\n` text nodes
|
||||
if (!(isNodeElement(listItem) && listItem.tagName === "li")) continue;
|
||||
if (!(listItem.type === "element" && listItem.tagName === "li"))
|
||||
continue;
|
||||
|
||||
// Strip nodes that only contain newlines
|
||||
listItem.children = listItem.children.filter(
|
||||
@@ -165,13 +170,13 @@ export const rehypeFileTree = () => {
|
||||
}
|
||||
|
||||
replaceAllBetween(
|
||||
tree,
|
||||
tree as never as Parent,
|
||||
{ type: "raw", value: "<!-- filetree:start -->" } as never,
|
||||
{ type: "raw", value: "<!-- filetree:end -->" } as never,
|
||||
replaceFiletreeNodes,
|
||||
);
|
||||
replaceAllBetween(
|
||||
tree,
|
||||
tree as never as Parent,
|
||||
{ type: "comment", value: " filetree:start " } as never,
|
||||
{ type: "comment", value: " filetree:end " } as never,
|
||||
replaceFiletreeNodes,
|
||||
|
||||
@@ -19,15 +19,17 @@ function isNodeSummary(e: Node) {
|
||||
*/
|
||||
export const rehypeHints: Plugin<[], Root> = () => {
|
||||
return (tree) => {
|
||||
visit(tree, (node: Element, index, parent: Element) => {
|
||||
visit(tree, "element", (node: Element, index, parent) => {
|
||||
if (node.tagName !== "details") return;
|
||||
|
||||
const summaryNode = node.children.find(isNodeSummary);
|
||||
|
||||
parent.children[index] = Hint({
|
||||
title: toString(summaryNode as never),
|
||||
children: node.children.filter((e) => !isNodeSummary(e)),
|
||||
});
|
||||
if (index !== undefined && parent?.children) {
|
||||
parent.children[index] = Hint({
|
||||
title: toString(summaryNode as never),
|
||||
children: node.children.filter((e) => !isNodeSummary(e)),
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,12 +9,10 @@ export const iFrameClickToRun = () => {
|
||||
[...iframeButtons].forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(iframe as any).loading = "lazy";
|
||||
iframe.src = el.parentElement.dataset.iframeurl;
|
||||
const propsToPreserve = JSON.parse(
|
||||
el.parentElement.dataset.iframeprops || "{}",
|
||||
);
|
||||
const parent = el.parentElement!;
|
||||
iframe.loading = "lazy";
|
||||
iframe.src = String(parent.dataset.iframeurl);
|
||||
const propsToPreserve = JSON.parse(parent.dataset.iframeprops || "{}");
|
||||
for (const prop in propsToPreserve) {
|
||||
const val = propsToPreserve[prop];
|
||||
// Handle array props per hast spec:
|
||||
@@ -25,9 +23,9 @@ export const iFrameClickToRun = () => {
|
||||
}
|
||||
iframe.setAttribute(prop, propsToPreserve[prop]);
|
||||
}
|
||||
iframe.style.width = el.parentElement.style.width;
|
||||
iframe.style.height = el.parentElement.style.height;
|
||||
el.parentElement.replaceWith(iframe);
|
||||
iframe.style.width = parent.style.width;
|
||||
iframe.style.height = parent.style.height;
|
||||
parent.replaceWith(iframe);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Root, Element } from "hast";
|
||||
import { Plugin } from "unified";
|
||||
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
@@ -13,8 +12,6 @@ import type { GetPictureResult } from "@astrojs/image/dist/lib/get-picture";
|
||||
import probe from "probe-image-size";
|
||||
import { IFramePlaceholder } from "./iframe-placeholder";
|
||||
|
||||
interface RehypeUnicornIFrameClickToRunProps {}
|
||||
|
||||
// default icon, used if a frame's favicon cannot be resolved
|
||||
let defaultPageIcon: Promise<GetPictureResult>;
|
||||
function fetchDefaultPageIcon(): Promise<GetPictureResult> {
|
||||
@@ -34,20 +31,21 @@ function fetchDefaultPageIcon(): Promise<GetPictureResult> {
|
||||
// and multiple fetchPageInfo() calls can await the same icon
|
||||
const pageIconMap = new Map<string, Promise<GetPictureResult>>();
|
||||
function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
||||
if (pageIconMap.has(src.origin)) return pageIconMap.get(src.origin);
|
||||
if (pageIconMap.has(src.origin)) return pageIconMap.get(src.origin)!;
|
||||
|
||||
const promise = (async () => {
|
||||
// <link rel="manifest" href="/manifest.json">
|
||||
const manifestPath: Element = find(
|
||||
const manifestPath: Element | undefined = find(
|
||||
srcHast,
|
||||
(node: unknown) => (node as Element)?.properties?.rel?.[0] === "manifest",
|
||||
(node: unknown) =>
|
||||
(node as Element)?.properties?.rel?.toString() === "manifest",
|
||||
);
|
||||
|
||||
let iconLink: string;
|
||||
let iconLink: string | undefined;
|
||||
|
||||
if (manifestPath) {
|
||||
if (manifestPath?.properties?.href) {
|
||||
// `/manifest.json`
|
||||
const manifestRelativeURL = manifestPath.properties.href.toString();
|
||||
const manifestRelativeURL = String(manifestPath.properties.href);
|
||||
const fullManifestURL = new URL(manifestRelativeURL, src).href;
|
||||
|
||||
const manifest = await fetch(fullManifestURL)
|
||||
@@ -56,20 +54,22 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
||||
|
||||
if (manifest) {
|
||||
const largestIcon = getLargestManifestIcon(manifest);
|
||||
iconLink = new URL(largestIcon.icon.src, src.origin).href;
|
||||
if (largestIcon?.icon)
|
||||
iconLink = new URL(largestIcon.icon.src, src.origin).href;
|
||||
}
|
||||
}
|
||||
|
||||
if (!iconLink) {
|
||||
// fetch `favicon.ico`
|
||||
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
|
||||
const favicon: Element = find(
|
||||
const favicon: Element | undefined = find(
|
||||
srcHast,
|
||||
(node: unknown) =>
|
||||
(node as Element)?.properties?.rel?.toString()?.includes("icon"),
|
||||
(node as Element)?.properties?.rel?.toString()?.includes("icon") ??
|
||||
false,
|
||||
);
|
||||
|
||||
if (favicon) {
|
||||
if (favicon?.properties?.href) {
|
||||
iconLink = new URL(favicon.properties.href.toString(), src).href;
|
||||
}
|
||||
}
|
||||
@@ -96,11 +96,11 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
||||
|
||||
const pageHtmlMap = new Map<string, Promise<Root | null>>();
|
||||
function fetchPageHtml(src: string): Promise<Root | null> {
|
||||
if (pageHtmlMap.has(src)) return pageHtmlMap.get(src);
|
||||
if (pageHtmlMap.has(src)) return pageHtmlMap.get(src)!;
|
||||
|
||||
const promise = (async () => {
|
||||
const srcHTML = await fetch(src)
|
||||
.then((r) => r.status === 200 && r.text())
|
||||
.then((r) => (r.status === 200 ? r.text() : undefined))
|
||||
.catch(() => null);
|
||||
|
||||
// if fetch fails...
|
||||
@@ -129,7 +129,7 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
|
||||
if (!srcHast) return null;
|
||||
|
||||
// find <title> element in response HTML
|
||||
const titleEl: Element = find(srcHast, { tagName: "title" });
|
||||
const titleEl = find<Element>(srcHast, { tagName: "title" });
|
||||
const titleContentEl = titleEl && titleEl.children[0];
|
||||
const title =
|
||||
titleContentEl?.type === "text" ? titleContentEl.value : undefined;
|
||||
@@ -140,13 +140,10 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
|
||||
}
|
||||
|
||||
// TODO: Add switch/case and dedicated files ala "Components"
|
||||
export const rehypeUnicornIFrameClickToRun: Plugin<
|
||||
[RehypeUnicornIFrameClickToRunProps | never],
|
||||
Root
|
||||
> = () => {
|
||||
return async (tree) => {
|
||||
export const rehypeUnicornIFrameClickToRun = () => {
|
||||
return async (tree: Root) => {
|
||||
const iframeNodes: Element[] = [];
|
||||
visit(tree, (node: Element) => {
|
||||
visit(tree, "element", (node: Element) => {
|
||||
if (node.tagName === "iframe") {
|
||||
iframeNodes.push(node);
|
||||
}
|
||||
@@ -168,7 +165,7 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
|
||||
width = width ?? EMBED_SIZE.w;
|
||||
height = height ?? EMBED_SIZE.h;
|
||||
const info: PageInfo = (await fetchPageInfo(
|
||||
iframeNode.properties.src.toString(),
|
||||
String(iframeNode.properties.src),
|
||||
).catch(() => null)) || { icon: await fetchDefaultPageIcon() };
|
||||
|
||||
const [, heightPx] = /^([0-9]+)(px)?$/.exec(height + "") || [];
|
||||
@@ -177,7 +174,7 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
|
||||
const iframeReplacement = IFramePlaceholder({
|
||||
width: width.toString(),
|
||||
height: height.toString(),
|
||||
src: src.toString(),
|
||||
src: String(src),
|
||||
pageTitle: String(dataFrameTitle ?? "") || info.title || "",
|
||||
pageIcon: info.icon,
|
||||
propsToPreserve: JSON.stringify(propsToPreserve),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Plugin } from "unified";
|
||||
import rehypeSlug from "rehype-slug-custom-id";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import { rehypeTabs } from "./tabs/rehype-transform";
|
||||
@@ -17,11 +16,17 @@ import { rehypeHeaderText } from "./rehype-header-text";
|
||||
import { rehypeHeaderClass } from "./rehype-header-class";
|
||||
import { rehypeFileTree } from "./file-tree/rehype-file-tree";
|
||||
import { rehypeTwoslashTabindex } from "./twoslash-tabindex/rehype-transform";
|
||||
import { Plugin } from "unified";
|
||||
|
||||
// TODO: this does not actually validate that the adjacent config matches the type of the plugin in an array structure
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type RehypePlugin = Plugin<any[]> | [Plugin<any[]>, any];
|
||||
type RehypePlugin<T = any> =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
| (T | ((config: T) => any))[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
| (() => any);
|
||||
|
||||
export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
|
||||
export function createRehypePlugins(config: MarkdownConfig): Plugin[] {
|
||||
return [
|
||||
// This is required to handle unsafe HTML embedded into Markdown
|
||||
[rehypeRaw, { passThrough: [`mdxjsEsm`] }],
|
||||
@@ -29,7 +34,7 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
|
||||
...(config.format === "epub"
|
||||
? [
|
||||
rehypeFixTwoSlashXHTML,
|
||||
[rehypeMakeImagePathsAbsolute, { path: config.path }] as RehypePlugin,
|
||||
[rehypeMakeImagePathsAbsolute, { path: config.path }],
|
||||
rehypeMakeHrefPathsAbsolute,
|
||||
]
|
||||
: []),
|
||||
@@ -70,8 +75,8 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
|
||||
className: (depth: number) =>
|
||||
`text-style-headline-${Math.min(depth + 1, 6)}`,
|
||||
},
|
||||
] as RehypePlugin,
|
||||
],
|
||||
]
|
||||
: []),
|
||||
];
|
||||
] as RehypePlugin[] as Plugin[];
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@ import path from "path";
|
||||
*/
|
||||
import { getPicture } from "./get-picture-hack";
|
||||
import { getImageSize } from "../get-image-size";
|
||||
import { fileURLToPath } from "url";
|
||||
import { resolvePath } from "../url-paths";
|
||||
import { getLargestSourceSetSrc } from "../get-largest-source-set-src";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
import { ISizeCalculationResult } from "image-size/dist/types/interface";
|
||||
|
||||
const MAX_WIDTH = 768;
|
||||
const MAX_HEIGHT = 768;
|
||||
@@ -33,7 +31,7 @@ function getPixelValue(attr: unknown): number | undefined {
|
||||
export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
||||
return async (tree, file) => {
|
||||
const imgNodes: Element[] = [];
|
||||
visit(tree, (node: Element) => {
|
||||
visit(tree, "element", (node: Element) => {
|
||||
if (node.tagName === "img") {
|
||||
imgNodes.push(node);
|
||||
}
|
||||
@@ -72,7 +70,7 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
||||
const nodeWidth = getPixelValue(node.properties.width);
|
||||
const nodeHeight = getPixelValue(node.properties.height);
|
||||
|
||||
const dimensions = { ...srcSize };
|
||||
const dimensions = { ...srcSize } as { width: number; height: number };
|
||||
if (nodeHeight) {
|
||||
dimensions.height = nodeHeight;
|
||||
dimensions.width = Math.floor(nodeHeight * imageRatio);
|
||||
@@ -119,7 +117,7 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
||||
alt: nodeAlt || "",
|
||||
});
|
||||
|
||||
pngSource = originalPictureResult.sources.reduce(
|
||||
const newPngSource = originalPictureResult.sources.reduce(
|
||||
(prev, source) => {
|
||||
const largestSrc = getLargestSourceSetSrc(source.srcset);
|
||||
// select first option
|
||||
@@ -146,8 +144,10 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
null as ReturnType<typeof getLargestSourceSetSrc>,
|
||||
undefined as ReturnType<typeof getLargestSourceSetSrc> | undefined,
|
||||
);
|
||||
|
||||
if (newPngSource) pngSource = newPngSource;
|
||||
}
|
||||
|
||||
const sources = pictureResult.sources.map((attrs) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { hasProperty } from "hast-util-has-property";
|
||||
import { toString } from "hast-util-to-string";
|
||||
import { Root, Parent } from "hast";
|
||||
import { visit } from "unist-util-visit";
|
||||
import { AstroVFile } from "./types";
|
||||
|
||||
interface RehypeHeaderClassOpts {
|
||||
depth: number;
|
||||
@@ -14,19 +15,22 @@ interface RehypeHeaderClassOpts {
|
||||
* at the intended visual level.
|
||||
*/
|
||||
export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => {
|
||||
return (tree: Root, file) => {
|
||||
return (tree: Root, file: AstroVFile) => {
|
||||
// hacky (temporary) fix to exclude the site/about-us*.mdx files, since
|
||||
// those start at a different heading level
|
||||
if (file.data.astro.frontmatter.slug === "site") return;
|
||||
|
||||
// Find the minimum heading rank in the file
|
||||
// (e.g. if it starts at h2, minDepth = 2)
|
||||
let minDepth: number;
|
||||
let minDepth: number | undefined;
|
||||
visit(tree, "element", (node: Parent["children"][number]) => {
|
||||
const nodeHeadingRank = headingRank(node);
|
||||
if (!minDepth || nodeHeadingRank < minDepth) minDepth = nodeHeadingRank;
|
||||
if (
|
||||
!minDepth ||
|
||||
(nodeHeadingRank !== undefined && nodeHeadingRank < minDepth)
|
||||
)
|
||||
minDepth = nodeHeadingRank;
|
||||
});
|
||||
minDepth ||= 1;
|
||||
|
||||
visit(tree, "element", (node: Parent["children"][number]) => {
|
||||
const nodeHeadingRank = headingRank(node);
|
||||
@@ -42,7 +46,7 @@ export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => {
|
||||
// - when (minDepth = 5, depth = 2) h5 + 2 - 4 -> h3
|
||||
// - when (minDepth = 1, depth = 2) h1 + 2 + 0 -> h3
|
||||
const tagHeadingRank = Math.min(
|
||||
nodeHeadingRank + opts.depth + (1 - minDepth),
|
||||
nodeHeadingRank + opts.depth + (1 - (minDepth ?? 1)),
|
||||
6,
|
||||
);
|
||||
const className = opts.className(nodeHeadingRank);
|
||||
|
||||
@@ -4,12 +4,13 @@ import { toString } from "hast-util-to-string";
|
||||
import { Root, Parent } from "hast";
|
||||
import { visit } from "unist-util-visit";
|
||||
import { PostHeadingInfo } from "src/types/index";
|
||||
import { AstroVFile } from "./types";
|
||||
|
||||
/**
|
||||
* Plugin to add `data-header-text`s to headings.
|
||||
*/
|
||||
export const rehypeHeaderText = () => {
|
||||
return (tree: Root, file) => {
|
||||
return (tree: Root, file: AstroVFile) => {
|
||||
const headingsWithId: PostHeadingInfo[] =
|
||||
(file.data.astro.frontmatter.headingsWithId = []);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Root, Element } from "hast";
|
||||
import { Plugin } from "unified";
|
||||
import { visit } from "unist-util-visit";
|
||||
import { urlPathRegex, resolvePath } from "../url-paths";
|
||||
|
||||
import path from "path";
|
||||
import { AstroVFile } from "./types";
|
||||
|
||||
// TODO: Add switch/case and dedicated files ala "Components"
|
||||
export const rehypeUnicornElementMap: Plugin<[], Root> = () => {
|
||||
return async (tree, file) => {
|
||||
visit(tree, (node: Element) => {
|
||||
export const rehypeUnicornElementMap = () => {
|
||||
return async (tree: Root, file: AstroVFile) => {
|
||||
visit(tree, "element", (node: Element) => {
|
||||
if (node.tagName === "video") {
|
||||
node.properties.muted ??= true;
|
||||
node.properties.autoPlay ??= true;
|
||||
@@ -19,7 +19,7 @@ export const rehypeUnicornElementMap: Plugin<[], Root> = () => {
|
||||
|
||||
if (file.path) {
|
||||
const resolvedPath = resolvePath(
|
||||
node.properties.src.toString(),
|
||||
String(node.properties.src),
|
||||
path.dirname(file.path),
|
||||
);
|
||||
if (resolvedPath)
|
||||
|
||||
@@ -8,7 +8,7 @@ export const TwitchTransformer = {
|
||||
},
|
||||
getHTML(url: string) {
|
||||
const srcUrl = new URL(url);
|
||||
let embedUrl: URL;
|
||||
let embedUrl: URL | undefined = undefined;
|
||||
|
||||
if (srcUrl.host === "clips.twitch.tv") {
|
||||
const clipId =
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Root } from "hast";
|
||||
import replaceAllBetween from "unist-util-replace-all-between";
|
||||
import { Plugin } from "unified";
|
||||
import { getHeaderNodeId, slugs } from "rehype-slug-custom-id";
|
||||
import { Element, Node, Parent, Text } from "hast";
|
||||
import { TabInfo, Tabs } from "./tabs";
|
||||
@@ -77,8 +76,8 @@ const getApproxLineCount = (nodes: Node[], inParagraph?: boolean): number => {
|
||||
* To align with React Tabs package:
|
||||
* @see https://github.com/reactjs/react-tabs
|
||||
*/
|
||||
export const rehypeTabs: Plugin<[], Root> = () => {
|
||||
return (tree) => {
|
||||
export const rehypeTabs = () => {
|
||||
return (tree: Root) => {
|
||||
const replaceTabNodes = (nodes: Node[]) => {
|
||||
let sectionStarted = false;
|
||||
const largestSize = findLargestHeading(nodes as Element[]);
|
||||
@@ -109,18 +108,18 @@ export const rehypeTabs: Plugin<[], Root> = () => {
|
||||
}
|
||||
|
||||
// For any other heading found in the tab contents, append to the nested headers array
|
||||
if (isNodeHeading(localNode)) {
|
||||
if (isNodeHeading(localNode) && tabs.length) {
|
||||
const lastTab = tabs.at(-1);
|
||||
|
||||
// Store the related tab ID in the attributes of the header
|
||||
localNode.properties["data-tabname"] = lastTab.slug;
|
||||
localNode.properties["data-tabname"] = lastTab?.slug;
|
||||
|
||||
// Add header ID to array
|
||||
tabs.at(-1).headers.push(localNode.properties.id.toString());
|
||||
tabs.at(-1)?.headers?.push(String(localNode.properties.id));
|
||||
}
|
||||
|
||||
// Otherwise, append the node as tab content
|
||||
tabs.at(-1).contents.push(localNode);
|
||||
tabs.at(-1)?.contents?.push(localNode);
|
||||
}
|
||||
|
||||
// Determine if the set of tabs should use a constant height (via the "tabs-small" class)
|
||||
|
||||
@@ -60,6 +60,8 @@ export const enableTabs = () => {
|
||||
function handleClick(e: Event) {
|
||||
const target = e.target as HTMLElement;
|
||||
const tabName = target.dataset.tabname;
|
||||
if (!tabName) return;
|
||||
|
||||
changeTabs(tabName);
|
||||
|
||||
if (shouldScrollToTab) {
|
||||
@@ -76,32 +78,34 @@ export const enableTabs = () => {
|
||||
}
|
||||
|
||||
// Iterate through all tabs to populate tabEntries & set listeners
|
||||
document.querySelectorAll('[role="tablist"]').forEach((tabList) => {
|
||||
const entry: TabEntry = new Map();
|
||||
const parent = tabList.parentElement;
|
||||
document
|
||||
.querySelectorAll<HTMLElement>('[role="tablist"]')
|
||||
.forEach((tabList) => {
|
||||
const entry: TabEntry = new Map();
|
||||
const parent = tabList.parentElement!;
|
||||
|
||||
const tabs: NodeListOf<HTMLElement> =
|
||||
tabList.querySelectorAll('[role="tab"]');
|
||||
const tabs: NodeListOf<HTMLElement> =
|
||||
tabList.querySelectorAll('[role="tab"]');
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const panel = parent.querySelector<HTMLElement>(
|
||||
`#${tab.getAttribute("aria-controls")}`,
|
||||
);
|
||||
entry.set(tab.dataset.tabname, {
|
||||
tab,
|
||||
panel,
|
||||
tabs.forEach((tab) => {
|
||||
const panel = parent.querySelector<HTMLElement>(
|
||||
`#${tab.getAttribute("aria-controls")}`,
|
||||
)!;
|
||||
entry.set(tab.dataset.tabname!, {
|
||||
tab,
|
||||
panel,
|
||||
});
|
||||
|
||||
// Add a click event handler to each tab
|
||||
tab.addEventListener("click", handleClick);
|
||||
});
|
||||
|
||||
// Add a click event handler to each tab
|
||||
tab.addEventListener("click", handleClick);
|
||||
// Enable arrow navigation between tabs in the tab list
|
||||
tabList.addEventListener("keydown", handleKeydown);
|
||||
|
||||
tabEntries.push(entry);
|
||||
});
|
||||
|
||||
// Enable arrow navigation between tabs in the tab list
|
||||
tabList.addEventListener("keydown", handleKeydown);
|
||||
|
||||
tabEntries.push(entry);
|
||||
});
|
||||
|
||||
function changeTabs(tabName: string) {
|
||||
// find all tabs on the page that match the selected tabname
|
||||
for (const tabEntry of tabEntries) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { toString } from "hast-util-to-string";
|
||||
*/
|
||||
export const rehypeTooltips: Plugin<[], Root> = () => {
|
||||
return (tree) => {
|
||||
visit(tree, (node: Element, index, parent: Element) => {
|
||||
visit(tree, "element", (node: Element, index, parent) => {
|
||||
if (node.tagName !== "blockquote") return;
|
||||
|
||||
const firstParagraph = node.children.find((e) => e.type === "element");
|
||||
@@ -42,11 +42,13 @@ export const rehypeTooltips: Plugin<[], Root> = () => {
|
||||
// remove `firstText` from children nodes
|
||||
firstParagraph.children.splice(0, 1);
|
||||
|
||||
parent.children[index] = Tooltip({
|
||||
icon: firstText.tagName === "em" ? "warning" : "info",
|
||||
title: toString(firstText as never).replace(/:$/, ""),
|
||||
children: node.children,
|
||||
});
|
||||
if (parent?.children && index !== undefined) {
|
||||
parent.children[index] = Tooltip({
|
||||
icon: firstText.tagName === "em" ? "warning" : "info",
|
||||
title: toString(firstText as never).replace(/:$/, ""),
|
||||
children: node.children,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { visit } from "unist-util-visit";
|
||||
|
||||
export const rehypeTwoslashTabindex: Plugin<[], Root> = () => {
|
||||
return async (tree, _) => {
|
||||
visit(tree, (node: Element) => {
|
||||
visit(tree, "element", (node: Element) => {
|
||||
if (
|
||||
node.tagName === "div" &&
|
||||
node.properties.className instanceof Array &&
|
||||
|
||||
@@ -3,8 +3,8 @@ import { languages } from "../constants/index";
|
||||
import { basename } from "path";
|
||||
import { MDXInstance, MarkdownInstance } from "astro";
|
||||
|
||||
function isLanguageKey(str: string): str is Languages {
|
||||
return Object.keys(languages).includes(str);
|
||||
function isLanguageKey(str: string | undefined): str is Languages {
|
||||
return str !== undefined && Object.keys(languages).includes(str);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +116,7 @@ export function getTranslatedPage<
|
||||
}
|
||||
|
||||
// fetch translation files from /data/i18n
|
||||
let i18nFiles: Record<string, unknown>;
|
||||
let i18nFiles: Record<string, { default: Record<string, string> }>;
|
||||
try {
|
||||
i18nFiles = import.meta.glob("../../content/data/i18n/*.json", {
|
||||
eager: true,
|
||||
@@ -127,12 +127,10 @@ try {
|
||||
|
||||
const i18n: Partial<Record<Languages, Map<string, string>>> =
|
||||
Object.fromEntries(
|
||||
Object.entries(i18nFiles).map(
|
||||
([file, content]: [string, { default: Record<string, string> }]) => [
|
||||
basename(file).split(".")[0],
|
||||
new Map(Object.entries(content.default)),
|
||||
],
|
||||
),
|
||||
Object.entries(i18nFiles).map(([file, content]) => [
|
||||
basename(file).split(".")[0],
|
||||
new Map(Object.entries(content.default)),
|
||||
]),
|
||||
);
|
||||
|
||||
// warn about any values that do not have full translations
|
||||
|
||||
@@ -8,7 +8,7 @@ import mastodon from "src/icons/mastodon.svg?raw";
|
||||
import facebook from "src/icons/facebook.svg?raw";
|
||||
import rss from "src/icons/rss.svg?raw";
|
||||
|
||||
const icons = { discord, linkedin, twitter, mastodon, facebook, rss };
|
||||
const icons: Record<string, string> = { discord, linkedin, twitter, mastodon, facebook, rss };
|
||||
|
||||
// Components used in the .MDX about files
|
||||
// - see /content/site/about-us*.mdx for usages
|
||||
|
||||
@@ -15,13 +15,13 @@ export const themeToggle = () => {
|
||||
const lightIconEls = document.querySelectorAll<HTMLElement>(
|
||||
"[data-theme-toggle-icon='light']",
|
||||
);
|
||||
function toggleButton(theme) {
|
||||
function toggleButton(theme: string) {
|
||||
themeToggleBtns.forEach((el) => (el.ariaPressed = `${theme === "dark"}`));
|
||||
lightIconEls.forEach((el) => {
|
||||
el.style.display = theme === "light" ? null : "none";
|
||||
el.style.display = theme === "light" ? "" : "none";
|
||||
});
|
||||
darkIconEls.forEach((el) => {
|
||||
el.style.display = theme === "light" ? "none" : null;
|
||||
el.style.display = theme === "light" ? "none" : "";
|
||||
});
|
||||
|
||||
// update the meta theme-color attribute(s) based on the user preference
|
||||
|
||||
@@ -4,7 +4,7 @@ export function getShortTitle(
|
||||
post: PostInfo,
|
||||
collection?: CollectionInfo,
|
||||
): string {
|
||||
const collectionTitle = collection?.title || post.collection;
|
||||
const collectionTitle = collection?.title || post.collection || "";
|
||||
// if the post title starts with its collection title, remove it
|
||||
if (post.title.startsWith(`${collectionTitle}: `))
|
||||
return post.title.substring(collectionTitle.length + 2);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck TODO
|
||||
|
||||
import { JSX } from "preact";
|
||||
import {
|
||||
useCallback,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck TODO
|
||||
|
||||
function throttle(callback, limit) {
|
||||
let waiting = false;
|
||||
return function (...props) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck TODO
|
||||
|
||||
// https://github.com/wuct/raf-throttle/blob/master/rafThrottle.js
|
||||
const rafThrottle = (callback) => {
|
||||
let requestId = null;
|
||||
|
||||
@@ -90,7 +90,7 @@ const FilterDialogMobile = ({
|
||||
count={author.numPosts}
|
||||
icon={
|
||||
<UUPicture
|
||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)}
|
||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
|
||||
alt={""}
|
||||
class={styles.authorIcon}
|
||||
/>
|
||||
@@ -198,7 +198,7 @@ const FilterDialogSmallTablet = ({
|
||||
<UUPicture
|
||||
picture={unicornProfilePicMap.find(
|
||||
(u) => u.id === author.id,
|
||||
)}
|
||||
)!}
|
||||
alt={""}
|
||||
class={styles.authorIcon}
|
||||
/>
|
||||
@@ -261,7 +261,7 @@ export const FilterDialog = ({
|
||||
};
|
||||
|
||||
const onFormConfirm = useCallback(
|
||||
(returnValue: string) => {
|
||||
(returnValue?: string) => {
|
||||
// if the "confirm" button is pressed, the dialog should
|
||||
// close with the selected values
|
||||
if (returnValue === "confirm") {
|
||||
|
||||
@@ -8,9 +8,11 @@ import { useWindowSize } from "../../../hooks/use-window-size";
|
||||
import { tabletLarge } from "../../../tokens/breakpoints";
|
||||
import { FilterDialog } from "./filter-dialog";
|
||||
import { FilterSidebar } from "./filter-sidebar";
|
||||
import tagMap from "../../../../content/data/tags.json";
|
||||
import tagsObj from "../../../../content/data/tags.json";
|
||||
import { SortType } from "./types";
|
||||
|
||||
const tagsMap = new Map(Object.entries(tagsObj));
|
||||
|
||||
interface FilterDisplayProps {
|
||||
unicornProfilePicMap: ProfilePictureMap;
|
||||
posts: PostInfo[];
|
||||
@@ -73,9 +75,7 @@ export const FilterDisplay = ({
|
||||
.map((tag) => ({
|
||||
tag,
|
||||
numPosts: tagToPostNumMap.get(tag) || 0,
|
||||
emoji: tagMap[tag]?.emoji,
|
||||
image: tagMap[tag]?.image,
|
||||
displayName: tagMap[tag]?.displayName,
|
||||
...tagsMap.get(tag),
|
||||
}));
|
||||
}, [posts]);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export const FilterSection = ({
|
||||
// When cleared, the focus needs to be passed to the heading button
|
||||
// to avoid resetting to <body> when the clear button is removed from the DOM.
|
||||
// https://github.com/unicorn-utterances/unicorn-utterances/issues/742
|
||||
const buttonRef = useRef<HTMLButtonElement>();
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const handleClear = () => {
|
||||
onClear();
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export const FilterSidebar = ({
|
||||
count={author.numPosts}
|
||||
icon={
|
||||
<UUPicture
|
||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)}
|
||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
|
||||
alt={""}
|
||||
class={styles.authorIcon}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { JSXNode } from "components/types";
|
||||
import styles from "./search-hero.module.scss";
|
||||
import tags from "../../../../content/data/tags.json";
|
||||
import { MutableRef } from "preact/hooks";
|
||||
import { Ref } from "preact";
|
||||
|
||||
const stickers = Object.values(tags)
|
||||
.filter((tag) => !!tag["shownWithBranding"] && !!tag["image"])
|
||||
.filter((tag) => "shownWithBranding" in tag && "image" in tag && tag.shownWithBranding)
|
||||
.sort(() => 0.5 - Math.random()) as { image: string }[];
|
||||
|
||||
const stickerTransforms = [
|
||||
|
||||
@@ -9,7 +9,7 @@ interface SearchResultCountProps {
|
||||
}
|
||||
|
||||
export const SearchResultCount = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLDivElement | null,
|
||||
SearchResultCountProps
|
||||
>(({ numberOfPosts, numberOfCollections }, ref) => {
|
||||
const language = useMemo(() => {
|
||||
|
||||
@@ -77,7 +77,7 @@ export const SearchTopbar = ({
|
||||
type="submit"
|
||||
aria-label="Search"
|
||||
dangerouslySetInnerHTML={{ __html: forward }}
|
||||
children={null}
|
||||
children={[]}
|
||||
/>
|
||||
</form>
|
||||
<div className={style.bigScreenContainer} />
|
||||
|
||||
@@ -41,7 +41,7 @@ afterAll(() => server.close());
|
||||
function mockFetch(fn: (searchStr: string) => ServerReturnType) {
|
||||
server.use(
|
||||
rest.get<ServerReturnType>(`/api/search`, async (req, res, ctx) => {
|
||||
const searchString = req.url.searchParams.get("query");
|
||||
const searchString = req.url.searchParams.get("query")!;
|
||||
return res(ctx.json(fn(searchString)));
|
||||
}),
|
||||
);
|
||||
@@ -53,7 +53,9 @@ function mockFetchWithStatus(
|
||||
) {
|
||||
server.use(
|
||||
rest.get<never>(`/api/search`, async (req, res, ctx) => {
|
||||
const searchString = req.url.searchParams.get("query");
|
||||
const searchString = req.url.searchParams.get("query")!;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore it's fine
|
||||
return res(ctx.status(status), ctx.json(fn(searchString)));
|
||||
}),
|
||||
);
|
||||
@@ -338,7 +340,7 @@ describe("Search page", () => {
|
||||
const select =
|
||||
container instanceof HTMLSelectElement
|
||||
? container
|
||||
: container.querySelector("select");
|
||||
: container.querySelector("select")!;
|
||||
|
||||
await user.selectOptions(select, "newest");
|
||||
|
||||
@@ -398,7 +400,7 @@ describe("Search page", () => {
|
||||
const select =
|
||||
container instanceof HTMLSelectElement
|
||||
? container
|
||||
: container.querySelector("select");
|
||||
: container.querySelector("select")!;
|
||||
|
||||
user.selectOptions(select, "newest");
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
||||
500,
|
||||
);
|
||||
|
||||
const resultsHeading = useRef<HTMLDivElement>();
|
||||
const resultsHeading = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const onManualSubmit = useCallback(
|
||||
(str: string) => {
|
||||
@@ -139,7 +139,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
||||
totalPosts: 0,
|
||||
collections: [],
|
||||
totalCollections: 0,
|
||||
},
|
||||
} as ServerReturnType,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
enabled,
|
||||
@@ -194,7 +194,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
||||
// Setup content to display
|
||||
const contentToDisplay = useMemo(() => {
|
||||
const urlVal = urlParams.get(CONTENT_TO_DISPLAY_KEY);
|
||||
const isValid = ["all", "articles", "collections"].includes(urlVal);
|
||||
const isValid = ["all", "articles", "collections"].includes(String(urlVal));
|
||||
if (isValid) return urlVal as "all" | "articles" | "collections";
|
||||
return DEFAULT_CONTENT_TO_DISPLAY;
|
||||
}, [urlParams]);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
// Enable stricter transpilation for better output.
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"jsxImportSource": "preact",
|
||||
|
||||
Reference in New Issue
Block a user