Initial commit from Astro
This commit is contained in:
313
src/layouts/PostDetails.astro
Normal file
313
src/layouts/PostDetails.astro
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Tag from "@components/Tag.astro";
|
||||
import Datetime from "@components/Datetime";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { slugifyStr } from "@utils/slugify";
|
||||
import ShareLinks from "@components/ShareLinks.astro";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
posts: CollectionEntry<"blog">[];
|
||||
}
|
||||
|
||||
const { post, posts } = Astro.props;
|
||||
|
||||
const {
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
ogImage,
|
||||
canonicalURL,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
tags,
|
||||
editPost,
|
||||
} = post.data;
|
||||
|
||||
const { Content } = await post.render();
|
||||
|
||||
const ogImageUrl = typeof ogImage === "string" ? ogImage : ogImage?.src;
|
||||
const ogUrl = new URL(
|
||||
ogImageUrl ?? `/posts/${slugifyStr(title)}.png`,
|
||||
Astro.url.origin
|
||||
).href;
|
||||
|
||||
const layoutProps = {
|
||||
title: `${title} | ${SITE.title}`,
|
||||
author,
|
||||
description,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
canonicalURL,
|
||||
ogImage: ogUrl,
|
||||
scrollSmooth: true,
|
||||
};
|
||||
|
||||
/* ========== Prev/Next Posts ========== */
|
||||
|
||||
const allPosts = posts.map(({ data: { title }, slug }) => ({
|
||||
slug,
|
||||
title,
|
||||
}));
|
||||
|
||||
const currentPostIndex = allPosts.findIndex(a => a.slug === post.slug);
|
||||
|
||||
const prevPost = currentPostIndex !== 0 ? allPosts[currentPostIndex - 1] : null;
|
||||
const nextPost =
|
||||
currentPostIndex !== allPosts.length ? allPosts[currentPostIndex + 1] : null;
|
||||
---
|
||||
|
||||
<Layout {...layoutProps}>
|
||||
<Header />
|
||||
|
||||
<div class="mx-auto flex w-full max-w-3xl justify-start px-2">
|
||||
<button
|
||||
class="focus-outline mb-2 mt-8 flex hover:opacity-75"
|
||||
onclick="(() => (history.length === 1) ? window.location = '/' : history.back())()"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
|
||||
></path>
|
||||
</svg><span>Go back</span>
|
||||
</button>
|
||||
</div>
|
||||
<main id="main-content">
|
||||
<h1 transition:name={slugifyStr(title)} class="post-title inline-block">{title}</h1>
|
||||
<Datetime
|
||||
pubDatetime={pubDatetime}
|
||||
modDatetime={modDatetime}
|
||||
size="lg"
|
||||
className="my-2"
|
||||
editPost={editPost}
|
||||
postId={post.id}
|
||||
/>
|
||||
<article id="article" class="prose mx-auto mt-8 max-w-3xl">
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<ul class="my-8">
|
||||
{tags.map(tag => <Tag tag={slugifyStr(tag)} />)}
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="flex flex-col-reverse items-center justify-between gap-6 sm:flex-row-reverse sm:items-end sm:gap-4"
|
||||
>
|
||||
<button
|
||||
id="back-to-top"
|
||||
class="focus-outline whitespace-nowrap py-1 hover:opacity-75"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="rotate-90">
|
||||
<path
|
||||
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Back to Top</span>
|
||||
</button>
|
||||
|
||||
<ShareLinks />
|
||||
</div>
|
||||
|
||||
<hr class="my-6 border-dashed" />
|
||||
|
||||
<!-- Previous/Next Post Buttons -->
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{
|
||||
prevPost && (
|
||||
<a
|
||||
href={`/posts/${prevPost.slug}`}
|
||||
class="flex w-full gap-1 hover:opacity-75"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left flex-none"
|
||||
>
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M15 6l-6 6l6 6" />
|
||||
</>
|
||||
</svg>
|
||||
<div>
|
||||
<span>Previous Post</span>
|
||||
<div class="text-sm text-skin-accent/85">{prevPost.title}</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
nextPost && (
|
||||
<a
|
||||
href={`/posts/${nextPost.slug}`}
|
||||
class="flex w-full justify-end gap-1 text-right hover:opacity-75 sm:col-start-2"
|
||||
>
|
||||
<div>
|
||||
<span>Next Post</span>
|
||||
<div class="text-sm text-skin-accent/85">{nextPost.title}</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right flex-none"
|
||||
>
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 6l6 6l-6 6" />
|
||||
</>
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
main {
|
||||
@apply mx-auto w-full max-w-3xl px-4 pb-12;
|
||||
}
|
||||
.post-title {
|
||||
@apply text-2xl font-semibold text-skin-accent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline data-astro-rerun>
|
||||
/** Create a progress indicator
|
||||
* at the top */
|
||||
function createProgressBar() {
|
||||
// Create the main container div
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.className =
|
||||
"progress-container fixed top-0 z-10 h-1 w-full bg-skin-fill";
|
||||
|
||||
// Create the progress bar div
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.className = "progress-bar h-1 w-0 bg-skin-accent";
|
||||
progressBar.id = "myBar";
|
||||
|
||||
// Append the progress bar to the progress container
|
||||
progressContainer.appendChild(progressBar);
|
||||
|
||||
// Append the progress container to the document body or any other desired parent element
|
||||
document.body.appendChild(progressContainer);
|
||||
}
|
||||
createProgressBar();
|
||||
|
||||
/** Update the progress bar
|
||||
* when user scrolls */
|
||||
function updateScrollProgress() {
|
||||
document.addEventListener("scroll", () => {
|
||||
const winScroll =
|
||||
document.body.scrollTop || document.documentElement.scrollTop;
|
||||
const height =
|
||||
document.documentElement.scrollHeight -
|
||||
document.documentElement.clientHeight;
|
||||
const scrolled = (winScroll / height) * 100;
|
||||
if (document) {
|
||||
const myBar = document.getElementById("myBar");
|
||||
if (myBar) {
|
||||
myBar.style.width = scrolled + "%";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
updateScrollProgress();
|
||||
|
||||
/** Attaches links to headings in the document,
|
||||
* allowing sharing of sections easily */
|
||||
function addHeadingLinks() {
|
||||
const headings = Array.from(
|
||||
document.querySelectorAll("h2, h3, h4, h5, h6")
|
||||
);
|
||||
for (const heading of headings) {
|
||||
heading.classList.add("group");
|
||||
const link = document.createElement("a");
|
||||
link.className =
|
||||
"heading-link ml-2 opacity-0 group-hover:opacity-100 focus:opacity-100";
|
||||
link.href = "#" + heading.id;
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.ariaHidden = "true";
|
||||
span.innerText = "#";
|
||||
link.appendChild(span);
|
||||
heading.appendChild(link);
|
||||
}
|
||||
}
|
||||
addHeadingLinks();
|
||||
|
||||
/** Attaches copy buttons to code blocks in the document,
|
||||
* allowing users to copy code easily. */
|
||||
function attachCopyButtons() {
|
||||
const copyButtonLabel = "Copy";
|
||||
const codeBlocks = Array.from(document.querySelectorAll("pre"));
|
||||
|
||||
for (const codeBlock of codeBlocks) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.style.position = "relative";
|
||||
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.className =
|
||||
"copy-code absolute right-3 -top-3 rounded bg-skin-card px-2 py-1 text-xs leading-4 text-skin-base font-medium";
|
||||
copyButton.innerHTML = copyButtonLabel;
|
||||
codeBlock.setAttribute("tabindex", "0");
|
||||
codeBlock.appendChild(copyButton);
|
||||
|
||||
// wrap codebock with relative parent element
|
||||
codeBlock?.parentNode?.insertBefore(wrapper, codeBlock);
|
||||
wrapper.appendChild(codeBlock);
|
||||
|
||||
copyButton.addEventListener("click", async () => {
|
||||
await copyCode(codeBlock, copyButton);
|
||||
});
|
||||
}
|
||||
|
||||
async function copyCode(block, button) {
|
||||
const code = block.querySelector("code");
|
||||
const text = code?.innerText;
|
||||
|
||||
await navigator.clipboard.writeText(text ?? "");
|
||||
|
||||
// visual feedback that task is completed
|
||||
button.innerText = "Copied";
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerText = copyButtonLabel;
|
||||
}, 700);
|
||||
}
|
||||
}
|
||||
attachCopyButtons();
|
||||
|
||||
/** Scrolls the document to the top when
|
||||
* the "Back to Top" button is clicked. */
|
||||
function backToTop() {
|
||||
document.querySelector("#back-to-top")?.addEventListener("click", () => {
|
||||
document.body.scrollTop = 0; // For Safari
|
||||
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
|
||||
});
|
||||
}
|
||||
backToTop();
|
||||
|
||||
/* Go to page start after page swap */
|
||||
document.addEventListener("astro:after-swap", () =>
|
||||
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
|
||||
);
|
||||
</script>
|
||||
Reference in New Issue
Block a user