Merge branch 'partial-uwu' into uwu-search-page

# Conflicts:
#	astro.config.ts
#	package-lock.json
#	package.json
#	src/types/plausible.d.ts
#	src/utils/debounce.ts
This commit is contained in:
Corbin Crutchley
2023-07-26 17:44:00 -07:00
145 changed files with 6739 additions and 4249 deletions

View File

@@ -40,7 +40,7 @@ async function generateEpubHTML(slug: string, content: string) {
createRehypePlugins({
format: "epub",
path: resolve(process.cwd(), `content/blog/${slug}/`),
})
}),
)
// Voids: [] is required for epub generation, and causes little/no harm for non-epub usage
.use(rehypeStringify, { allowDangerousHtml: true, voids: [] });
@@ -55,7 +55,7 @@ type EpubOptions = ConstructorParameters<typeof EPub>[0];
async function generateCollectionEPub(
collection: RawCollectionInfo & Pick<CollectionInfo, "coverImgMeta">,
collectionPosts: ExtendedPostInfo[],
fileLocation: string
fileLocation: string,
) {
const authors = collection.authors.map((id) => {
return unicorns.find((u) => u.id === id).name;
@@ -115,10 +115,10 @@ async function generateCollectionEPub(
collectionPosts.map(async (post) => ({
title: post.title,
data: await generateEpubHTML(post.slug, post.contentMeta),
}))
})),
),
} as Partial<EpubOptions> as EpubOptions,
fileLocation
fileLocation,
);
await epub.render();
@@ -128,12 +128,12 @@ const posts = [...getAllExtendedPosts("en")];
for (const collection of collections) {
const collectionPosts = posts.filter(
(post) => post.collection === collection.slug
(post) => post.collection === collection.slug,
);
generateCollectionEPub(
collection,
collectionPosts,
resolve(process.cwd(), `public/${collection.slug}.epub`)
resolve(process.cwd(), `public/${collection.slug}.epub`),
);
}

View File

@@ -38,7 +38,7 @@ const createPostIndex = () => {
{ name: "description", weight: 1.2 },
{ name: "excerpt", weight: 1.2 },
],
posts
posts,
).toJSON();
};
@@ -74,7 +74,7 @@ const createCollectionIndex = () => {
weight: 1.2,
},
],
collections
collections,
).toJSON();
};

View File

@@ -71,7 +71,7 @@ const page = await context.newPage();
async function renderPostImage(
layout: Layout,
post: ExtendedPostInfo,
path: string
path: string,
) {
const label = `${post.slug} (${layout.name})`;
console.time(label);
@@ -97,7 +97,7 @@ for (const post of getAllExtendedPosts("en")) {
await renderPostImage(
twitterPreview,
post,
resolve(outDir, `.${post.socialImg}`)
resolve(outDir, `.${post.socialImg}`),
);
}
}

View File

