feat: use new comment interface, the artalk now serve as the backend. (#42)
This commit is contained in:
parent
2165ea262c
commit
3d0036b242
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -28,6 +28,7 @@
|
||||
"cimage",
|
||||
"ckenh",
|
||||
"cnip",
|
||||
"commentform",
|
||||
"csvg",
|
||||
"ctan",
|
||||
"cynosura",
|
||||
|
@ -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
|
||||
|
@ -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' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
76
package-lock.json
generated
76
package-lock.json
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 566 KiB After Width: | Height: | Size: 348 KiB |
BIN
public/images/default-avatar.png
Normal file
BIN
public/images/default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -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 '<li>评论失败<li>';
|
||||
});
|
||||
|
||||
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',
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
---
|
||||
|
||||
<div
|
||||
id="comments"
|
||||
class="comments py-5"
|
||||
data-key={commentKey}
|
||||
data-title={title}
|
||||
data-server={options.settings.comments.server}
|
||||
data-site={options.title}
|
||||
>
|
||||
评论正在加载中 ...
|
||||
</div>
|
18
src/components/comment/Comment.astro
Normal file
18
src/components/comment/Comment.astro
Normal file
@ -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) => <CommentItem comment={item} config={config} depth={1} />)}
|
67
src/components/comment/CommentItem.astro
Normal file
67
src/components/comment/CommentItem.astro
Normal file
@ -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;
|
||||
---
|
||||
|
||||
<li id={`atk-comment-${comment.id}`} class={`comment odd alt thread-odd thread-alt depth-${depth}`}>
|
||||
<article id={`div-comment-${comment.id}`} class="comment-body">
|
||||
<div class="comment-avatar flex-avatar">
|
||||
<img
|
||||
alt={comment.nick}
|
||||
src={urlJoin(config.frontend_conf.gravatar.mirror, `${comment.email_encrypted}?s=80&d=mm&r=g`)}
|
||||
class="avatar avatar-40 photo"
|
||||
height="40"
|
||||
width="40"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="comment-inner">
|
||||
<div class="comment-author fw-bold">
|
||||
{
|
||||
comment.link === '' ? (
|
||||
comment.nick
|
||||
) : (
|
||||
<a href={comment.link ?? '#'} rel="nofollow" target="_blank">
|
||||
{comment.nick}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<Fragment set:html={comment.content} />
|
||||
{pending && <p class="text-xs text-danger tip-comment-check">您的评论正在等待审核中...</p>}
|
||||
</div>
|
||||
<div class="comment-footer text-xs text-muted">
|
||||
<time class="me-2">{formatLocalDate(comment.date)}</time>
|
||||
<span class="text-secondary comment-reply-link" data-rid={comment.id}>回复</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{
|
||||
comment.children &&
|
||||
(depth === 1 ? (
|
||||
<ul class="children">
|
||||
{comment.children.map((childComment) => (
|
||||
<Astro.self comment={childComment} config={config} depth={depth + 1} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<>
|
||||
{comment.children.map((childComment) => (
|
||||
<Astro.self comment={childComment} config={config} depth={depth + 1} />
|
||||
))}
|
||||
</>
|
||||
))
|
||||
}
|
||||
</li>
|
80
src/components/comment/Comments.astro
Normal file
80
src/components/comment/Comments.astro
Normal file
@ -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;
|
||||
---
|
||||
|
||||
<div id="comments" class="comments py-5">
|
||||
{
|
||||
comments != null && config != null ? (
|
||||
<>
|
||||
<div class="h5 mb-4 comment-total-count">
|
||||
评论 <small class="font-theme text-sm">({comments.count})</small>
|
||||
</div>
|
||||
<div id="respond" class="comment-respond mb-3 mb-md-4">
|
||||
<form method="post" action="/comments/new" id="commentForm" class="comment-form">
|
||||
<div class="comment-from-avatar flex-avatar">
|
||||
<img
|
||||
alt="头像"
|
||||
src="/images/default-avatar.png"
|
||||
class="avatar avatar-40 photo avatar-default"
|
||||
height="40"
|
||||
width="40"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div class="comment-from-input flex-fill">
|
||||
<div class="comment-form-text mb-3">
|
||||
<textarea id="content" name="content" class="form-control" rows="3" required />
|
||||
</div>
|
||||
<div class="comment-form-info row g-2 g-md-3 mb-3">
|
||||
<div class="col">
|
||||
<input class="form-control" placeholder="昵称" name="name" type="text" required="required" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<input class="form-control" name="email" placeholder="邮箱" type="email" required="required" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input hidden name="page_key" type="text" value={commentKey} />
|
||||
<input hidden name="rid" type="text" value="0" />
|
||||
<input class="form-control" placeholder="网址" name="link" type="url" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-submit text-end">
|
||||
<input type="button" id="cancel-comment-reply-link" class="btn btn-light me-1" value="再想想" hidden />
|
||||
<input name="submit" type="submit" id="submit" class="btn btn-primary" value="发表评论" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ul class="comment-list">
|
||||
<Comment comments={comments} config={config} />
|
||||
</ul>
|
||||
{comments.roots_count > config.frontend_conf.pagination.pageSize && (
|
||||
<div class="text-center mt-3 mt-md-4">
|
||||
<button
|
||||
id="comments-next-button"
|
||||
data-key={commentKey}
|
||||
data-size={config.frontend_conf.pagination.pageSize}
|
||||
data-offset={config.frontend_conf.pagination.pageSize}
|
||||
class="btn btn-light"
|
||||
>
|
||||
加载更多
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
'评论加载失败 ❌'
|
||||
)
|
||||
}
|
||||
</div>
|
140
src/components/comment/artalk.ts
Normal file
140
src/components/comment/artalk.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import type {
|
||||
Comment,
|
||||
CommentConfig,
|
||||
CommentItem,
|
||||
CommentReq,
|
||||
CommentResp,
|
||||
Comments,
|
||||
ErrorResp,
|
||||
} from '@/components/comment/types';
|
||||
import { increaseViews } from '@/helpers/db/query';
|
||||
import { options } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import { getSecret } from 'astro:env/server';
|
||||
import _ from 'lodash';
|
||||
import { marked } from 'marked';
|
||||
import * as querystring from 'node:querystring';
|
||||
import { transform } from 'ultrahtml';
|
||||
import sanitize from 'ultrahtml/transformers/sanitize';
|
||||
|
||||
// Access the artalk in internal docker host when it was deployed on zeabur.
|
||||
const server = import.meta.env.PROD ? `http://${getSecret('ARTALK_HOST')}:23366` : options.settings.comments.server;
|
||||
|
||||
export const getConfig = async (): Promise<CommentConfig | null> => {
|
||||
const data = await fetch(urlJoin(server, '/api/v2/conf'))
|
||||
.then((response) => response.json())
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return null;
|
||||
});
|
||||
return data != null ? (data as CommentConfig) : data;
|
||||
};
|
||||
|
||||
export const loadComments = async (key: string, offset: number, config: CommentConfig): Promise<Comments | null> => {
|
||||
const query = querystring.stringify({
|
||||
limit: config.frontend_conf.pagination.pageSize,
|
||||
offset: offset,
|
||||
flat_mode: false,
|
||||
page_key: key,
|
||||
site_name: options.title,
|
||||
});
|
||||
const data = await fetch(urlJoin(server, `/api/v2/comments?${query}`))
|
||||
.then((response) => response.json())
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return null;
|
||||
});
|
||||
|
||||
// Increase the PV.
|
||||
await increaseViews(key);
|
||||
|
||||
return data != null ? (data as Comments) : data;
|
||||
};
|
||||
|
||||
export const createComment = async (req: CommentReq): Promise<ErrorResp | CommentResp> => {
|
||||
const response = await fetch(urlJoin(server, '/api/v2/comments'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify({ ...req, site_name: options.title, rid: req.rid ? Number(req.rid) : 0 }),
|
||||
}).catch((e) => {
|
||||
console.log(e);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (response === null) {
|
||||
return { msg: 'failed to create comment' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return (await response.json()) as ErrorResp;
|
||||
}
|
||||
|
||||
return (await response.json()) as CommentResp;
|
||||
};
|
||||
|
||||
export const parseComments = async (comments: Comment[]): Promise<CommentItem[]> => {
|
||||
const parsedComments = await Promise.all(
|
||||
comments.map(async (comment) => ({ ...comment, content: await parseContent(comment.content) })),
|
||||
);
|
||||
const childComments = _.groupBy(
|
||||
parsedComments.filter((comment) => !rootCommentFilter(comment)),
|
||||
(c) => c.rid,
|
||||
);
|
||||
|
||||
return parsedComments.filter(rootCommentFilter).map((comment) => commentItems(comment, childComments));
|
||||
};
|
||||
|
||||
const rootCommentFilter = (comment: Comment): boolean =>
|
||||
comment.rid === 0 || comment.rid === null || typeof comment.rid === 'undefined';
|
||||
|
||||
const commentItems = (comment: Comment, childComments: _.Dictionary<Comment[]>): CommentItem => {
|
||||
const children = childComments[`${comment.id}`];
|
||||
if (typeof children === 'undefined') {
|
||||
return comment;
|
||||
}
|
||||
|
||||
return { ...comment, children: children.map((child) => commentItems(child, childComments)) };
|
||||
};
|
||||
|
||||
const parseContent = async (content: string): Promise<string> => {
|
||||
// Support paragraph in blank line.
|
||||
const escapedContent = content.replace(/\r\n/g, '\n').replace(/(?<!\n)\n(?!\n)/g, '<br />');
|
||||
const parsed = await marked.parse(escapedContent);
|
||||
// Avoid the XSS attack.
|
||||
return transform(parsed, [
|
||||
sanitize({
|
||||
allowElements: [
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'p',
|
||||
'a',
|
||||
'img',
|
||||
'span',
|
||||
'strong',
|
||||
'code',
|
||||
'pre',
|
||||
'blockquote',
|
||||
'del',
|
||||
'i',
|
||||
'u',
|
||||
'sup',
|
||||
'sub',
|
||||
'em',
|
||||
'b',
|
||||
'font',
|
||||
'hr',
|
||||
'br',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
],
|
||||
allowComments: false,
|
||||
}),
|
||||
]);
|
||||
};
|
63
src/components/comment/types.ts
Normal file
63
src/components/comment/types.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export interface CommentConfig {
|
||||
frontend_conf: FrontendConf;
|
||||
}
|
||||
|
||||
export interface FrontendConf {
|
||||
flatMode: boolean;
|
||||
gravatar: Gravatar;
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
export interface Gravatar {
|
||||
mirror: string;
|
||||
params: string;
|
||||
}
|
||||
|
||||
export interface Pagination {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface Comments {
|
||||
comments: Comment[];
|
||||
count: number;
|
||||
roots_count: number;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: number;
|
||||
content: string;
|
||||
nick: string;
|
||||
email_encrypted: string;
|
||||
link: string;
|
||||
date: string;
|
||||
rid: number;
|
||||
}
|
||||
|
||||
export interface PV {
|
||||
page_key: string;
|
||||
page_title: string;
|
||||
site_name: string;
|
||||
}
|
||||
|
||||
export interface CommentItem extends Comment {
|
||||
children?: CommentItem[];
|
||||
}
|
||||
|
||||
// Create comment request
|
||||
export interface CommentReq {
|
||||
page_key: string;
|
||||
name: string;
|
||||
email: string;
|
||||
link?: string;
|
||||
content: string;
|
||||
rid?: number;
|
||||
}
|
||||
|
||||
// Create comment response
|
||||
export interface CommentResp extends Comment {
|
||||
is_pending: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorResp {
|
||||
msg: string;
|
||||
}
|
@ -11,10 +11,17 @@ interface Props {
|
||||
const { post } = Astro.props;
|
||||
const postURL = urlJoin(options.website, post.permalink);
|
||||
const qq = querystring.stringify({
|
||||
query: `url=${postURL}&pics=${urlJoin(options.website, post.cover.src)}&summary=${post.summary}`,
|
||||
url: postURL,
|
||||
pics: urlJoin(options.website, post.cover.src),
|
||||
summary: post.summary,
|
||||
});
|
||||
const weibo = querystring.stringify({
|
||||
query: `url=${postURL}&type=button&language=zh_cn&pic=${urlJoin(options.website, post.cover.src)}&searchPic=true&title=【${post.title}】${post.summary}`,
|
||||
url: postURL,
|
||||
type: 'button',
|
||||
language: 'zh_cn',
|
||||
pic: urlJoin(options.website, post.cover.src),
|
||||
searchPic: true,
|
||||
title: `【${post.title}】${post.summary}`,
|
||||
});
|
||||
---
|
||||
|
||||
|
@ -3,7 +3,11 @@ import Image from '@/components/image/Image.astro';
|
||||
import UnstyledMusicPlayer from '@/components/player/UnstyledMusicPlayer.astro';
|
||||
import { posts } from '@/helpers/schema';
|
||||
|
||||
const { slug } = Astro.params;
|
||||
interface Props {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
const post = posts.find((post) => post.slug === slug);
|
||||
if (!post) {
|
||||
return Astro.redirect('/404');
|
||||
|
@ -42,7 +42,7 @@ settings:
|
||||
icpNo: 皖ICP备2021002315号-2
|
||||
locale: zh-CN
|
||||
timeZone: Asia/Shanghai
|
||||
timeFormat: yyyy年MM月dd日
|
||||
timeFormat: yyyy-MM-dd
|
||||
twitter: amehochan
|
||||
post:
|
||||
# asc or desc
|
||||
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
title: 使用 Java 进行面向对象编程
|
||||
slug: data-oriented-programming-in-java
|
||||
date: 2024-05-29 22:42:12
|
||||
updated: 2024-05-29 23:15:12
|
||||
tags:
|
||||
- 读书
|
||||
category: 杂谈
|
||||
summary: 藏书如山积,读书如水流。山形有限度,水流无时休。
|
||||
cover: /images/2024/05/2024052912190600.jpg
|
||||
published: false
|
||||
---
|
35
src/helpers/container.ts
Normal file
35
src/helpers/container.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { getContainerRenderer } from '@astrojs/mdx';
|
||||
import type { AstroRenderer, SSRLoadedRenderer } from 'astro';
|
||||
import { experimental_AstroContainer as AstroContainer, type ContainerRenderOptions } from 'astro/container';
|
||||
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
||||
|
||||
// FIXME: This is a monkey patch which should be removed after bumping the astro to 4.10.3
|
||||
export async function loadRenderers(renderers: AstroRenderer[]) {
|
||||
const loadedRenderers = await Promise.all(
|
||||
renderers.map(async (renderer) => {
|
||||
const mod = await import(/* @vite-ignore */ renderer.serverEntrypoint);
|
||||
if (typeof mod.default !== 'undefined') {
|
||||
return {
|
||||
...renderer,
|
||||
ssr: mod.default,
|
||||
} as SSRLoadedRenderer;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
);
|
||||
|
||||
return loadedRenderers.filter((r): r is SSRLoadedRenderer => Boolean(r));
|
||||
}
|
||||
|
||||
const renderers = await loadRenderers([getContainerRenderer()]);
|
||||
const container = await AstroContainer.create({ renderers: renderers });
|
||||
|
||||
// We only want to make sure the container instance is singleton.
|
||||
export const partialRender = async (
|
||||
component: AstroComponentFactory,
|
||||
options?: ContainerRenderOptions,
|
||||
): Promise<string> => {
|
||||
const html = await container.renderToString(component, options);
|
||||
// Remove this doctype by default.
|
||||
return html.startsWith('<!DOCTYPE html>') ? html.slice(15) : html;
|
||||
};
|
@ -43,6 +43,15 @@ export const latestComments = async (): Promise<Comment[]> => {
|
||||
|
||||
const generateKey = (slug: string): string => urlJoin(options.website, '/posts', slug, '/');
|
||||
|
||||
export const increaseViews = async (pageKey: string) => {
|
||||
await db
|
||||
.update(atk_pages)
|
||||
.set({
|
||||
pv: sql`${atk_pages.pv} + 1`,
|
||||
})
|
||||
.where(eq(atk_pages.key, sql`${pageKey}`));
|
||||
};
|
||||
|
||||
export const increaseLikes = async (slug: string): Promise<{ likes: number; token: string }> => {
|
||||
const pageKey = generateKey(slug);
|
||||
const token = makeToken(250);
|
||||
|
@ -58,3 +58,11 @@ export const formatShowDate = (date: Date) => {
|
||||
.setLocale(options.settings.locale)
|
||||
.toFormat(options.settings.timeFormat);
|
||||
};
|
||||
|
||||
export const formatLocalDate = (source: string) => {
|
||||
const date = new Date(source);
|
||||
return DateTime.fromJSDate(date)
|
||||
.setZone(options.settings.timeZone)
|
||||
.setLocale(options.settings.locale)
|
||||
.toFormat(options.settings.timeFormat);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
import ArtalkComment from '@/components/comment/Artalk.astro';
|
||||
import Comments from '@/components/comment/Comments.astro';
|
||||
import Image from '@/components/image/Image.astro';
|
||||
import PageMeta from '@/components/meta/PageMeta.astro';
|
||||
import FriendLinks from '@/components/page/friend/FriendLinks.astro';
|
||||
@ -29,11 +29,7 @@ const { Content } = await page.render();
|
||||
</div>
|
||||
</div>
|
||||
{page.friend && <FriendLinks />}
|
||||
{
|
||||
page.comments && (
|
||||
<ArtalkComment commentKey={urlJoin(options.website, page.permalink, '/')} title={page.title} />
|
||||
)
|
||||
}
|
||||
{page.comments && <Comments commentKey={urlJoin(options.website, page.permalink, '/')} title={page.title} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
import ArtalkComment from '@/components/comment/Artalk.astro';
|
||||
import Comments from '@/components/comment/Comments.astro';
|
||||
import Image from '@/components/image/Image.astro';
|
||||
import LikeButton from '@/components/like/LikeButton.astro';
|
||||
import Share from '@/components/like/Share.astro';
|
||||
@ -51,7 +51,7 @@ const { Content } = await post.render();
|
||||
<Share {post} />
|
||||
{
|
||||
post.comments && (
|
||||
<ArtalkComment commentKey={urlJoin(options.website, post.permalink, '/')} title={post.title} />
|
||||
<Comments commentKey={urlJoin(options.website, post.permalink, '/')} title={post.title} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
20
src/pages/comments/avatar.ts
Normal file
20
src/pages/comments/avatar.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { getConfig } from '@/components/comment/artalk';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import type { APIRoute } from 'astro';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const GET: APIRoute = async ({ url }) => {
|
||||
const email = url.searchParams.get('email');
|
||||
if (email == null) {
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
if (config === null) {
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
// Decode the email into Gravatar hash.
|
||||
const hash = crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex');
|
||||
return new Response(urlJoin(config.frontend_conf.gravatar.mirror, `${hash}?d=mm&s=80`));
|
||||
};
|
29
src/pages/comments/list.ts
Normal file
29
src/pages/comments/list.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { getConfig, loadComments } from '@/components/comment/artalk';
|
||||
import Comment from '@/components/comment/Comment.astro';
|
||||
import { partialRender } from '@/helpers/container';
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const GET: APIRoute = async ({ url }) => {
|
||||
const key = url.searchParams.get('key');
|
||||
if (key == null) {
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
const offset = url.searchParams.get('offset');
|
||||
if (offset == null) {
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
if (config === null) {
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
const comments = await loadComments(key, Number(offset), config);
|
||||
if (comments === null) {
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
const html = await partialRender(Comment, { props: { comments: comments, config: config } });
|
||||
return new Response(html);
|
||||
};
|
21
src/pages/comments/new.ts
Normal file
21
src/pages/comments/new.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import CommentItem from '@/components/comment/CommentItem.astro';
|
||||
import { createComment, getConfig } from '@/components/comment/artalk';
|
||||
import type { CommentReq } from '@/components/comment/types';
|
||||
import { partialRender } from '@/helpers/container';
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const body = (await request.json()) as CommentReq;
|
||||
const resp = await createComment(body);
|
||||
|
||||
if ('msg' in resp) {
|
||||
return new Response(`<li>${resp.msg}</li>`);
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
const content = await partialRender(CommentItem, {
|
||||
props: { depth: 2, comment: resp, pending: resp.is_pending, config: config },
|
||||
});
|
||||
|
||||
return new Response(content);
|
||||
};
|
@ -1,17 +1,13 @@
|
||||
import PostContent from '@/components/page/post/PostContent.astro';
|
||||
import { partialRender } from '@/helpers/container';
|
||||
import { options, posts, type Post } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import { getContainerRenderer } from '@astrojs/mdx';
|
||||
import rss from '@astrojs/rss';
|
||||
import type { AstroRenderer, SSRLoadedRenderer } from 'astro';
|
||||
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
|
||||
import { ELEMENT_NODE, TEXT_NODE, transform, walk, type TextNode } from 'ultrahtml';
|
||||
import sanitize from 'ultrahtml/transformers/sanitize';
|
||||
|
||||
const cleanupContent = async (html: string) => {
|
||||
const content = html.startsWith('<!DOCTYPE html>') ? html.slice(15) : html;
|
||||
|
||||
return await transform(content, [
|
||||
return await transform(html, [
|
||||
async (node) => {
|
||||
await walk(node, (node) => {
|
||||
if (node.type === ELEMENT_NODE) {
|
||||
@ -56,34 +52,14 @@ const cleanupContent = async (html: string) => {
|
||||
]);
|
||||
};
|
||||
|
||||
// Copy from astro source code. I don't know why this virtual vite module didn't works.
|
||||
export async function loadRenderers(renderers: AstroRenderer[]) {
|
||||
const loadedRenderers = await Promise.all(
|
||||
renderers.map(async (renderer) => {
|
||||
const mod = await import(/* @vite-ignore */ renderer.serverEntrypoint);
|
||||
if (typeof mod.default !== 'undefined') {
|
||||
return {
|
||||
...renderer,
|
||||
ssr: mod.default,
|
||||
} as SSRLoadedRenderer;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
);
|
||||
|
||||
return loadedRenderers.filter((r): r is SSRLoadedRenderer => Boolean(r));
|
||||
}
|
||||
|
||||
const renderPostsContent = async (feedPosts: Post[]): Promise<Map<string, string>> => {
|
||||
const contents = new Map<string, string>();
|
||||
|
||||
if (options.settings.feed.full) {
|
||||
const renderers = await loadRenderers([getContainerRenderer()]);
|
||||
const container = await AstroContainer.create({ renderers: renderers });
|
||||
const promises = feedPosts.map(async (post) => ({
|
||||
key: post.slug,
|
||||
value: await container.renderToString(PostContent, {
|
||||
params: {
|
||||
value: await partialRender(PostContent, {
|
||||
props: {
|
||||
slug: post.slug,
|
||||
},
|
||||
}),
|
||||
|
@ -8,23 +8,23 @@ export const POST: APIRoute = async ({ params, request }) => {
|
||||
// Increase.
|
||||
if (resp.action === 'increase') {
|
||||
if (typeof slug === 'undefined') {
|
||||
return new Response(JSON.stringify({ likes: 0, token: '' }));
|
||||
return Response.json({ likes: 0, token: '' });
|
||||
}
|
||||
|
||||
const { likes, token } = await increaseLikes(slug);
|
||||
return new Response(JSON.stringify({ likes: likes, token: token }));
|
||||
return Response.json({ likes: likes, token: token });
|
||||
}
|
||||
|
||||
// Decrease.
|
||||
if (resp.action === 'decrease' && resp.token !== '') {
|
||||
if (typeof slug === 'undefined') {
|
||||
return new Response(JSON.stringify({ likes: 0 }));
|
||||
return Response.json({ likes: 0 });
|
||||
}
|
||||
|
||||
await decreaseLikes(slug, resp.token);
|
||||
const likes = await queryLikes(slug);
|
||||
return new Response(JSON.stringify({ likes: likes }));
|
||||
return Response.json({ likes: likes });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ likes: 0 }));
|
||||
return Response.json({ likes: 0 });
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user