Merge branch 'uwu' into uwu-search-page

# Conflicts:
#	package-lock.json
#	package.json
This commit is contained in:
Corbin Crutchley
2023-07-15 22:32:41 -07:00
19 changed files with 7450 additions and 4666 deletions

View File

@@ -1,4 +1,4 @@
import puppeteer from "puppeteer-core";
import { chromium } from "playwright";
import { promises as fsPromises } from "fs";
import { resolve } from "path";
import { getAllExtendedPosts } from "utils/get-all-posts";
@@ -57,33 +57,31 @@ const browser_args = [
"--window-size=1920,1080",
];
const browser = await puppeteer.launch({
const browser = await chromium.launch({
args: browser_args,
headless: true,
ignoreHTTPSErrors: true,
});
const [page] = await browser.pages();
// log any page errors
page.on("error", console.error);
await page.setViewport({
width: PAGE_WIDTH,
height: PAGE_HEIGHT,
const context = await browser.newContext({
viewport: {
width: PAGE_WIDTH,
height: PAGE_HEIGHT,
},
});
const page = await context.newPage();
async function renderPostImage(layout: Layout, post: ExtendedPostInfo) {
async function renderPostImage(
layout: Layout,
post: ExtendedPostInfo,
path: string
) {
const label = `${post.slug} (${layout.name})`;
console.time(label);
await page.setContent(await renderPostPreviewToString(layout, post), {
timeout: 0,
});
const buffer = (await page.screenshot({ type: "jpeg" })) as Buffer;
await page.screenshot({ type: "jpeg", path });
console.timeEnd(label);
return buffer;
}
// Relative to root
@@ -96,20 +94,19 @@ await fsPromises.mkdir(resolve(outDir, "./generated"), { recursive: true });
*/
for (const post of getAllExtendedPosts("en")) {
if (post.socialImg) {
await fsPromises.writeFile(
resolve(outDir, `.${post.socialImg}`),
await renderPostImage(twitterPreview, post)
await renderPostImage(
twitterPreview,
post,
resolve(outDir, `.${post.socialImg}`)
);
}
}
for (const post of getAllExtendedPosts("en")) {
if (post.bannerImg) {
await fsPromises.writeFile(
resolve(outDir, `.${post.bannerImg}`),
await renderPostImage(banner, post)
);
await renderPostImage(banner, post, resolve(outDir, `.${post.bannerImg}`));
}
}
await context.close();
await browser.close();

11526
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@
"format": "prettier -w . --cache --plugin-search-dir=.",
"lint": "eslint . --ext .js,.ts,.astro",
"search-index": "tsx build-scripts/search-index.ts",
"social-previews:build": "node node_modules/puppeteer-core/install.js && tsx --tsconfig tsconfig.script.json build-scripts/social-previews/index.ts",
"social-previews:build": "tsx --tsconfig tsconfig.script.json build-scripts/social-previews/index.ts",
"social-previews:dev:build": "tsx watch --tsconfig tsconfig.script.json build-scripts/social-previews/live-server.ts",
"social-previews:dev:server": "live-server build-scripts/social-previews/dist",
"social-previews:dev": "npm-run-all --parallel social-previews:dev:build social-previews:dev:server",
@@ -85,13 +85,13 @@
"junk": "^4.0.1",
"lint-staged": "^13.2.3",
"npm-run-all": "^4.1.5",
"playwright": "^1.36.1",
"postcss": "^8.4.25",
"postcss-csso": "^6.0.1",
"preact-render-to-string": "^6.1.0",
"prettier": "^2.8.8",
"prettier-plugin-astro": "^0.10.0",
"probe-image-size": "^7.2.3",
"puppeteer-core": "^10.4.0",
"rehype-raw": "^6.1.1",
"rehype-retext": "^3.0.2",
"rehype-slug-custom-id": "^1.1.0",

View File

@@ -4,6 +4,7 @@ import { ProfilePictureMap } from "utils/get-unicorn-profile-pic-map";
import { Chip } from "components/index";
import date from "src/icons/date.svg?raw";
import authors from "src/icons/authors.svg?raw";
import { getHrefContainerProps } from "utils/href-container-script";
interface PostCardProps {
post: PostInfo;
@@ -73,7 +74,7 @@ export const PostCardExpanded = ({
}: PostCardProps) => {
return (
<li
data-navigation-path={`/posts/${post.slug}`}
{...getHrefContainerProps(`/posts/${post.slug}`)}
className={`${className} ${style.postBase} ${style.extendedPostContainer}`}
>
<div className={style.extendedPostImageContainer}>
@@ -101,7 +102,7 @@ export const PostCard = ({
}: PostCardProps) => {
return (
<li
data-navigation-path={`/posts/${post.slug}`}
{...getHrefContainerProps(`/posts/${post.slug}`)}
className={`${className} ${style.postContainer} ${style.postBase} ${style.regularPostContainer}`}
>
<a href={`/posts/${post.slug}`} className={`${style.postHeaderBase}`}>

View File

@@ -14,4 +14,8 @@ const lang = getPrefixLanguageFromPath(Astro.url.pathname);
<Header />
<slot />
<slot name="post-body" />
<script>
import "src/utils/href-container-script";
</script>
</Barebones>

View File

@@ -0,0 +1,128 @@
/**
* This allows items like cards to bind a click event to navigate to a path.
*
* Handles:
* - Left clicks
* - Middle clicks
* - Ctrl + Left clicks
* - Shift + Left clicks
* - Meta + Left clicks
*
*
* Ignores:
* - Right clicks (used for context menus)
* - Alt clicks (used for downloading)
* - Default prevented events
* - Nested elements with data-navigation-path
* - Elements with [data-dont-bind-navigate-click]
* - <a> tags and <button>s
*/
function isNestedElement(e: MouseEvent) {
// if the targeted element is nested inside another clickable anchor/button
let target = e.target as HTMLElement;
while (target !== e.currentTarget) {
if (
target.tagName.toLowerCase() === "a" ||
target.tagName.toLowerCase() === "button"
)
return true;
// Explicitly don't bind
if (target.getAttribute("data-dont-bind-navigate-click") !== null)
return true;
target = target.parentElement;
}
return false;
}
globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
const isMiddleClick = e.button === 1;
if (!isMiddleClick) return;
if (e.defaultPrevented) return;
if (isNestedElement(e)) return;
e.preventDefault();
};
// 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;
// otherwise, pass mouseup events to auxclick
globalThis.handleHrefContainerAuxClick(e);
};
// Handle middle click button - should open a new tab (cannot be detected via "click" event)
// - prefer the "auxclick" event for this, since it ensures that mousedown/mouseup both occur within the same element
// otherwise, using "mouseup" would activate on mouseup even when dragging between elements, which should not trigger a click
globalThis.handleHrefContainerAuxClick = (e: MouseEvent) => {
const href = (e.currentTarget as HTMLElement).dataset.href;
// only handle middle click events
if (e.button !== 1) return;
if (e.defaultPrevented) return;
if (isNestedElement(e)) return;
e.preventDefault();
window.open(href, "_blank");
return false;
};
globalThis.handleHrefContainerClick = (e: MouseEvent) => {
const href = (e.currentTarget as HTMLElement).dataset.href;
if (e.defaultPrevented) return;
if (isNestedElement(e)) return;
// only handle left click events
if (e.button !== 0) return;
// Download
if (e.altKey) return;
e.preventDefault();
// Open in new tab
if (e.metaKey || e.ctrlKey || e.shiftKey) {
window.open(href, "_blank");
return false;
}
// If text is selected, don't activate on mouseup (but ctrl+click should still work)
const selection = window.getSelection();
if (
selection?.toString()?.length &&
selection.containsNode(e.target as Node, true)
)
return;
window.location.href = href;
};
export function getHrefContainerProps(href: string) {
// hack to detect whether the function is in an Astro or Preact environment,
// assuming that Preact is only used outside of a node environment
if (
typeof process !== "undefined" &&
typeof process?.versions !== "undefined" &&
process.versions?.node
) {
// if running in NodeJS (Astro), return string props
return {
onmousedown: "handleHrefContainerMouseDown(event)",
onmouseup: "handleHrefContainerMouseUp(event)",
onauxclick: "handleHrefContainerAuxClick(event)",
onclick: "handleHrefContainerClick(event)",
"data-href": href,
};
} else {
// otherwise, need to return client-side functions
return {
onMouseDown: globalThis.handleHrefContainerMouseDown,
onMouseUp: globalThis.handleHrefContainerMouseUp,
onAuxClick: globalThis.handleHrefContainerAuxClick,
onClick: globalThis.handleHrefContainerClick,
"data-href": href,
};
}
}

View File

@@ -38,16 +38,16 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
const promise = (async () => {
// <link rel="manifest" href="/manifest.json">
const manifestPath = find(
const manifestPath: Element = find(
srcHast,
(node) => node?.properties?.rel?.[0] === "manifest"
(node: unknown) => (node as Element)?.properties?.rel?.[0] === "manifest"
);
let iconLink: string;
if (manifestPath) {
// `/manifest.json`
const manifestRelativeURL = manifestPath.properties.href;
const manifestRelativeURL = manifestPath.properties.href.toString();
const fullManifestURL = new URL(manifestRelativeURL, src).href;
const manifest = await fetch(fullManifestURL)
@@ -63,12 +63,12 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
if (!iconLink) {
// fetch `favicon.ico`
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
const favicon = find(srcHast, (node) =>
node?.properties?.rel?.includes("icon")
const favicon: Element = find(srcHast, (node: unknown) =>
(node as Element)?.properties?.rel?.toString()?.includes("icon")
);
if (favicon) {
iconLink = new URL(favicon.properties.href, src).href;
iconLink = new URL(favicon.properties.href.toString(), src).href;
}
}
@@ -127,8 +127,10 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
if (!srcHast) return null;
// find <title> element in response HTML
const titleEl = find(srcHast, { tagName: "title" });
const title = titleEl ? titleEl.children[0].value : undefined;
const titleEl: Element = find(srcHast, { tagName: "title" });
const titleContentEl = titleEl && titleEl.children[0];
const title =
titleContentEl?.type === "text" ? titleContentEl.value : undefined;
// find the page favicon (cache by page origin)
const icon = await fetchPageIcon(url, srcHast);

View File

@@ -1,102 +0,0 @@
/**
* This allows items like cards to bind a click event to navigate to a path.
*
* Handles:
* - Left clicks
* - Middle clicks
* - Ctrl + Left clicks
* - Shift + Left clicks
* - Meta + Left clicks
*
*
* Ignores:
* - Right clicks (used for context menus)
* - Alt clicks (used for downloading)
* - Default prevented events
* - Nested elements with data-navigation-path
* - Elements with [data-dont-bind-navigate-click]
* - <a> tags and <button>s
*/
function isNestedElement(e: MouseEvent) {
// if the targeted element is nested inside another clickable anchor/button
let target = e.target as HTMLElement;
while (target !== e.currentTarget) {
if (
target.tagName.toLowerCase() === "a" ||
target.tagName.toLowerCase() === "button"
)
return true;
// Explicitly don't bind
if (target.getAttribute("data-dont-bind-navigate-click") !== null)
return true;
target = target.parentElement;
}
return false;
}
export const setupNavigationPaths = () => {
document
.querySelectorAll(`[data-navigation-path]`)
.forEach((el: HTMLElement) => {
const path = el.dataset.navigationPath;
// Prevent middle click from starting a scroll
el.addEventListener("mousedown", (e: MouseEvent) => {
const isMiddleClick = e.button === 1;
if (!isMiddleClick) return;
if (e.defaultPrevented) return;
if (isNestedElement(e)) return;
e.preventDefault();
});
// Handle middle click button - should open a new tab (cannot be detected via "click" event)
// - prefer the "auxclick" event since it ensures that mousedown/mouseup both occur within the same element
// otherwise, using "mouseup" would activate on mouseup even when dragging between elements, which should not trigger a click
el.addEventListener(
// if "auxclick" is unsupported, fall back to "mouseup" for browser support (safari)
"onauxclick" in el ? "auxclick" : "mouseup",
(e: MouseEvent) => {
// only handle middle click events
if (e.button !== 1) return;
if (e.defaultPrevented) return;
if (isNestedElement(e)) return;
e.preventDefault();
window.open(path, "_blank");
return false;
}
);
// Use "click" to ensure that mousedown/mouseup both occur within the same element
el.addEventListener("click", (e: MouseEvent) => {
if (e.defaultPrevented) return;
if (isNestedElement(e)) return;
// only handle left click events
if (e.button !== 0) return;
// Download
if (e.altKey) return;
e.preventDefault();
// Open in new tab
if (e.metaKey || e.ctrlKey || e.shiftKey) {
window.open(path, "_blank");
return false;
}
// If text is selected, don't activate on mouseup (but ctrl+click should still work)
const selection = window.getSelection();
if (
selection?.toString()?.length &&
selection.containsNode(e.target as Node, true)
)
return;
location.href = path;
});
});
};

View File

@@ -0,0 +1,127 @@
@import "src/tokens/index";
:root {
--article-nav_padding-vertical: var(--site-spacing);
--article-nav_gap: var(--site-spacing);
--article-nav_item_padding-horizontal: var(--spc-4x);
--article-nav_item_padding-vertical: var(--spc-4x);
--article-nav_item_gap: var(--spc-2x);
--article-nav_item_arrow_size: var(--icon-size_dense);
--article-nav_item_arrow_margin: var(--spc-2x);
--article-nav_item_corner-radius: var(--corner-radius_m);
--article-nav_item_border-width: var(--border-width_l);
--article-nav_item_overline-color: var(--primary_default);
--article-nav_item_background-color: var(--transparent);
--article-nav_item_background-color_hovered: var(--surface_primary_emphasis-low);
--article-nav_item_background-color_pressed: var(--surface_primary_emphasis-low);
--article-nav_item_background-color_focused: var(--background_focus);
--article-nav_item_border_color: var(--surface_primary_emphasis-low);
--article-nav_item_border_color_hovered: var(--transparent);
--article-nav_item_border_color_pressed: var(--surface_primary_emphasis-high);
--article-nav_item_border_color_focused: var(--focus-outline_primary);
}
.container {
display: grid;
gap: var(--article-nav_gap);
padding: var(--article-nav_padding-vertical) 0;
flex-direction: column;
grid-template-rows: auto auto;
@include from($tabletSmall) {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
}
}
.item {
display: flex;
flex-direction: column;
gap: var(--article-nav_item_gap);
padding: var(--article-nav_item_padding-vertical) var(--article-nav_item_padding-horizontal);
background-color: var(--article-nav_item_background-color);
border: var(--article-nav_item_border-width) solid var(--article-nav_item_border_color);
border-radius: var(--article-nav_item_corner-radius);
@include transition(background-color border-color);
&__overline {
color: var(--article-nav_item_overline-color);
display: flex;
align-items: center;
gap: var(--article-nav_item_arrow_margin);
}
a {
color: var(--foreground_emphasis-high);
text-decoration: none;
outline: none !important;
}
&:hover {
background-color: var(--article-nav_item_background-color_hovered);
border-color: var(--article-nav_item_border_color_hovered);
a {
text-decoration: underline;
}
}
&:active {
background-color: var(--article-nav_item_background-color_pressed);
border-color: var(--article-nav_item_border_color_pressed);
}
@supports selector(:has(*)) {
&:has(:focus-visible) {
background-color: var(--article-nav_item_background-color_focused);
border-color: var(--article-nav_item_border_color_focused);
}
}
@supports not selector(:has(*)) {
&:focus-within {
background-color: var(--article-nav_item_background-color_focused);
border-color: var(--article-nav_item_border_color_focused);
}
}
&--previous {
&:not(:only-child) {
grid-row: 2;
}
@include from($tabletSmall) {
grid-row: 1;
grid-column: 1;
}
}
&--next {
grid-row: 1;
@include from($tabletSmall) {
grid-column: 2;
}
.item__overline {
justify-content: end;
}
text-align: right;
}
}
.icon {
display: inline-flex;
padding: var(--icon-size-dense-padding);
svg {
width: var(--article-nav_item_arrow_size);
height: var(--article-nav_item_arrow_size);
}
}

View File

@@ -0,0 +1,61 @@
import { PostInfo } from "types/PostInfo";
import style from "./article-nav.module.scss";
import arrow_left from "../../../icons/arrow_left.svg?raw";
import arrow_right from "../../../icons/arrow_right.svg?raw";
import { getShortTitle } from "../series/base";
type ArticleNavItemProps = {
post: PostInfo;
type: "next" | "previous";
};
function ArticleNavItem({ post, type }: ArticleNavItemProps) {
const href = `/posts/${post.slug}`;
return (
<div
class={`${style.item} ${style[`item--${type}`]}`}
data-navigation-path={href}
>
{
type === "previous"
? (
<span class={`${style.item__overline} text-style-button-regular`}>
<span
class={`${style.icon}`}
dangerouslySetInnerHTML={{ __html: arrow_left }}
/>
Previous article
</span>
)
: (
<span class={`${style.item__overline} text-style-button-regular`}>
Next article
<span
class={`${style.icon}`}
dangerouslySetInnerHTML={{ __html: arrow_right }}
/>
</span>
)
}
<a href={href} class="text-style-body-medium-bold">{getShortTitle(post)}</a>
</div>
)
}
export interface ArticleNavProps {
post: PostInfo;
postSeries: PostInfo[];
}
export function ArticleNav({ post, postSeries }: ArticleNavProps) {
const postIndex = postSeries.findIndex((p) => p.order === post.order);
const prevPost = postSeries[postIndex - 1];
const nextPost = postSeries[postIndex + 1];
return (
<div class={style.container}>
{prevPost && <ArticleNavItem post={prevPost} type="previous" />}
{nextPost && <ArticleNavItem post={nextPost} type="next" />}
</div>
)
}

View File

@@ -15,7 +15,7 @@ import "../../styles/shiki.scss";
import "../../styles/markdown/tabs.scss";
import "../../styles/convertkit.scss";
import SeriesToC from "./series/series-toc.astro";
import SeriesNav from "./series/series-nav.astro";
import { ArticleNav } from "./article-nav/article-nav";
import { getPrefixLanguageFromPath } from "utils/translations";
import RelatedPosts from "components/related-posts/related-posts.astro";
@@ -86,7 +86,7 @@ if (post.collection && post.order) {
<Content />
{
post.collection ? (
<SeriesNav post={post} postSeries={seriesPosts} />
<ArticleNav post={post} postSeries={seriesPosts} />
) : null
}
</main>

View File

@@ -1,6 +1,6 @@
import { ExtendedPostInfo } from "types/index";
import { ExtendedPostInfo, PostInfo } from "types/index";
export function getShortTitle(post: ExtendedPostInfo): string {
export function getShortTitle(post: PostInfo): string {
const collectionTitle = post.collectionMeta?.title || post.collection;
// if the post title starts with its collection title, remove it
if (post.title.startsWith(`${collectionTitle}: `))

View File

@@ -1,39 +0,0 @@
---
import styles from "./series-nav.module.scss";
import NavigateNext from "../../../icons/arrow_right.svg?raw";
import NavigateBefore from "../../../icons/arrow_left.svg?raw";
import { getShortTitle } from "./base";
import { ExtendedPostInfo } from "types/index";
interface SeriesNavProps {
post: ExtendedPostInfo;
postSeries: ExtendedPostInfo[];
}
const { post, postSeries } = Astro.props as SeriesNavProps;
const postIndex = postSeries.findIndex((p) => p.order === post.order);
const prevPost = postSeries[postIndex - 1];
const nextPost = postSeries[postIndex + 1];
---
<div class={styles.seriesNav}>
{
prevPost ? (
<a href={`/posts/${prevPost.slug}`} class={`baseBtn prependIcon`}>
<span style={{ display: "inline-flex" }} set:html={NavigateBefore} />
Previous Chapter: {getShortTitle(prevPost)}
</a>
) : null
}
{
nextPost ? (
<a href={`/posts/${nextPost.slug}`} class={`baseBtn appendIcon`}>
Next Chapter: {getShortTitle(nextPost)}
<span style={{ display: "inline-flex" }} set:html={NavigateNext} />
</a>
) : (
<div />
)
}
</div>

View File

@@ -1,22 +0,0 @@
.seriesNav {
margin: 2rem 0;
& > * {
display: inline-flex !important;
}
& > *:last-child {
float: right;
margin-left: 1rem;
}
&::after {
clear: both;
display: block;
content: " ";
}
& > a > img {
all: unset !important;
}
}

View File

@@ -1,5 +1,6 @@
---
import styles from "./collection-chapter.module.scss";
import { getHrefContainerProps } from "utils/href-container-script";
interface CollectionChapter {
num: number;
@@ -11,7 +12,7 @@ interface CollectionChapter {
const { num, title, description, href } = Astro.props as CollectionChapter;
---
<div class={styles.container} data-navigation-path={href}>
<div class={styles.container} {...getHrefContainerProps(href)}>
<div class={styles.numAndTitleContainer}>
<div class={styles.numContainer}>
<p class={`text-style-headline-6 ${styles.num}`} aria-hidden="true">

View File

@@ -5,6 +5,7 @@ import { Picture } from "@astrojs/image/components";
import { Picture as UUPicture } from "components/image/picture";
import { LargeButton, Button } from "components/button/button";
import { translate } from "src/utils/translations";
import { getHrefContainerProps } from "utils/href-container-script";
import "../../styles/post-body.scss";
import "../../styles/markdown/tabs.scss";
@@ -92,7 +93,10 @@ const coverImgAspectRatio = coverImgSize.width / coverImgSize.height;
);
const href = `/unicorns/${author.id}`;
return (
<li class={styles.authorContainer} data-navigation-path={href}>
<li
class={styles.authorContainer}
{...getHrefContainerProps(href)}
>
<div
class={styles.authorImage}
style={{ borderColor: author.color }}
@@ -148,8 +152,3 @@ const coverImgAspectRatio = coverImgSize.width / coverImgSize.height;
</div>
</div>
</div>
<script>
import { setupNavigationPaths } from "../../utils/navigation-path-script";
setupNavigationPaths();
</script>

View File

@@ -33,11 +33,6 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
/>
</section>
<script>
import { setupNavigationPaths } from "../../utils/navigation-path-script";
setupNavigationPaths();
</script>
<section>
<SubHeader tag="h1" text="Collections">
<Button href="/page/1">{translate(Astro, "action.view_all")}</Button>

View File

@@ -21,9 +21,4 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
unicornProfilePicMap={unicornProfilePicMap}
/>
<script>
import { setupNavigationPaths } from "../../utils/navigation-path-script";
setupNavigationPaths();
</script>
<Pagination page={page} client:load />

View File

@@ -25,8 +25,3 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
unicornProfilePicMap={unicornProfilePicMap}
/>
</UnicornLayout>
<script>
import { setupNavigationPaths } from "../../utils/navigation-path-script";
setupNavigationPaths();
</script>