SanYeCao-blog/src/components/Comments.astro

341 lines
8.4 KiB
Text

---
import { getLangFromUrl } from "@/i18n";
const limit = Number(Astro.props.limit ?? 5);
const lang = getLangFromUrl(Astro.url);
function formatDate(iso: string) {
return new Date(iso).toISOString().slice(0, 10);
}
---
<section
id="latest-comments"
class="latest-comments"
data-lang={lang}
data-limit={limit}
>
<h2>{lang === "zh" ? "最新评论" : "Latest Comments"}</h2>
<div class="comments-loading" id="comments-loading">
<div class="loading-card"></div>
<div class="loading-card"></div>
<div class="loading-card"></div>
</div>
<ul class="comment-list" id="comment-list" style="display: none;"></ul>
<p class="empty" id="comments-empty" style="display: none;">
{lang === "zh" ? "还没有评论。" : "No comments yet."}
</p>
<p class="empty" id="comments-error" style="display: none;">
{lang === "zh" ? "评论加载失败。" : "Failed to load comments."}
</p>
</section>
<script>
type CommentAuthor = {
login?: string;
avatarUrl?: string;
url?: string;
};
type LatestCommentItem = {
id: string;
body?: string;
updatedAt: string;
author?: CommentAuthor | null;
postId: string;
postTitle: string;
localUrl: string;
isReply: boolean;
replyTo?: string;
};
type CommentsResponse = {
comments?: LatestCommentItem[];
error?: string;
};
const root = document.getElementById(
"latest-comments",
) as HTMLElement | null;
if (root) {
const lang = root.dataset.lang || "zh";
const limit = root.dataset.limit || "5";
const loadingEl = root.querySelector("#comments-loading");
const listEl = root.querySelector("#comment-list");
const emptyEl = root.querySelector("#comments-empty");
const errorEl = root.querySelector("#comments-error");
if (
!(loadingEl instanceof HTMLElement) ||
!(listEl instanceof HTMLElement) ||
!(emptyEl instanceof HTMLElement) ||
!(errorEl instanceof HTMLElement)
) {
throw new Error("Comments elements not found");
}
const formatDate = (iso: string) =>
new Date(iso).toISOString().slice(0, 10);
const renderComment = (comment: LatestCommentItem) => {
console.log("Start render");
const li = document.createElement("li");
li.className = `comment-card ${comment.isReply ? "is-reply" : ""}`;
li.innerHTML = `
<a href="${comment.localUrl}" class="comment-link" data-astro-reload>
<div class="comment-top">
${
comment.author?.avatarUrl
? `<img
src="${comment.author.avatarUrl}"
alt="${comment.author?.login ?? "author"}"
class="comment-avatar"
/>`
: ""
}
<div class="comment-meta">
<span class="comment-author">${comment.author?.login ?? "Unknown"}</span>
<span class="comment-date">${formatDate(comment.updatedAt)}</span>
</div>
<span class="comment-post-title" title="${comment.postTitle}">
${comment.postTitle}
</span>
</div>
${
comment.isReply
? `<p class="comment-kind">${
lang === "zh"
? `回复 @${comment.replyTo ?? "Unknown"}`
: `Reply to @${comment.replyTo ?? "Unknown"}`
}</p>`
: ""
}
<p class="comment-body">${comment.body ?? ""}</p>
</a>
`;
return li;
};
fetch(`/api/comments.json?lang=${lang}&limit=${limit}`)
.then((res) => {
if (!res.ok) throw new Error("Request failed");
return res.json() as Promise<CommentsResponse>;
})
.then((data) => {
loadingEl.style.display = "none";
const comments = data.comments ?? [];
if (!comments.length) {
emptyEl.style.display = "block";
return;
}
listEl.style.display = "grid";
comments.forEach((comment) => {
listEl.appendChild(renderComment(comment));
});
})
.catch((err) => {
console.error("Comments fetch failed:", err);
loadingEl.style.display = "none";
errorEl.style.display = "block";
});
}
</script>
<style is:global>
.latest-comments {
margin-top: 1.5rem;
}
.latest-comments h2 {
margin-bottom: 0.8rem;
}
.comment-list {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.9rem;
}
.comment-card {
margin: 0;
}
.comment-link {
display: block;
padding: 0.9rem 1rem;
text-decoration: none;
color: inherit;
border: 1px dashed #aeb8c2;
background: rgba(160, 175, 190, 0.06);
}
.comment-card.is-reply .comment-link {
border-style: dashed;
background: rgba(160, 175, 190, 0.1);
}
.comment-top {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.comment-meta {
display: flex;
flex-direction: column;
min-width: 0;
}
.comment-post-title {
margin-left: auto;
text-align: right;
font-size: 0.92rem;
font-weight: 700;
color: #6f8090;
max-width: 45%;
line-height: 1.35;
overflow-wrap: anywhere;
}
.comment-avatar {
width: 42px;
height: 42px;
object-fit: cover;
display: block;
}
.comment-author {
font-weight: 700;
line-height: 1.3;
}
.comment-date {
font-size: 0.9rem;
color: #6f8090;
}
.comment-kind {
margin: 0 0 0.35rem 0;
font-size: 0.82rem;
color: #8c8c8c;
font-style: italic;
}
.comment-body {
margin: 0;
color: #555;
line-height: 1.6;
font-style: italic;
}
.empty {
color: #6f8090;
}
.comments-loading {
display: grid;
gap: 0.9rem;
}
.loading-card {
height: 92px;
border: 1px dashed #aeb8c2;
background: linear-gradient(
90deg,
rgba(160, 175, 190, 0.06) 25%,
rgba(160, 175, 190, 0.16) 50%,
rgba(160, 175, 190, 0.06) 75%
);
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
}
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
:global(.dark) .comment-link {
border-color: #7f8c97;
background: rgba(180, 190, 200, 0.06);
}
:global(.dark) .comment-card.is-reply .comment-link {
background: rgba(180, 190, 200, 0.1);
}
:global(.dark) .comment-post-title {
color: #c8d2dc;
}
:global(.dark) .comment-date,
:global(.dark) .empty {
color: #aab7c4;
}
:global(.dark) .comment-kind {
color: #b8b8b8;
}
:global(.dark) .comment-body {
color: #d3d7db;
}
:global(.dark) .loading-card {
border-color: #7f8c97;
background: linear-gradient(
90deg,
rgba(180, 190, 200, 0.06) 25%,
rgba(180, 190, 200, 0.16) 50%,
rgba(180, 190, 200, 0.06) 75%
);
background-size: 200% 100%;
}
@media (max-width: 640px) {
.comment-top {
align-items: flex-start;
flex-wrap: wrap;
}
.comment-post-title {
margin-left: 0;
max-width: 100%;
width: 100%;
text-align: left;
}
}
</style>