diff --git a/.vscode/settings.json b/.vscode/settings.json index 73ee75d..8a66df4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,6 +28,7 @@ "cimage", "ckenh", "cnip", + "commentform", "csvg", "ctan", "cynosura", diff --git a/README.md b/README.md index 19bb882..7b5eb16 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,6 @@ For instance, the [giscus](https://giscus.app) is an opinionated choice. - [ ] Add music to the articles. Remain **54** posts. - [ ] Clean up the legacy links in the weblog comments. - [ ] External the article inner links with different target. -- [ ] Artalk integration with custom stylesheet. - [ ] Slide share components integration. ### Long Term Goals diff --git a/astro.config.ts b/astro.config.ts index daad670..b150148 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ POSTGRES_USERNAME: envField.string({ context: 'server', access: 'secret' }), POSTGRES_PASSWORD: envField.string({ context: 'server', access: 'secret' }), POSTGRES_DATABASE: envField.string({ context: 'server', access: 'secret' }), + ARTALK_HOST: envField.string({ context: 'server', access: 'secret' }), }, }, }, diff --git a/package-lock.json b/package-lock.json index c843cfa..d47d21a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "fuse.js": "^7.0.0", "lodash": "^4.17.21", "luxon": "^3.4.4", + "marked": "^13.0.0", "pg": "^8.12.0", "qrcode-svg": "^1.1.0", "ultrahtml": "^1.5.3" @@ -32,7 +33,6 @@ "@types/qrcode-svg": "^1.1.4", "@types/unist": "^3.0.2", "aplayer": "^1.10.1", - "artalk": "^2.8.7", "bootstrap": "^5.3.3", "prettier": "^3.3.2", "prettier-plugin-astro": "^0.14.0", @@ -2901,13 +2901,6 @@ "dev": true, "license": "MIT" }, - "node_modules/abortcontroller-polyfill": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", - "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/acorn": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", @@ -3053,25 +3046,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/artalk": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/artalk/-/artalk-2.8.7.tgz", - "integrity": "sha512-Pl6oKnG0mLf6/c0X93SEmUm7RqqO1zsfvf4j8EzO25pvcDn9pJdYQqLrWKupaHwO7JLvEcgq5pBBrJCLmBKGpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "abortcontroller-polyfill": "^1.7.5", - "hanabi": "^0.4.0", - "insane": "^2.6.2", - "marked": "^12.0.2" - } - }, - "node_modules/assignment": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assignment/-/assignment-2.0.0.tgz", - "integrity": "sha512-naMULXjtgCs9SVUEtyvJNt68aF18em7/W+dhbR59kbz9cXWPEvUkCun2tqlgqRPSqZaKPpqLc5ZnwL8jVmJRvw==", - "dev": true - }, "node_modules/astring": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", @@ -3722,16 +3696,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/comment-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/comment-regex/-/comment-regex-1.0.1.tgz", - "integrity": "sha512-IWlN//Yfby92tOIje7J18HkNmWRR7JESA/BK8W7wqY/akITpU5B0JQWnbTjCfdChSrDNb0DrdA9jfAxiiBXyiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/common-ancestor-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", @@ -4580,16 +4544,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/hanabi": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/hanabi/-/hanabi-0.4.0.tgz", - "integrity": "sha512-ixJH94fwmmVzUSdxl7TMkVZJmsq4d2JKrxedpM5V1V+91iVHL0q6NnJi4xiDahK6Vo00xT17H8H6b4F6RVbsOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "comment-regex": "^1.0.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4859,16 +4813,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/he": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/he/-/he-0.5.0.tgz", - "integrity": "sha512-DoufbNNOFzwRPy8uecq+j+VCPQ+JyDelHTmSgygrA5TsR8Cbw4Qcir5sGtWiusB4BdT89nmlaVDhSJOqC/33vw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -4938,17 +4882,6 @@ "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", "license": "MIT" }, - "node_modules/insane": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/insane/-/insane-2.6.2.tgz", - "integrity": "sha512-BqEL1CJsjJi+/C/zKZxv31zs3r6zkLH5Nz1WMFb7UBX2KHY2yXDpbFTSEmNHzomBbGDysIfkTX55A0mQZ2CQiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assignment": "2.0.0", - "he": "0.5.0" - } - }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -5450,10 +5383,9 @@ } }, "node_modules/marked": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", - "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", - "dev": true, + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.0.tgz", + "integrity": "sha512-VTeDCd9txf4KLLljUZ0nljE/Incb9SrWuueE44QVuU0pkOdh4sfCeW1Z6lPcxyDRSVY6rm8db/0OPaN75RNUmw==", "license": "MIT", "bin": { "marked": "bin/marked.js" diff --git a/package.json b/package.json index 3bf4861..93898ff 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "fuse.js": "^7.0.0", "lodash": "^4.17.21", "luxon": "^3.4.4", + "marked": "^13.0.0", "pg": "^8.12.0", "qrcode-svg": "^1.1.0", "ultrahtml": "^1.5.3" @@ -64,7 +65,6 @@ "@types/qrcode-svg": "^1.1.4", "@types/unist": "^3.0.2", "aplayer": "^1.10.1", - "artalk": "^2.8.7", "bootstrap": "^5.3.3", "prettier": "^3.3.2", "prettier-plugin-astro": "^0.14.0", diff --git a/public/images/2024/05/2024052911583500.jpg b/public/images/2024/05/2024052911583500.jpg index 1a65bce..f14a908 100644 Binary files a/public/images/2024/05/2024052911583500.jpg and b/public/images/2024/05/2024052911583500.jpg differ diff --git a/public/images/default-avatar.png b/public/images/default-avatar.png new file mode 100644 index 0000000..5e9d814 Binary files /dev/null and b/public/images/default-avatar.png differ diff --git a/src/asserts/scripts/yufan.me.js b/src/asserts/scripts/yufan.me.js index 94a3fbb..eb72487 100644 --- a/src/asserts/scripts/yufan.me.js +++ b/src/asserts/scripts/yufan.me.js @@ -1,5 +1,4 @@ import Aplayer from 'aplayer/dist/APlayer.min.js'; -import Artalk from 'artalk/dist/ArtalkLite'; import stickySidebar from './sticky-sidebar.js'; // Menu toggle. @@ -87,16 +86,112 @@ document.querySelector('.global-search-close').addEventListener('click', (event) }); // Loading the comments. -const comment = document.querySelector('#comments'); -if (typeof comment !== 'undefined' && comment !== null) { - const { key, title, server, site } = comment.dataset; - Artalk.init({ - el: '#comments', - pageKey: key, - pageTitle: title, - server: server, - site: site, +const comments = document.querySelector('#comments'); +if (typeof comments !== 'undefined' && comments !== null) { + const cancel = comments.querySelector('#cancel-comment-reply-link'); + const replyForm = comments.querySelector('#respond'); + const cancelReply = () => { + cancel.hidden = true; + replyForm.querySelector('input[name="rid"]').value = '0'; + replyForm.querySelector('textarea[name="content"]').value = ''; + + // Move the form back to top. + const commentCount = comments.querySelector('.comment-total-count'); + commentCount.after(replyForm); + }; + + // TODO: Load the commenter information from the cookie. + + comments.addEventListener('focusout', (event) => { + if (event.target === document.querySelector('input[name="email"]')) { + event.stopPropagation(); + const email = event.target.value; + if (email !== '' && email.includes('@')) { + // Replace the avatar after typing the email. + fetch(`/comments/avatar?email=${email}`) + .then((res) => res.text()) + .then((link) => { + document.querySelector('#commentForm img.avatar').src = link; + }) + .catch((e) => console.log(e)); + } else { + document.querySelector('#commentForm img.avatar').src = '/images/default-avatar.png'; + } + } }); + + comments.addEventListener('click', async (event) => { + // Loading more comments from server. + if (event.target === comments.querySelector('#comments-next-button')) { + const { size, offset, key } = event.target.dataset; + const html = await fetch(`/comments/list?key=${key}&offset=${offset}`) + .then((res) => res.text()) + .catch((e) => { + console.log(e); + return ''; + }); + if (html === '') { + // Remove the load more button. + event.target.remove(); + } else { + // Append the comments into the list. + event.target.dataset.offset = Number(offset) + Number(size); + comments.querySelector('.comment-list').insertAdjacentHTML('beforeend', html); + } + } + + // Reply a comment. + if (event.target.matches('.comment-reply-link')) { + cancel.hidden = false; + replyForm.querySelector('input[name="rid"]').value = event.target.dataset.rid; + + // Move form to the reply. + const commentItem = event.target.closest('li'); + commentItem.after(replyForm); + replyForm.querySelector('#content').focus(); + } + + // Cancel reply comment. + if (event.target === cancel) { + cancelReply(); + } + }); + + // Reply a comment. + comments.addEventListener('submit', async (event) => { + event.preventDefault(); + event.stopPropagation(); + + const formData = new FormData(event.target); + const data = {}; + for (const [key, value] of formData) { + data[key] = value; + } + + const resp = await fetch('/comments/new', { + method: 'POST', + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify(data), + }) + .then((res) => res.text()) + .catch((e) => { + console.log(e); + return '
  • 评论失败
  • '; + }); + + if (data.rid !== '0') { + replyForm.insertAdjacentHTML('beforebegin', resp); + } else { + const list = comments.querySelector('.comment-list'); + list.insertAdjacentHTML('afterbegin', resp); + } + + cancelReply(); + }); + + // TODO: Highlighting the selected comment. } // Add like button for updating likes. @@ -104,7 +199,7 @@ const likeButton = document.querySelector('button.post-like'); const increaseLikes = (count) => { count.textContent = Number.parseInt(count.textContent) + 1; - fetch(`${window.location.href}/likes`, { + fetch('/likes', { method: 'POST', headers: { 'Content-type': 'application/json; charset=UTF-8', @@ -125,7 +220,7 @@ const decreaseLikes = (count) => { } count.textContent = Number.parseInt(count.textContent) - 1; - fetch(`${window.location.href}/likes`, { + fetch('/likes', { method: 'POST', headers: { 'Content-type': 'application/json; charset=UTF-8', diff --git a/src/asserts/styles/globals.css b/src/asserts/styles/globals.css index 61978f8..f87f91b 100644 --- a/src/asserts/styles/globals.css +++ b/src/asserts/styles/globals.css @@ -1059,7 +1059,7 @@ textarea.form-control { position: relative; line-height: 1; white-space: nowrap; - font-weight: bold; + font-weight: 600; display: -ms-flexbox; display: -webkit-box; display: flex; @@ -2610,7 +2610,7 @@ a:hover .overlay { .post-content > table th, .post-content div > table th { - font-weight: 500; + font-weight: 600; } .post-content > table th, @@ -2643,7 +2643,7 @@ a:hover .overlay { .post-content caption { background: var(--bg-light); - font-weight: 500; + font-weight: 600; padding: 0.5em; text-align: center; } @@ -2847,7 +2847,7 @@ a:hover .overlay { } .widget_recent_comments ul li span { - font-weight: 500; + font-weight: 600; color: var(--color-dark); margin-right: 5px; } @@ -3092,7 +3092,7 @@ a:hover .overlay { } .widget_calendar td#today { - font-weight: bold; + font-weight: 600; color: var(--color-primary); } @@ -3550,6 +3550,197 @@ a:hover .overlay { } } +/*-------------------------------------------------------------- + comment style +--------------------------------------------------------------*/ + +.comment-list .comment-respond { + position: relative; + margin: 0 0 2rem 0; +} + +.comment { + position: relative; + margin: 0 0 1.5rem; + padding: 0 0 1.5rem; + border-bottom: 1px solid var(--border-light); +} + +.comment:last-child { + margin: 0; + padding: 0; + border-bottom: 0; +} + +.comment-form { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.comment-body { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.comment_at { + font-weight: 600; + color: var(--color-dark); +} + +.comment-author a { + vertical-align: middle; +} + +.comment-from-avatar, +.comment-avatar { + width: 40px; + height: 40px; + margin: 0 0.9375rem 0 0; +} + +.comment .comment-inner { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.comment .comment-content { + margin: 0.5rem 0 0.5rem; + line-height: 1.85; +} + +.comment .comment-footer { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.comment .comment-footer .comment-date { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.children { + font-size: 0.875rem; + padding: 1.5rem; + margin: 1.25rem 0 0 3.5rem; + border-radius: var(--radius-sm); + background-color: var(--bg-light); +} + +.children .comment { + margin: 0 0 1rem; + padding: 0; + border-bottom: 0; +} + +.children .comment:last-child { + margin: 0; +} + +.children .form-control { + background-color: var(--bg-white); +} + +.children .comment-from-avatar, +.children .comment .comment-avatar { + width: 30px; + height: 30px; +} + +.children .comment .comment-content { + margin: 0.375rem 0 0.375rem; + word-wrap: break-word; + word-break: break-all; +} + +.children .comment .comment-inner { + margin: 0.25rem 0 0 0; +} + +.children .comment .comment-footer .comment-date { + -webkit-box-flex: 0; + -ms-flex: none; + flex: none; +} + +.comment-reply-link { + cursor: pointer; +} + +.comment-reply-link:hover { + transition: all 500ms; + font-weight: 800; +} + +@media only screen and (max-width: 767px) { + .comment-list .comment-respond { + position: relative; + margin: 0 0 2rem; + } + .comment { + position: relative; + margin: 0 0 1rem; + padding: 0 0 1rem; + } + .comment-from-avatar, + .comment-avatar { + width: 28px; + height: 28px; + margin: 0 0.625rem 0 0; + } + .comment .comment-inner { + margin: 0.125rem 0 0 0; + } + .comment .comment-content { + margin: 0.5rem 0 0.5rem; + } + .children { + padding: 1rem; + margin: 1rem 0 0 2.375rem; + border-radius: var(--radius-sm); + background-color: var(--bg-light); + } + .children .comment { + margin: 0 0 1rem; + padding: 0; + border-bottom: 0; + } + .children .comment:last-child { + margin: 0; + } + .children .form-control { + background-color: var(--bg-white); + } + .children .comment-from-avatar, + .children .comment .comment-avatar { + width: 28px; + height: 28px; + } + .children .comment .comment-content { + margin: 0.3125rem 0 0.3125rem; + } + .children .comment .comment-inner { + margin: 0.125rem 0 0 0; + } +} + /* Quotes ------------------------------------ */ blockquote { diff --git a/src/asserts/styles/reset.css b/src/asserts/styles/reset.css index 844808f..575912c 100644 --- a/src/asserts/styles/reset.css +++ b/src/asserts/styles/reset.css @@ -292,7 +292,7 @@ h3, h4, h5, h6 { - font-weight: 500; + font-weight: 600; margin: 0; } @@ -659,7 +659,7 @@ dd { } dt { - font-weight: 500; + font-weight: 600; } dt + dd { @@ -688,7 +688,7 @@ cite { color: inherit; font-size: 85%; font-style: normal; - font-weight: 500; + font-weight: 600; line-height: 1.25; } diff --git a/src/components/comment/Artalk.astro b/src/components/comment/Artalk.astro deleted file mode 100644 index 18d7b85..0000000 --- a/src/components/comment/Artalk.astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -import { options } from '@/helpers/schema'; -import 'artalk/dist/ArtalkLite.css'; - -interface Props { - commentKey: string; - title: string; -} - -const { commentKey, title } = Astro.props; ---- - -
    - 评论正在加载中 ... -
    diff --git a/src/components/comment/Comment.astro b/src/components/comment/Comment.astro new file mode 100644 index 0000000..2a95a5e --- /dev/null +++ b/src/components/comment/Comment.astro @@ -0,0 +1,18 @@ +--- +/** + * This module is a reuse component which can be used in ajax loading. + */ +import CommentItem from '@/components/comment/CommentItem.astro'; +import { parseComments } from '@/components/comment/artalk'; +import type { CommentConfig, Comments } from '@/components/comment/types'; + +interface Props { + comments: Comments; + config: CommentConfig; +} + +const { comments, config } = Astro.props; +const parsedComments = await parseComments(comments.comments); +--- + +{parsedComments.map((item) => )} diff --git a/src/components/comment/CommentItem.astro b/src/components/comment/CommentItem.astro new file mode 100644 index 0000000..d9f0ed6 --- /dev/null +++ b/src/components/comment/CommentItem.astro @@ -0,0 +1,67 @@ +--- +import type { CommentConfig, CommentItem } from '@/components/comment/types'; +import { formatLocalDate } from '@/helpers/formatter'; +import { urlJoin } from '@/helpers/tools'; + +interface Props { + depth: number; + comment: CommentItem; + config: CommentConfig; + pending?: false; +} + +const { comment, config, depth, pending } = Astro.props; +--- + +
  • +
    +
    + {comment.nick} +
    +
    +
    + { + comment.link === '' ? ( + comment.nick + ) : ( + + {comment.nick} + + ) + } +
    +
    + + {pending &&

    您的评论正在等待审核中...

    } +
    + +
    +
    + { + comment.children && + (depth === 1 ? ( + + ) : ( + <> + {comment.children.map((childComment) => ( + + ))} + + )) + } +
  • diff --git a/src/components/comment/Comments.astro b/src/components/comment/Comments.astro new file mode 100644 index 0000000..df8fbad --- /dev/null +++ b/src/components/comment/Comments.astro @@ -0,0 +1,80 @@ +--- +import { getConfig, loadComments } from '@/components/comment/artalk'; +import Comment from '@/components/comment/Comment.astro'; + +// This is a component which loads comments and renders it on server-side. +interface Props { + commentKey: string; + title: string; +} + +const { commentKey, title } = Astro.props; +const config = await getConfig(); +const comments = config != null ? await loadComments(commentKey, 0, config) : null; +--- + +
    + { + comments != null && config != null ? ( + <> +
    + 评论 ({comments.count}) +
    +
    +
    +
    + 头像 +
    +
    +
    +