使用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)

参考
Listening to database changes in Supabase hosted project using WebSocket or any third party library - Stack Overflow