mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-06 12:57:44 +00:00
Merge branch 'uwu' into uwu-search-page
# Conflicts: # package-lock.json # package.json
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import puppeteer from "puppeteer-core";
|
import { chromium } from "playwright";
|
||||||
import { promises as fsPromises } from "fs";
|
import { promises as fsPromises } from "fs";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import { getAllExtendedPosts } from "utils/get-all-posts";
|
import { getAllExtendedPosts } from "utils/get-all-posts";
|
||||||
@@ -57,33 +57,31 @@ const browser_args = [
|
|||||||
"--window-size=1920,1080",
|
"--window-size=1920,1080",
|
||||||
];
|
];
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await chromium.launch({
|
||||||
args: browser_args,
|
args: browser_args,
|
||||||
headless: true,
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
});
|
});
|
||||||
|
const context = await browser.newContext({
|
||||||
const [page] = await browser.pages();
|
viewport: {
|
||||||
|
|
||||||
// log any page errors
|
|
||||||
page.on("error", console.error);
|
|
||||||
|
|
||||||
await page.setViewport({
|
|
||||||
width: PAGE_WIDTH,
|
width: PAGE_WIDTH,
|
||||||
height: PAGE_HEIGHT,
|
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})`;
|
const label = `${post.slug} (${layout.name})`;
|
||||||
console.time(label);
|
console.time(label);
|
||||||
|
|
||||||
await page.setContent(await renderPostPreviewToString(layout, post), {
|
await page.setContent(await renderPostPreviewToString(layout, post), {
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
});
|
});
|
||||||
const buffer = (await page.screenshot({ type: "jpeg" })) as Buffer;
|
await page.screenshot({ type: "jpeg", path });
|
||||||
|
|
||||||
console.timeEnd(label);
|
console.timeEnd(label);
|
||||||
return buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relative to root
|
// Relative to root
|
||||||
@@ -96,20 +94,19 @@ await fsPromises.mkdir(resolve(outDir, "./generated"), { recursive: true });
|
|||||||
*/
|
*/
|
||||||
for (const post of getAllExtendedPosts("en")) {
|
for (const post of getAllExtendedPosts("en")) {
|
||||||
if (post.socialImg) {
|
if (post.socialImg) {
|
||||||
await fsPromises.writeFile(
|
await renderPostImage(
|
||||||
resolve(outDir, `.${post.socialImg}`),
|
twitterPreview,
|
||||||
await renderPostImage(twitterPreview, post)
|
post,
|
||||||
|
resolve(outDir, `.${post.socialImg}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const post of getAllExtendedPosts("en")) {
|
for (const post of getAllExtendedPosts("en")) {
|
||||||
if (post.bannerImg) {
|
if (post.bannerImg) {
|
||||||
await fsPromises.writeFile(
|
await renderPostImage(banner, post, resolve(outDir, `.${post.bannerImg}`));
|
||||||
resolve(outDir, `.${post.bannerImg}`),
|
|
||||||
await renderPostImage(banner, post)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await context.close();
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|||||||
11520
package-lock.json
generated
11520
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
|||||||
"format": "prettier -w . --cache --plugin-search-dir=.",
|
"format": "prettier -w . --cache --plugin-search-dir=.",
|
||||||
"lint": "eslint . --ext .js,.ts,.astro",
|
"lint": "eslint . --ext .js,.ts,.astro",
|
||||||
"search-index": "tsx build-scripts/search-index.ts",
|
"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: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:server": "live-server build-scripts/social-previews/dist",
|
||||||
"social-previews:dev": "npm-run-all --parallel social-previews:dev:build social-previews:dev:server",
|
"social-previews:dev": "npm-run-all --parallel social-previews:dev:build social-previews:dev:server",
|
||||||
@@ -85,13 +85,13 @@
|
|||||||
"junk": "^4.0.1",
|
"junk": "^4.0.1",
|
||||||
"lint-staged": "^13.2.3",
|
"lint-staged": "^13.2.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"playwright": "^1.36.1",
|
||||||
"postcss": "^8.4.25",
|
"postcss": "^8.4.25",
|
||||||
"postcss-csso": "^6.0.1",
|
"postcss-csso": "^6.0.1",
|
||||||
"preact-render-to-string": "^6.1.0",
|
"preact-render-to-string": "^6.1.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-astro": "^0.10.0",
|
"prettier-plugin-astro": "^0.10.0",
|
||||||
"probe-image-size": "^7.2.3",
|
"probe-image-size": "^7.2.3",
|
||||||
"puppeteer-core": "^10.4.0",
|
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"rehype-retext": "^3.0.2",
|
"rehype-retext": "^3.0.2",
|
||||||
"rehype-slug-custom-id": "^1.1.0",
|
"rehype-slug-custom-id": "^1.1.0",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ProfilePictureMap } from "utils/get-unicorn-profile-pic-map";
|
|||||||
import { Chip } from "components/index";
|
import { Chip } from "components/index";
|
||||||
import date from "src/icons/date.svg?raw";
|
import date from "src/icons/date.svg?raw";
|
||||||
import authors from "src/icons/authors.svg?raw";
|
import authors from "src/icons/authors.svg?raw";
|
||||||
|
import { getHrefContainerProps } from "utils/href-container-script";
|
||||||
|
|
||||||
interface PostCardProps {
|
interface PostCardProps {
|
||||||
post: PostInfo;
|
post: PostInfo;
|
||||||
@@ -73,7 +74,7 @@ export const PostCardExpanded = ({
|
|||||||
}: PostCardProps) => {
|
}: PostCardProps) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
data-navigation-path={`/posts/${post.slug}`}
|
{...getHrefContainerProps(`/posts/${post.slug}`)}
|
||||||
className={`${className} ${style.postBase} ${style.extendedPostContainer}`}
|
className={`${className} ${style.postBase} ${style.extendedPostContainer}`}
|
||||||
>
|
>
|
||||||
<div className={style.extendedPostImageContainer}>
|
<div className={style.extendedPostImageContainer}>
|
||||||
@@ -101,7 +102,7 @@ export const PostCard = ({
|
|||||||
}: PostCardProps) => {
|
}: PostCardProps) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
data-navigation-path={`/posts/${post.slug}`}
|
{...getHrefContainerProps(`/posts/${post.slug}`)}
|
||||||
className={`${className} ${style.postContainer} ${style.postBase} ${style.regularPostContainer}`}
|
className={`${className} ${style.postContainer} ${style.postBase} ${style.regularPostContainer}`}
|
||||||
>
|
>
|
||||||
<a href={`/posts/${post.slug}`} className={`${style.postHeaderBase}`}>
|
<a href={`/posts/${post.slug}`} className={`${style.postHeaderBase}`}>
|
||||||
|
|||||||
@@ -14,4 +14,8 @@ const lang = getPrefixLanguageFromPath(Astro.url.pathname);
|
|||||||
<Header />
|
<Header />
|
||||||
<slot />
|
<slot />
|
||||||
<slot name="post-body" />
|
<slot name="post-body" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import "src/utils/href-container-script";
|
||||||
|
</script>
|
||||||
</Barebones>
|
</Barebones>
|
||||||
|
|||||||
128
src/utils/href-container-script.ts
Normal file
128
src/utils/href-container-script.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,16 +38,16 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
|||||||
|
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
// <link rel="manifest" href="/manifest.json">
|
// <link rel="manifest" href="/manifest.json">
|
||||||
const manifestPath = find(
|
const manifestPath: Element = find(
|
||||||
srcHast,
|
srcHast,
|
||||||
(node) => node?.properties?.rel?.[0] === "manifest"
|
(node: unknown) => (node as Element)?.properties?.rel?.[0] === "manifest"
|
||||||
);
|
);
|
||||||
|
|
||||||
let iconLink: string;
|
let iconLink: string;
|
||||||
|
|
||||||
if (manifestPath) {
|
if (manifestPath) {
|
||||||
// `/manifest.json`
|
// `/manifest.json`
|
||||||
const manifestRelativeURL = manifestPath.properties.href;
|
const manifestRelativeURL = manifestPath.properties.href.toString();
|
||||||
const fullManifestURL = new URL(manifestRelativeURL, src).href;
|
const fullManifestURL = new URL(manifestRelativeURL, src).href;
|
||||||
|
|
||||||
const manifest = await fetch(fullManifestURL)
|
const manifest = await fetch(fullManifestURL)
|
||||||
@@ -63,12 +63,12 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
|||||||
if (!iconLink) {
|
if (!iconLink) {
|
||||||
// fetch `favicon.ico`
|
// fetch `favicon.ico`
|
||||||
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
|
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
|
||||||
const favicon = find(srcHast, (node) =>
|
const favicon: Element = find(srcHast, (node: unknown) =>
|
||||||
node?.properties?.rel?.includes("icon")
|
(node as Element)?.properties?.rel?.toString()?.includes("icon")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (favicon) {
|
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;
|
if (!srcHast) return null;
|
||||||
|
|
||||||
// find <title> element in response HTML
|
// find <title> element in response HTML
|
||||||
const titleEl = find(srcHast, { tagName: "title" });
|
const titleEl: Element = find(srcHast, { tagName: "title" });
|
||||||
const title = titleEl ? titleEl.children[0].value : undefined;
|
const titleContentEl = titleEl && titleEl.children[0];
|
||||||
|
const title =
|
||||||
|
titleContentEl?.type === "text" ? titleContentEl.value : undefined;
|
||||||
|
|
||||||
// find the page favicon (cache by page origin)
|
// find the page favicon (cache by page origin)
|
||||||
const icon = await fetchPageIcon(url, srcHast);
|
const icon = await fetchPageIcon(url, srcHast);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
127
src/views/blog-post/article-nav/article-nav.module.scss
Normal file
127
src/views/blog-post/article-nav/article-nav.module.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/views/blog-post/article-nav/article-nav.tsx
Normal file
61
src/views/blog-post/article-nav/article-nav.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import "../../styles/shiki.scss";
|
|||||||
import "../../styles/markdown/tabs.scss";
|
import "../../styles/markdown/tabs.scss";
|
||||||
import "../../styles/convertkit.scss";
|
import "../../styles/convertkit.scss";
|
||||||
import SeriesToC from "./series/series-toc.astro";
|
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 { getPrefixLanguageFromPath } from "utils/translations";
|
||||||
import RelatedPosts from "components/related-posts/related-posts.astro";
|
import RelatedPosts from "components/related-posts/related-posts.astro";
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ if (post.collection && post.order) {
|
|||||||
<Content />
|
<Content />
|
||||||
{
|
{
|
||||||
post.collection ? (
|
post.collection ? (
|
||||||
<SeriesNav post={post} postSeries={seriesPosts} />
|
<ArticleNav post={post} postSeries={seriesPosts} />
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -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;
|
const collectionTitle = post.collectionMeta?.title || post.collection;
|
||||||
// if the post title starts with its collection title, remove it
|
// if the post title starts with its collection title, remove it
|
||||||
if (post.title.startsWith(`${collectionTitle}: `))
|
if (post.title.startsWith(`${collectionTitle}: `))
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import styles from "./collection-chapter.module.scss";
|
import styles from "./collection-chapter.module.scss";
|
||||||
|
import { getHrefContainerProps } from "utils/href-container-script";
|
||||||
|
|
||||||
interface CollectionChapter {
|
interface CollectionChapter {
|
||||||
num: number;
|
num: number;
|
||||||
@@ -11,7 +12,7 @@ interface CollectionChapter {
|
|||||||
const { num, title, description, href } = Astro.props as 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.numAndTitleContainer}>
|
||||||
<div class={styles.numContainer}>
|
<div class={styles.numContainer}>
|
||||||
<p class={`text-style-headline-6 ${styles.num}`} aria-hidden="true">
|
<p class={`text-style-headline-6 ${styles.num}`} aria-hidden="true">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Picture } from "@astrojs/image/components";
|
|||||||
import { Picture as UUPicture } from "components/image/picture";
|
import { Picture as UUPicture } from "components/image/picture";
|
||||||
import { LargeButton, Button } from "components/button/button";
|
import { LargeButton, Button } from "components/button/button";
|
||||||
import { translate } from "src/utils/translations";
|
import { translate } from "src/utils/translations";
|
||||||
|
import { getHrefContainerProps } from "utils/href-container-script";
|
||||||
|
|
||||||
import "../../styles/post-body.scss";
|
import "../../styles/post-body.scss";
|
||||||
import "../../styles/markdown/tabs.scss";
|
import "../../styles/markdown/tabs.scss";
|
||||||
@@ -92,7 +93,10 @@ const coverImgAspectRatio = coverImgSize.width / coverImgSize.height;
|
|||||||
);
|
);
|
||||||
const href = `/unicorns/${author.id}`;
|
const href = `/unicorns/${author.id}`;
|
||||||
return (
|
return (
|
||||||
<li class={styles.authorContainer} data-navigation-path={href}>
|
<li
|
||||||
|
class={styles.authorContainer}
|
||||||
|
{...getHrefContainerProps(href)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class={styles.authorImage}
|
class={styles.authorImage}
|
||||||
style={{ borderColor: author.color }}
|
style={{ borderColor: author.color }}
|
||||||
@@ -148,8 +152,3 @@ const coverImgAspectRatio = coverImgSize.width / coverImgSize.height;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { setupNavigationPaths } from "../../utils/navigation-path-script";
|
|
||||||
setupNavigationPaths();
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -33,11 +33,6 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { setupNavigationPaths } from "../../utils/navigation-path-script";
|
|
||||||
setupNavigationPaths();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<SubHeader tag="h1" text="Collections">
|
<SubHeader tag="h1" text="Collections">
|
||||||
<Button href="/page/1">{translate(Astro, "action.view_all")}</Button>
|
<Button href="/page/1">{translate(Astro, "action.view_all")}</Button>
|
||||||
|
|||||||
@@ -21,9 +21,4 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
|
|||||||
unicornProfilePicMap={unicornProfilePicMap}
|
unicornProfilePicMap={unicornProfilePicMap}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { setupNavigationPaths } from "../../utils/navigation-path-script";
|
|
||||||
setupNavigationPaths();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Pagination page={page} client:load />
|
<Pagination page={page} client:load />
|
||||||
|
|||||||
@@ -25,8 +25,3 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
|
|||||||
unicornProfilePicMap={unicornProfilePicMap}
|
unicornProfilePicMap={unicornProfilePicMap}
|
||||||
/>
|
/>
|
||||||
</UnicornLayout>
|
</UnicornLayout>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { setupNavigationPaths } from "../../utils/navigation-path-script";
|
|
||||||
setupNavigationPaths();
|
|
||||||
</script>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user