mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-02-03 22:15:42 -06:00
lint
This commit is contained in:
parent
320d0047e9
commit
93f6bc2e55
@ -5,9 +5,7 @@ import * as Component from "./quartz/components"
|
|||||||
export const sharedPageComponents: SharedLayout = {
|
export const sharedPageComponents: SharedLayout = {
|
||||||
head: Component.Head(),
|
head: Component.Head(),
|
||||||
header: [],
|
header: [],
|
||||||
afterBody: [
|
afterBody: [Component.BlueskyComments()],
|
||||||
Component.BlueskyComments(),
|
|
||||||
],
|
|
||||||
footer: Component.Footer({
|
footer: Component.Footer({
|
||||||
links: {
|
links: {
|
||||||
GitHub: "https://github.com/jackyzha0/quartz",
|
GitHub: "https://github.com/jackyzha0/quartz",
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -6,22 +5,30 @@ import script from "./scripts/bluesky.inline"
|
|||||||
import style from "./styles/bluesky.scss"
|
import style from "./styles/bluesky.scss"
|
||||||
|
|
||||||
const BlueskyComments: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
const BlueskyComments: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => {
|
||||||
const blueskyUrl = fileData.frontmatter?.blueskyUrl as string | undefined
|
const blueskyUrl = fileData.frontmatter?.blueskyUrl as string | undefined
|
||||||
if (!blueskyUrl) {
|
if (!blueskyUrl) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(displayClass, "bluesky-comments-container")} id="bluesky-comments" data-url={blueskyUrl}>
|
<div
|
||||||
<h2>Comments</h2>
|
class={classNames(displayClass, "bluesky-comments-container")}
|
||||||
<p class="bluesky-meta">
|
id="bluesky-comments"
|
||||||
Post a reply on <a href={blueskyUrl} target="_blank" rel="noopener noreferrer">Bluesky</a> to join the conversation.
|
data-url={blueskyUrl}
|
||||||
</p>
|
>
|
||||||
<div id="bluesky-comments-list">
|
<h2>Comments</h2>
|
||||||
<p>Loading comments...</p>
|
<p class="bluesky-meta">
|
||||||
</div>
|
Post a reply on{" "}
|
||||||
</div>
|
<a href={blueskyUrl} target="_blank" rel="noopener noreferrer">
|
||||||
)
|
Bluesky
|
||||||
|
</a>{" "}
|
||||||
|
to join the conversation.
|
||||||
|
</p>
|
||||||
|
<div id="bluesky-comments-list">
|
||||||
|
<p>Loading comments...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
BlueskyComments.afterDOMLoaded = script
|
BlueskyComments.afterDOMLoaded = script
|
||||||
|
|||||||
@ -1,199 +1,196 @@
|
|||||||
|
|
||||||
document.addEventListener("nav", async () => {
|
document.addEventListener("nav", async () => {
|
||||||
const container = document.getElementById("bluesky-comments")
|
const container = document.getElementById("bluesky-comments")
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
const url = container.getAttribute("data-url")
|
const url = container.getAttribute("data-url")
|
||||||
if (!url) return
|
if (!url) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { handle, postId } = parseBlueskyUrl(url)
|
const { handle, postId } = parseBlueskyUrl(url)
|
||||||
const did = await resolveHandle(handle)
|
const did = await resolveHandle(handle)
|
||||||
const replies = await fetchThread(did, postId)
|
const replies = await fetchThread(did, postId)
|
||||||
renderComments(replies)
|
renderComments(replies)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error loading Bluesky comments:", e)
|
console.error("Error loading Bluesky comments:", e)
|
||||||
const list = document.getElementById("bluesky-comments-list")
|
const list = document.getElementById("bluesky-comments-list")
|
||||||
if (list) {
|
if (list) {
|
||||||
list.textContent = "Error loading comments."
|
list.textContent = "Error loading comments."
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function parseBlueskyUrl(url: string) {
|
function parseBlueskyUrl(url: string) {
|
||||||
const urlParts = new URL(url).pathname.split("/")
|
const urlParts = new URL(url).pathname.split("/")
|
||||||
const handle = urlParts[2]
|
const handle = urlParts[2]
|
||||||
const postId = urlParts[4]
|
const postId = urlParts[4]
|
||||||
|
|
||||||
if (!handle || !postId) throw new Error("Invalid Bluesky URL")
|
if (!handle || !postId) throw new Error("Invalid Bluesky URL")
|
||||||
return { handle, postId }
|
return { handle, postId }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveHandle(handle: string) {
|
async function resolveHandle(handle: string) {
|
||||||
const resolveRes = await fetch(
|
const resolveRes = await fetch(
|
||||||
`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`
|
`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`,
|
||||||
)
|
)
|
||||||
if (!resolveRes.ok) throw new Error("Could not resolve handle")
|
if (!resolveRes.ok) throw new Error("Could not resolve handle")
|
||||||
const { did } = await resolveRes.json()
|
const { did } = await resolveRes.json()
|
||||||
return did
|
return did
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchThread(did: string, postId: string) {
|
async function fetchThread(did: string, postId: string) {
|
||||||
const atUri = `at://${did}/app.bsky.feed.post/${postId}`
|
const atUri = `at://${did}/app.bsky.feed.post/${postId}`
|
||||||
const threadRes = await fetch(
|
const threadRes = await fetch(
|
||||||
`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${atUri}&depth=10&parentHeight=0`
|
`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${atUri}&depth=10&parentHeight=0`,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!threadRes.ok) throw new Error("Could not fetch thread")
|
if (!threadRes.ok) throw new Error("Could not fetch thread")
|
||||||
const data = await threadRes.json()
|
const data = await threadRes.json()
|
||||||
return data.thread.replies
|
return data.thread.replies
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderComments(replies: any[]) {
|
function renderComments(replies: any[]) {
|
||||||
const commentsList = document.getElementById("bluesky-comments-list")
|
const commentsList = document.getElementById("bluesky-comments-list")
|
||||||
if (!commentsList) return
|
if (!commentsList) return
|
||||||
|
|
||||||
if (replies && replies.length > 0) {
|
if (replies && replies.length > 0) {
|
||||||
commentsList.innerHTML = "" // Clear loading text
|
commentsList.innerHTML = "" // Clear loading text
|
||||||
// Sort by likes
|
// Sort by likes
|
||||||
replies.sort((a: any, b: any) =>
|
replies.sort((a: any, b: any) => (b.post.likeCount || 0) - (a.post.likeCount || 0))
|
||||||
(b.post.likeCount || 0) - (a.post.likeCount || 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
replies.forEach((reply: any) => {
|
replies.forEach((reply: any) => {
|
||||||
if (!reply.post) return
|
if (!reply.post) return
|
||||||
commentsList.appendChild(createCommentNode(reply))
|
commentsList.appendChild(createCommentNode(reply))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
commentsList.textContent = "No comments yet."
|
commentsList.textContent = "No comments yet."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCommentNode(reply: any): HTMLElement {
|
function createCommentNode(reply: any): HTMLElement {
|
||||||
const post = reply.post
|
const post = reply.post
|
||||||
const author = post.author
|
const author = post.author
|
||||||
const date = new Date(post.indexedAt).toLocaleDateString()
|
const date = new Date(post.indexedAt).toLocaleDateString()
|
||||||
|
|
||||||
const div = document.createElement("div")
|
const div = document.createElement("div")
|
||||||
div.className = "comment"
|
div.className = "comment"
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
const header = document.createElement("div")
|
const header = document.createElement("div")
|
||||||
header.className = "comment-header"
|
header.className = "comment-header"
|
||||||
|
|
||||||
if (author.avatar) {
|
if (author.avatar) {
|
||||||
const avatar = document.createElement("img")
|
const avatar = document.createElement("img")
|
||||||
avatar.src = author.avatar
|
avatar.src = author.avatar
|
||||||
avatar.alt = author.handle
|
avatar.alt = author.handle
|
||||||
header.appendChild(avatar)
|
header.appendChild(avatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLink = document.createElement("a")
|
const handleLink = document.createElement("a")
|
||||||
const postId = post.uri.split("/").pop()
|
const postId = post.uri.split("/").pop()
|
||||||
handleLink.href = `https://bsky.app/profile/${author.did}/post/${postId}`
|
handleLink.href = `https://bsky.app/profile/${author.did}/post/${postId}`
|
||||||
handleLink.target = "_blank"
|
handleLink.target = "_blank"
|
||||||
handleLink.className = "handle"
|
handleLink.className = "handle"
|
||||||
handleLink.textContent = author.displayName || author.handle
|
handleLink.textContent = author.displayName || author.handle
|
||||||
header.appendChild(handleLink)
|
header.appendChild(handleLink)
|
||||||
|
|
||||||
const timeSpan = document.createElement("span")
|
const timeSpan = document.createElement("span")
|
||||||
timeSpan.className = "time"
|
timeSpan.className = "time"
|
||||||
timeSpan.textContent = date
|
timeSpan.textContent = date
|
||||||
header.appendChild(timeSpan)
|
header.appendChild(timeSpan)
|
||||||
|
|
||||||
div.appendChild(header)
|
div.appendChild(header)
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
div.appendChild(renderPostBody(post))
|
div.appendChild(renderPostBody(post))
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const actions = document.createElement("div")
|
const actions = document.createElement("div")
|
||||||
actions.className = "comment-actions"
|
actions.className = "comment-actions"
|
||||||
|
|
||||||
const likes = document.createElement("span")
|
const likes = document.createElement("span")
|
||||||
likes.textContent = `❤️ ${post.likeCount || 0}`
|
likes.textContent = `❤️ ${post.likeCount || 0}`
|
||||||
actions.appendChild(likes)
|
actions.appendChild(likes)
|
||||||
|
|
||||||
const reposts = document.createElement("span")
|
const reposts = document.createElement("span")
|
||||||
reposts.textContent = `🔁 ${post.repostCount || 0}`
|
reposts.textContent = `🔁 ${post.repostCount || 0}`
|
||||||
actions.appendChild(reposts)
|
actions.appendChild(reposts)
|
||||||
|
|
||||||
div.appendChild(actions)
|
div.appendChild(actions)
|
||||||
|
|
||||||
// Recursive rendering for nested replies
|
// Recursive rendering for nested replies
|
||||||
if (reply.replies && reply.replies.length > 0) {
|
if (reply.replies && reply.replies.length > 0) {
|
||||||
const repliesContainer = document.createElement("div")
|
const repliesContainer = document.createElement("div")
|
||||||
repliesContainer.className = "replies"
|
repliesContainer.className = "replies"
|
||||||
reply.replies.forEach((child: any) => {
|
reply.replies.forEach((child: any) => {
|
||||||
if (!child.post) return
|
if (!child.post) return
|
||||||
repliesContainer.appendChild(createCommentNode(child))
|
repliesContainer.appendChild(createCommentNode(child))
|
||||||
})
|
})
|
||||||
div.appendChild(repliesContainer)
|
div.appendChild(repliesContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
function renderPostBody(post: any): HTMLElement {
|
function renderPostBody(post: any): HTMLElement {
|
||||||
const text = post.record.text
|
const text = post.record.text
|
||||||
const facets = post.record.facets
|
const facets = post.record.facets
|
||||||
const body = document.createElement("div")
|
const body = document.createElement("div")
|
||||||
body.className = "comment-body"
|
body.className = "comment-body"
|
||||||
|
|
||||||
if (!facets || facets.length === 0) {
|
|
||||||
body.textContent = text
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
facets.sort((a: any, b: any) => a.index.byteStart - b.index.byteStart)
|
|
||||||
|
|
||||||
const bytes = new TextEncoder().encode(text)
|
|
||||||
let lastByteIndex = 0
|
|
||||||
|
|
||||||
for (const facet of facets) {
|
|
||||||
const { byteStart, byteEnd } = facet.index
|
|
||||||
|
|
||||||
if (byteStart > lastByteIndex) {
|
|
||||||
const slice = bytes.slice(lastByteIndex, byteStart)
|
|
||||||
const textNode = document.createTextNode(new TextDecoder().decode(slice))
|
|
||||||
body.appendChild(textNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
const slice = bytes.slice(byteStart, byteEnd)
|
|
||||||
const facetText = new TextDecoder().decode(slice)
|
|
||||||
const feature = facet.features[0]
|
|
||||||
|
|
||||||
if (feature["$type"] === "app.bsky.richtext.facet#link") {
|
|
||||||
const link = document.createElement("a")
|
|
||||||
link.href = feature.uri
|
|
||||||
link.target = "_blank"
|
|
||||||
link.rel = "noopener noreferrer"
|
|
||||||
link.textContent = facetText
|
|
||||||
body.appendChild(link)
|
|
||||||
} else if (feature["$type"] === "app.bsky.richtext.facet#mention") {
|
|
||||||
const link = document.createElement("a")
|
|
||||||
link.href = `https://bsky.app/profile/${feature.did}`
|
|
||||||
link.target = "_blank"
|
|
||||||
link.rel = "noopener noreferrer"
|
|
||||||
link.textContent = facetText
|
|
||||||
body.appendChild(link)
|
|
||||||
} else if (feature["$type"] === "app.bsky.richtext.facet#tag") {
|
|
||||||
const link = document.createElement("a")
|
|
||||||
link.href = `https://bsky.app/hashtag/${feature.tag}`
|
|
||||||
link.target = "_blank"
|
|
||||||
link.rel = "noopener noreferrer"
|
|
||||||
link.textContent = facetText
|
|
||||||
body.appendChild(link)
|
|
||||||
} else {
|
|
||||||
body.appendChild(document.createTextNode(facetText))
|
|
||||||
}
|
|
||||||
|
|
||||||
lastByteIndex = byteEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastByteIndex < bytes.length) {
|
|
||||||
const slice = bytes.slice(lastByteIndex)
|
|
||||||
body.appendChild(document.createTextNode(new TextDecoder().decode(slice)))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!facets || facets.length === 0) {
|
||||||
|
body.textContent = text
|
||||||
return body
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
facets.sort((a: any, b: any) => a.index.byteStart - b.index.byteStart)
|
||||||
|
|
||||||
|
const bytes = new TextEncoder().encode(text)
|
||||||
|
let lastByteIndex = 0
|
||||||
|
|
||||||
|
for (const facet of facets) {
|
||||||
|
const { byteStart, byteEnd } = facet.index
|
||||||
|
|
||||||
|
if (byteStart > lastByteIndex) {
|
||||||
|
const slice = bytes.slice(lastByteIndex, byteStart)
|
||||||
|
const textNode = document.createTextNode(new TextDecoder().decode(slice))
|
||||||
|
body.appendChild(textNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = bytes.slice(byteStart, byteEnd)
|
||||||
|
const facetText = new TextDecoder().decode(slice)
|
||||||
|
const feature = facet.features[0]
|
||||||
|
|
||||||
|
if (feature["$type"] === "app.bsky.richtext.facet#link") {
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = feature.uri
|
||||||
|
link.target = "_blank"
|
||||||
|
link.rel = "noopener noreferrer"
|
||||||
|
link.textContent = facetText
|
||||||
|
body.appendChild(link)
|
||||||
|
} else if (feature["$type"] === "app.bsky.richtext.facet#mention") {
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = `https://bsky.app/profile/${feature.did}`
|
||||||
|
link.target = "_blank"
|
||||||
|
link.rel = "noopener noreferrer"
|
||||||
|
link.textContent = facetText
|
||||||
|
body.appendChild(link)
|
||||||
|
} else if (feature["$type"] === "app.bsky.richtext.facet#tag") {
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = `https://bsky.app/hashtag/${feature.tag}`
|
||||||
|
link.target = "_blank"
|
||||||
|
link.rel = "noopener noreferrer"
|
||||||
|
link.textContent = facetText
|
||||||
|
body.appendChild(link)
|
||||||
|
} else {
|
||||||
|
body.appendChild(document.createTextNode(facetText))
|
||||||
|
}
|
||||||
|
|
||||||
|
lastByteIndex = byteEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastByteIndex < bytes.length) {
|
||||||
|
const slice = bytes.slice(lastByteIndex)
|
||||||
|
body.appendChild(document.createTextNode(new TextDecoder().decode(slice)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,77 +1,77 @@
|
|||||||
.bluesky-comments-container {
|
.bluesky-comments-container {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
border-top: 1px solid var(--lightgray);
|
border-top: 1px solid var(--lightgray);
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bluesky-meta {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--gray);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bluesky-meta {
|
.comment {
|
||||||
margin-bottom: 1.5rem;
|
margin-top: 1rem;
|
||||||
color: var(--gray);
|
padding-left: 1rem;
|
||||||
|
border-left: 2px solid var(--lightgray);
|
||||||
|
|
||||||
|
.comment-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--dark);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
.time {
|
||||||
color: var(--secondary);
|
color: var(--gray);
|
||||||
text-decoration: none;
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment {
|
.comment-body {
|
||||||
margin-top: 1rem;
|
font-size: 0.95rem;
|
||||||
padding-left: 1rem;
|
color: var(--darkgray);
|
||||||
border-left: 2px solid var(--lightgray);
|
white-space: pre-wrap;
|
||||||
|
|
||||||
.comment-header {
|
a {
|
||||||
display: flex;
|
color: var(--secondary);
|
||||||
align-items: center;
|
text-decoration: none;
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
img {
|
&:hover {
|
||||||
width: 24px;
|
text-decoration: underline;
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.handle {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--dark);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
color: var(--gray);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-body {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--darkgray);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--gray);
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.comment-actions {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--gray);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user