使用SupaBase JavaScript API与Quartz的自定义Components实现
https://supabase.com/docs/guides/api
https://quartz.jzhao.xyz/advanced/creating-components
实现代码参考
quartz/components/Comment.tsx
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import script from "./scripts/comment.inline"
export default (() => {
function Comment({ fileData }: QuartzComponentProps) {
const title = fileData.frontmatter?.title
return (
<div>
<div style="display: none;" id="comment-title">{title}</div>
<h>评论</h>
<></>
<form id="comment-form">
<br />
<textarea id="comment" rows="5" cols="35" placeholder="Your comment" required></textarea>
<br />
<button type="submit">回复</button>
</form>
<hr />
<div id="comments-section"></div>
<button id="prev-page" disabled>上一页</button><button id="next-page" disabled>下一页</button>
</div>
)
}
Comment.afterDOMLoaded = script
return Comment
}) satisfies QuartzComponentConstructor
quartz/components/scripts/comment.inline.ts
let postId = document.getElementById('comment-title').innerText; // 当前博客文章
function generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
Array.from({ length }).forEach(() => {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
});
return result;
}
function generateUniqueUsername() {
const part1 = generateRandomString(4);
const part2 = generateRandomString(4);
return part1 + '-' + part2;
}
let storedUsername = localStorage.getItem('uniqueUsername');
if (storedUsername === null) {
let username = generateUniqueUsername();
localStorage.setItem('uniqueUsername', username);
}
const supabaseUrl = 'YOUR-supabase.co';
const supabaseAnonKey = 'YOUR-KEY';
// 设置分页参数
let pageIndex = 1;
const pageSize = 10;
async function addComment(name, comment) {
postId = document.getElementById('comment-title').innerText;
const response = await fetch(`${supabaseUrl}/rest/v1/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': supabaseAnonKey,
'Authorization': `Bearer ${supabaseAnonKey}`
},
body: JSON.stringify({ post_id: postId, name: name, comment: comment })
});
if (response.ok) {
console.log('Comment added');
const comment = document.getElementById('comment');
comment.value = '';
} else {
alert('Error inserting comment');
}
}
async function getComments(page) {
// Enable/Disable pagination buttons
postId = document.getElementById('comment-title').innerText;
const totalComments = await getTotalComments(postId);
document.getElementById('prev-page').disabled = page === 1;
document.getElementById('next-page').disabled = page * pageSize >= totalComments;
const from = (page - 1) * pageSize;
const to = from + pageSize - 1;
const response = await fetch(`${supabaseUrl}/rest/v1/comments?post_id=eq.${postId}&select=*&order=created_at.desc&limit=${pageSize}&offset=${from}`, {
method: 'GET',
headers: {
'apikey': supabaseAnonKey,
'Authorization': `Bearer ${supabaseAnonKey}`
}
});
const comments = await response.json();
displayComments(comments);
}
// 获取评论总数的函数
async function getTotalComments() {
postId = document.getElementById('comment-title').innerText;
const response = await fetch(`${supabaseUrl}/rest/v1/comments?post_id=eq.${postId}&select=id&limit=0&offset=0`, {
method: 'GET',
headers: {
'apikey': supabaseAnonKey,
'Authorization': `Bearer ${supabaseAnonKey}`,
'Prefer':'count=exact'
}
});
let contentRange = await response.headers.get('content-range');
let match = contentRange.match(/\/(\d+)$/);
let totalCount = parseInt(match[1], 10);
return totalCount;
}
function displayComments(comments) {
const commentsSection = document.getElementById('comments-section');
commentsSection.innerHTML = '';
comments.forEach(comment => {
const commentElement = document.createElement('div');
commentElement.innerHTML = '【' + comment.name + '】' + comment.comment;
commentsSection.appendChild(commentElement);
});
}
document.getElementById('comment-form').addEventListener('submit', async (event) => {
event.preventDefault();
const name = localStorage.getItem('uniqueUsername');
const comment = document.getElementById('comment').value;
await addComment(name, comment);
});
document.getElementById('prev-page').addEventListener('click', () => {
if (pageIndex > 1) {
pageIndex--;
getComments(pageIndex);
}
});
document.getElementById('next-page').addEventListener('click', () => {
pageIndex++;
getComments(pageIndex);
});
getComments(pageIndex); // 初次加载时获取评论
document.addEventListener("nav", () => {
getComments(pageIndex)
})
未来计划通过Webhook自动刷新新评论,不知道是否能在无服务器的情况下做到。
已实现,有新消息会自动刷新评论
监听表插入发送POST请求到ntfy频道,再使用订阅sse流,有事件则获取最新评论
使用Supabase的RealTime订阅Websocket,速度更快,不依赖第三方ntfy
实现
let sentRef = 1;
const supabaseWS = 'wss://YOUR-SUPABASEURL';
const ws = new WebSocket(`${supabaseWS}/realtime/v1/websocket?apikey=${supabaseAnonKey}&log_level=info&vsn=1.0.0`);
ws.onopen = function(evt){
console.log('socket connection opened properly');
ws.send(JSON.stringify({"topic":"realtime:YOUR-REALTIME","event":"phx_join","payload":{"config":{"broadcast":{"ack":false,"self":false},"presence":{"key":""},"postgres_changes":[{"event":"*","schema":"public","table":"YOUR-TABLE"}]},"access_token": supabaseAnonKey},"ref":sentRef.toString(),"join_ref":"1"}));
sentRef++;
ws.send(JSON.stringify({"topic":"realtime:comments","event":"access_token","payload":{"access_token":supabaseAnonKey},"ref":sentRef.toString(),"join_ref":"1"}));
sentRef++;
}
ws.onmessage = function (evt) {
var j = JSON.parse(evt.data);
if(j.event !== null && j.event === 'postgres_changes')
getComments(pageIndex);
};
// need to keep the ws connection alive by sending a "heartbeat" every 30 seconds
setInterval(() => {
ws.send(JSON.stringify({"topic":"phoenix","event":"heartbeat","payload":{},"ref":sentRef.toString()}));
}, 30000)