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 { 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({
|
||||
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();
|
||||
|
||||
11518
package-lock.json
generated
11518
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=.",
|
||||
"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",
|
||||
|
||||
@@ -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}`}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
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 () => {
|
||||
// <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);
|
||||
|
||||
@@ -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/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>
|
||||
|
||||
@@ -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}: `))
|
||||
|
||||
@@ -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 { 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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -25,8 +25,3 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
|
||||
unicornProfilePicMap={unicornProfilePicMap}
|
||||
/>
|
||||
</UnicornLayout>
|
||||
|
||||
<script>
|
||||
import { setupNavigationPaths } from "../../utils/navigation-path-script";
|
||||
setupNavigationPaths();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user