@@ -1,18 +1,20 @@
import * as React from 'preact';
import { readFileAsBase64 } from '../utils';
import { ComponentProps, Layout } from '../base';
import style from './banner-css';
import classnames from 'classnames';
import * as React from "preact";
import { readFileAsBase64 } from "../utils";
import { ComponentProps, Layout } from "../base";
import style from "./banner-css";
import classnames from "classnames";
const tagStickers: Record<string, string> = {
"default": readFileAsBase64("public/stickers/role_devops.png"),
default: readFileAsBase64("public/stickers/role_devops.png"),
"html,webdev": readFileAsBase64("public/stickers/html.png"),
"vue": readFileAsBase64("public/stickers/vue.png"),
vue: readFileAsBase64("public/stickers/vue.png"),
"documentation,opinion": readFileAsBase64("public/stickers/role_author.png"),
'computer science,bash,javascript': readFileAsBase64("public/stickers/role_developer.png"),
"design": readFileAsBase64("public/stickers/role_designer.png"),
"rust": readFileAsBase64("public/stickers/ferris.png"),
"git": readFileAsBase64("public/stickers/git.png"),
"computer science,bash,javascript": readFileAsBase64(
"public/stickers/role_developer.png",
),
design: readFileAsBase64("public/stickers/role_designer.png"),
rust: readFileAsBase64("public/stickers/ferris.png"),
git: readFileAsBase64("public/stickers/git.png"),
};
function BannerCodeScreen({
@@ -20,16 +22,18 @@ function BannerCodeScreen({
postHtml,
blur,
}: {
post: ComponentProps['post'],
postHtml: string,
blur?: boolean,
post: ComponentProps["post"];
postHtml: string;
blur?: boolean;
}) {
const rotX = (post.description.length % 20) - 10;
const rotY = ((post.title.length * 3) % 20);
const rotY = (post.title.length * 3) % 20;
let tagImg = tagStickers["default"];
for (const tag of post.tags) {
const key = Object.keys(tagStickers).find(k => k.split(",").includes(tag));
const key = Object.keys(tagStickers).find((k) =>
k.split(",").includes(tag),
);
if (key) {
tagImg = tagStickers[key];
break;
@@ -38,38 +42,45 @@ function BannerCodeScreen({
const theme = post.title.length % 3;
return <>
<div class={classnames("absoluteFill", "codeScreenBg", blur && "blur", "theme-" + theme)} style={`--rotX: ${rotX}deg; --rotY: ${rotY}deg; --left: ${rotY}%;`}>
<div class="codeScreen">
<pre dangerouslySetInnerHTML={{ __html: postHtml }} />
<div class="tags">
{
post.tags.map((tag) => (
return (
<>
<div
class={classnames(
"absoluteFill",
"codeScreenBg",
blur && "blur",
"theme-" + theme,
)}
style={`--rotX: ${rotX}deg; --rotY: ${rotY}deg; --left: ${rotY}%;`}
>
<div class="codeScreen">
<pre dangerouslySetInnerHTML={{ __html: postHtml }} />
<div class="tags">
{post.tags.map((tag) => (
<span key={tag}>{tag}</span>
))
}
))}
</div>
</div>
<div class="rect" style="--z: 60px; --x: -80px; --y: -150px;">
<img src={tagImg} />
</div>
</div>
<div class="rect" style="--z: 60px; --x: -80px; --y: -150px;">
<img src={tagImg} />
</div>
</div>
</>;
</>
);
}
function Banner({
post,
postHtml,
}: ComponentProps) {
return <>
<BannerCodeScreen post={post} postHtml={postHtml} />
<div
className="absoluteFill codeScreenOverlay"
style={{
zIndex: -1,
}}
/>
</>;
function Banner({ post, postHtml }: ComponentProps) {
return (
<>
<BannerCodeScreen post={post} postHtml={postHtml} />
<div
className="absoluteFill codeScreenOverlay"
style={{
zIndex: -1,
}}
/>
</>
);
}
export default {

View File

@@ -1,54 +1,54 @@
import * as React from 'preact';
import * as React from "preact";
import { render } from "@testing-library/preact";
import { MockPost } from "__mocks__/data/mock-post";
import TwitterLargeCard, { splitSentence } from "./twitter-preview";
test("Social previews splitSentence", () => {
// doesn't split at start/end of short titles
expect(splitSentence("Topic: Topic")).toStrictEqual(["Topic: Topic", ""]);
// doesn't split at start/end of short titles
expect(splitSentence("Topic: Topic")).toStrictEqual(["Topic: Topic", ""]);
// splits by colon (including the colon char)
expect(splitSentence("A Topic: an Attribute")).toStrictEqual([
"A Topic",
": an Attribute",
]);
// splits by colon (including the colon char)
expect(splitSentence("A Topic: an Attribute")).toStrictEqual([
"A Topic",
": an Attribute",
]);
// splits by commas
expect(
splitSentence("An Attribute of Topic, Topic, and Topic")
).toStrictEqual(["An Attribute of ", "Topic, Topic, and Topic"]);
// splits by commas
expect(
splitSentence("An Attribute of Topic, Topic, and Topic"),
).toStrictEqual(["An Attribute of ", "Topic, Topic, and Topic"]);
// splits by apostrophe
expect(splitSentence("A Topic's Attribute")).toStrictEqual([
"A Topic's",
" Attribute",
]);
// splits by apostrophe
expect(splitSentence("A Topic's Attribute")).toStrictEqual([
"A Topic's",
" Attribute",
]);
// splits by apostrophe (plural)
expect(splitSentence("Some Topics' Attributes")).toStrictEqual([
"Some Topics'",
" Attributes",
]);
// splits by apostrophe (plural)
expect(splitSentence("Some Topics' Attributes")).toStrictEqual([
"Some Topics'",
" Attributes",
]);
// splits by lowercase words
expect(splitSentence("An Attribute in a Topic")).toStrictEqual([
"An Attribute in ",
"a Topic",
]);
// splits by lowercase words
expect(splitSentence("An Attribute in a Topic")).toStrictEqual([
"An Attribute in ",
"a Topic",
]);
});
test("Social preview renders", async () => {
const post = MockPost;
const { baseElement, findByText } = render(
<TwitterLargeCard.Component
post={post}
postHtml="<code>test();</code>"
width={1280}
height={640}
authorImageMap={{ [post.authors[0]]: "test.jpg" }}
/>
);
const post = MockPost;
const { baseElement, findByText } = render(
<TwitterLargeCard.Component
post={post}
postHtml="<code>test();</code>"
width={1280}
height={640}
authorImageMap={{ [post.authors[0]]: "test.jpg" }}
/>,
);
expect(baseElement).toBeInTheDocument();
expect(await findByText(post.title)).toBeInTheDocument();
expect(baseElement).toBeInTheDocument();
expect(await findByText(post.title)).toBeInTheDocument();
});

View File

@@ -1,130 +1,134 @@
import * as React from 'preact';
import { readFileAsBase64 } from '../utils';
import { ComponentProps, Layout } from '../base';
import style from './twitter-preview-css';
import * as React from "preact";
import { readFileAsBase64 } from "../utils";
import { ComponentProps, Layout } from "../base";
import style from "./twitter-preview-css";
export function splitSentence(str: string): [string, string] {
const splitStr = str.split(" ");
const splitBy = (
regex: RegExp,
matchLast: boolean = true
): [string, string] | null => {
const matches = splitStr.map((word, i) => ({ reg: regex.exec(word), i }));
const match = (matchLast ? matches.reverse() : matches)
.slice(1, -1)
.find(({ reg }) => !!reg);
const splitStr = str.split(" ");
const splitBy = (
regex: RegExp,
matchLast: boolean = true,
): [string, string] | null => {
const matches = splitStr.map((word, i) => ({ reg: regex.exec(word), i }));
const match = (matchLast ? matches.reverse() : matches)
.slice(1, -1)
.find(({ reg }) => !!reg);
// if match is not found, fail
if (!match || !match.reg) return null;
// if match is not found, fail
if (!match || !match.reg) return null;
const firstHalf = [
...splitStr.slice(0, match.i),
match.reg.input.substring(0, match.reg.index),
].join(" ");
const secondHalf = [match.reg[0], ...splitStr.slice(match.i + 1)].join(" ");
return [firstHalf, secondHalf];
};
const firstHalf = [
...splitStr.slice(0, match.i),
match.reg.input.substring(0, match.reg.index),
].join(" ");
const secondHalf = [match.reg[0], ...splitStr.slice(match.i + 1)].join(" ");
return [firstHalf, secondHalf];
};
let ret;
// try to split by "Topic[: Attribute]" or "Topic [- Attribute]" (hyphens/colons)
if ((ret = splitBy(/(?<=^\w+):$|^[-—]$/))) return ret;
// try to split by "Attribute in [Topic, Topic, and Topic]" (commas)
if ((ret = splitBy(/^\w+,$/, false))) return ret;
// try to split by "Topic['s Attribute]" (apostrophe)
if ((ret = splitBy(/(?<=^\w+\'s?)$/))) return ret;
// try to split by "Attribute [in Topic]" (lowercase words)
if ((ret = splitBy(/^[a-z][A-Za-z]*$/))) return ret;
// otherwise, don't split the string
return [str, ""];
let ret;
// try to split by "Topic[: Attribute]" or "Topic [- Attribute]" (hyphens/colons)
if ((ret = splitBy(/(?<=^\w+):$|^[-—]$/))) return ret;
// try to split by "Attribute in [Topic, Topic, and Topic]" (commas)
if ((ret = splitBy(/^\w+,$/, false))) return ret;
// try to split by "Topic['s Attribute]" (apostrophe)
if ((ret = splitBy(/(?<=^\w+\'s?)$/))) return ret;
// try to split by "Attribute [in Topic]" (lowercase words)
if ((ret = splitBy(/^[a-z][A-Za-z]*$/))) return ret;
// otherwise, don't split the string
return [str, ""];
}
const unicornUtterancesHead = readFileAsBase64("src/assets/unicorn_head_1024.png");
const unicornUtterancesHead = readFileAsBase64(
"src/assets/unicorn_head_1024.png",
);
interface TwitterCodeScreenProps {
title: string;
html: string;
blur: boolean;
title: string;
html: string;
blur: boolean;
}
const TwitterCodeScreen = ({ title, html, blur }: TwitterCodeScreenProps) => {
const rotations = [
"rotateX(-17deg) rotateY(32deg) rotateZ(-3deg) translate(16%, 0%)",
"rotateX(5deg) rotateY(35deg) rotateZ(345deg) translate(18%, 0)",
"rotateX(15deg) rotateY(25deg) rotateZ(12deg) translate(3%, -15%)",
];
const rotations = [
"rotateX(-17deg) rotateY(32deg) rotateZ(-3deg) translate(16%, 0%)",
"rotateX(5deg) rotateY(35deg) rotateZ(345deg) translate(18%, 0)",
"rotateX(15deg) rotateY(25deg) rotateZ(12deg) translate(3%, -15%)",
];
// use second char of title as "deterministic" random value
const transform = rotations[title.charCodeAt(1) % rotations.length];
// use second char of title as "deterministic" random value
const transform = rotations[title.charCodeAt(1) % rotations.length];
return (
<div className={`absoluteFill codeScreenBg ${blur ? "blur" : ""}`}>
<div
className="absoluteFill codeScreen"
style={`transform: ${transform};`}
>
<div className="absoluteFill">
<pre dangerouslySetInnerHTML={{ __html: html }} />
</div>
</div>
</div>
);
return (
<div className={`absoluteFill codeScreenBg ${blur ? "blur" : ""}`}>
<div
className="absoluteFill codeScreen"
style={`transform: ${transform};`}
>
<div className="absoluteFill">
<pre dangerouslySetInnerHTML={{ __html: html }} />
</div>
</div>
</div>
);
};
const TwitterLargeCard = ({
post,
postHtml,
width,
authorImageMap,
post,
postHtml,
width,
authorImageMap,
}: ComponentProps) => {
const title = post.title;
const [firstHalfTitle, secondHalfTitle] = splitSentence(title);
const title = post.title;
const [firstHalfTitle, secondHalfTitle] = splitSentence(title);
return <>
<TwitterCodeScreen title={post.title} html={postHtml} blur={true} />
<TwitterCodeScreen title={post.title} html={postHtml} blur={false} />
<div className="absoluteFill codeScreenOverlay" />
<div className="absoluteFill centerAll">
<h1
style={{
maxWidth: "90%",
textAlign: "center",
fontSize: `clamp(300%, 4.5rem, ${
Math.round(width / title.length) * 3
}px)`,
}}
>
{firstHalfTitle}
<span className="secondHalfTitle">{secondHalfTitle}</span>
</h1>
</div>
<div
className="absoluteFill backgroundColor"
style={{
zIndex: -1,
}}
/>
<div className="bottomContainer">
<div className="bottomImagesContainer centerAll">
{post.authors.map((author) => (
<img
key={author}
src={authorImageMap[author]}
alt=""
className="bottomProfImg"
height={80}
width={80}
/>
))}
</div>
<div className="bottomImagesContainer centerAll">
<p>unicorn-utterances.com</p>
<img src={unicornUtterancesHead} alt="" height={80} width={80} />
</div>
</div>
</>;
return (
<>
<TwitterCodeScreen title={post.title} html={postHtml} blur={true} />
<TwitterCodeScreen title={post.title} html={postHtml} blur={false} />
<div className="absoluteFill codeScreenOverlay" />
<div className="absoluteFill centerAll">
<h1
style={{
maxWidth: "90%",
textAlign: "center",
fontSize: `clamp(300%, 4.5rem, ${
Math.round(width / title.length) * 3
}px)`,
}}
>
{firstHalfTitle}
<span className="secondHalfTitle">{secondHalfTitle}</span>
</h1>
</div>
<div
className="absoluteFill backgroundColor"
style={{
zIndex: -1,
}}
/>
<div className="bottomContainer">
<div className="bottomImagesContainer centerAll">
{post.authors.map((author) => (
<img
key={author}
src={authorImageMap[author]}
alt=""
className="bottomProfImg"
height={80}
width={80}
/>
))}
</div>
<div className="bottomImagesContainer centerAll">
<p>unicorn-utterances.com</p>
<img src={unicornUtterancesHead} alt="" height={80} width={80} />
</div>
</div>
</>
);
};
export default {
name: "twitter-preview",
css: style,
Component: TwitterLargeCard,
name: "twitter-preview",
css: style,
Component: TwitterLargeCard,
} as Layout;