feat: use action for better configuring the backend API. (#50)
* chore: bump the dependencies. * feat: define the actions for all the rest requests. * feat: use action to perform requests.
This commit is contained in:
parent
3dfd0ab4d0
commit
6a4d7243c9
@ -18,6 +18,7 @@ export default defineConfig({
|
||||
service: !options.isProd() ? { entrypoint: './plugins/resize', config: {} } : undefined,
|
||||
},
|
||||
experimental: {
|
||||
actions: true,
|
||||
env: {
|
||||
schema: {
|
||||
// Postgres Database
|
||||
|
40
package-lock.json
generated
40
package-lock.json
generated
@ -29,7 +29,7 @@
|
||||
"@napi-rs/canvas": "^0.1.53",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.14.7",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/qrcode-svg": "^1.1.4",
|
||||
"@types/unist": "^3.0.2",
|
||||
@ -3581,9 +3581,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.7.0.tgz",
|
||||
"integrity": "sha512-O6j27b7dGmJbR3mjwh/aHH8Ld+GQvA0OQsNO43wKWnqbAae3AYXrhFyScHGX8hXZD6vX2ngjzDFkZY5srtIJbQ==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@smithy/abort-controller": {
|
||||
@ -4309,9 +4309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-waiter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.0.tgz",
|
||||
"integrity": "sha512-5OVcC5ZcmmutY208ADY/l2eB4H4DVXs+hPUo/M1spF4/YEmF9DdLkfwBvohej2dIeVJayKY7hMlD0X8j3F3/Uw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.1.tgz",
|
||||
"integrity": "sha512-xT+Bbpe5sSrC7cCWSElOreDdWzqovR1V+7xrp+fmwGAA+TPYBb78iasaXjO1pa+65sY6JjW5GtGeIoJwCK9B1g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -4457,9 +4457,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.14.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.7.tgz",
|
||||
"integrity": "sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==",
|
||||
"version": "20.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
|
||||
"integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -5740,9 +5740,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.807",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz",
|
||||
"integrity": "sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==",
|
||||
"version": "1.4.810",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz",
|
||||
"integrity": "sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emmet": {
|
||||
@ -9766,12 +9766,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.7.0.tgz",
|
||||
"integrity": "sha512-H5pMn4JA7ayx8H0qOz1k2qANq6mZVCMl1gKLK6kWIrv1s2Ial4EmD4s4jE8QB5Dw03d/oCQUxc24sotuyR5byA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.9.0.tgz",
|
||||
"integrity": "sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "1.7.0"
|
||||
"@shikijs/core": "1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
@ -10179,9 +10179,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-auto-import-cache": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.2.tgz",
|
||||
"integrity": "sha512-+laqe5SFL1vN62FPOOJSUDTZxtgsoOXjneYOXIpx5rQ4UMiN89NAtJLpqLqyebv9fgQ/IMeeTX+mQyRnwvJzvg==",
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.3.tgz",
|
||||
"integrity": "sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -61,7 +61,7 @@
|
||||
"@napi-rs/canvas": "^0.1.53",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.14.7",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/qrcode-svg": "^1.1.4",
|
||||
"@types/unist": "^3.0.2",
|
||||
|
109
src/actions/index.ts
Normal file
109
src/actions/index.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import Comment from '@/components/comment/Comment.astro';
|
||||
import CommentItem from '@/components/comment/CommentItem.astro';
|
||||
import { commentConfig, createComment, loadComments } from '@/components/comment/artalk';
|
||||
import { partialRender } from '@/helpers/container';
|
||||
import { decreaseLikes, increaseLikes, queryLikes } from '@/helpers/db/query';
|
||||
import { pages, posts } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import { z } from 'astro/zod';
|
||||
import { ActionError, defineAction } from 'astro:actions';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const keys = [...posts.map((post) => post.permalink), ...pages.map((page) => page.permalink)];
|
||||
const CommentConnectError = new ActionError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: "couldn't connect to comment server",
|
||||
});
|
||||
|
||||
export const server = {
|
||||
like: defineAction({
|
||||
accept: 'json',
|
||||
input: z
|
||||
.object({
|
||||
key: z.custom<string>((val) => keys.includes(val)),
|
||||
})
|
||||
.and(
|
||||
z
|
||||
.object({
|
||||
action: z.enum(['increase']),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
action: z.enum(['decrease']),
|
||||
token: z.string().min(1),
|
||||
}),
|
||||
),
|
||||
),
|
||||
handler: async (input) => {
|
||||
// Increase the like counts.
|
||||
if (input.action === 'increase') {
|
||||
return await increaseLikes(input.key);
|
||||
}
|
||||
// Decrease the like counts.
|
||||
await decreaseLikes(input.key, input.token);
|
||||
return { likes: await queryLikes(input.key) };
|
||||
},
|
||||
}),
|
||||
avatar: defineAction({
|
||||
accept: 'json',
|
||||
input: z.object({ email: z.string().email() }),
|
||||
handler: async ({ email }) => {
|
||||
const config = await commentConfig();
|
||||
if (config === null) {
|
||||
throw CommentConnectError;
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex');
|
||||
return { avatar: urlJoin(config.frontend_conf.gravatar.mirror, `${hash}?d=mm&s=80`) };
|
||||
},
|
||||
}),
|
||||
comment: defineAction({
|
||||
accept: 'json',
|
||||
input: z.object({
|
||||
page_key: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
link: z.string().optional(),
|
||||
content: z.string().min(1),
|
||||
rid: z.number().optional(),
|
||||
}),
|
||||
handler: async (request) => {
|
||||
const resp = await createComment(request);
|
||||
if ('msg' in resp) {
|
||||
throw new ActionError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: resp.msg,
|
||||
});
|
||||
}
|
||||
|
||||
const config = await commentConfig();
|
||||
const content = await partialRender(CommentItem, {
|
||||
props: { depth: 2, comment: resp, pending: resp.is_pending, config: config },
|
||||
});
|
||||
|
||||
return { content };
|
||||
},
|
||||
}),
|
||||
comments: defineAction({
|
||||
accept: 'json',
|
||||
input: z.object({
|
||||
page_key: z.string(),
|
||||
offset: z.number(),
|
||||
}),
|
||||
handler: async ({ page_key, offset }) => {
|
||||
const config = await commentConfig();
|
||||
if (config === null) {
|
||||
throw CommentConnectError;
|
||||
}
|
||||
|
||||
const comments = await loadComments(page_key, null, Number(offset), config);
|
||||
if (comments === null) {
|
||||
throw CommentConnectError;
|
||||
}
|
||||
|
||||
const content = await partialRender(Comment, { props: { comments: comments, config: config } });
|
||||
|
||||
return { content };
|
||||
},
|
||||
}),
|
||||
};
|
@ -1,6 +1,29 @@
|
||||
import Aplayer from 'aplayer/dist/APlayer.min.js';
|
||||
import { actions, isInputError } from 'astro:actions';
|
||||
import stickySidebar from './sticky-sidebar.js';
|
||||
|
||||
// Error Popup.
|
||||
const handleActionError = (error) => {
|
||||
const errorMsg = isInputError(error)
|
||||
? error.issues.map((issue) => `<p>${issue.message}</p>`).join('\n')
|
||||
: error.message;
|
||||
const errorPopup = `<div class="nice-popup nice-popup-center error nice-popup-error nice-popup-open">
|
||||
<div class="nice-popup-overlay"></div>
|
||||
<div class="nice-popup-body">
|
||||
<div class="nice-popup-close"><span class="svg-white"></span> <span class="svg-dark"></span></div>
|
||||
<div class="nice-popup-content">
|
||||
<div class="icon"></div>
|
||||
<div class="text-center">
|
||||
<p class="mt-1 mb-2">${errorMsg}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.querySelector('body').insertAdjacentHTML('beforeend', errorPopup);
|
||||
const popup = document.querySelector('.nice-popup-error');
|
||||
popup.querySelector('.nice-popup-close').addEventListener('click', () => popup.remove());
|
||||
};
|
||||
|
||||
// Menu toggle.
|
||||
const menuBody = document.querySelector('.site-aside');
|
||||
document.addEventListener('keydown', (event) => {
|
||||
@ -119,12 +142,12 @@ if (typeof comments !== 'undefined' && comments !== null) {
|
||||
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) => {
|
||||
avatar.src = link;
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
actions.avatar.safe({ email }).then(({ data, error }) => {
|
||||
if (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
avatar.src = data.avatar;
|
||||
});
|
||||
} else {
|
||||
avatar.src = avatar.dataset.src;
|
||||
}
|
||||
@ -135,19 +158,19 @@ if (typeof comments !== 'undefined' && comments !== null) {
|
||||
// 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 === '') {
|
||||
const { data, error } = await actions.comments.safe({ offset: Number(offset), page_key: key });
|
||||
if (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
|
||||
const { content } = data;
|
||||
if (content === '') {
|
||||
// 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);
|
||||
comments.querySelector('.comment-list').insertAdjacentHTML('beforeend', content);
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,30 +208,25 @@ if (typeof comments !== 'undefined' && comments !== null) {
|
||||
event.stopPropagation();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const data = {};
|
||||
const request = {};
|
||||
for (const [key, value] of formData) {
|
||||
data[key] = value;
|
||||
request[key] = value;
|
||||
}
|
||||
request.rid = request.rid === undefined ? 0 : Number(request.rid);
|
||||
|
||||
actions.comment.safe(request).then(({ data, error }) => {
|
||||
if (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
|
||||
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);
|
||||
const { content } = data;
|
||||
if (request.rid !== '0') {
|
||||
replyForm.insertAdjacentHTML('beforebegin', content);
|
||||
} else {
|
||||
const list = comments.querySelector('.comment-list');
|
||||
list.insertAdjacentHTML('afterbegin', resp);
|
||||
list.insertAdjacentHTML('afterbegin', content);
|
||||
}
|
||||
});
|
||||
|
||||
cancelReply();
|
||||
});
|
||||
@ -230,8 +248,8 @@ const scrollIntoView = (elem) => {
|
||||
window.scroll(scrollOptions);
|
||||
};
|
||||
|
||||
// Highlighting the selected comment.
|
||||
const focusComment = () => {
|
||||
// Highlighting the selected comment.
|
||||
if (location.hash.startsWith('#atk-comment-')) {
|
||||
for (const li of document.querySelectorAll('.comment-body')) {
|
||||
li.classList.remove('active');
|
||||
@ -250,46 +268,38 @@ window.addEventListener('load', focusComment);
|
||||
// Add like button for updating likes.
|
||||
const likeButton = document.querySelector('button.post-like');
|
||||
|
||||
const increaseLikes = (count) => {
|
||||
const increaseLikes = (count, permalink) => {
|
||||
count.textContent = Number.parseInt(count.textContent) + 1;
|
||||
fetch('/likes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify({ action: 'increase' }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ likes, token }) => {
|
||||
actions.like.safe({ action: 'increase', key: permalink }).then(({ data, error }) => {
|
||||
if (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
const { likes, token } = data;
|
||||
count.textContent = likes;
|
||||
localStorage.setItem(window.location.href, token);
|
||||
localStorage.setItem(permalink, token);
|
||||
});
|
||||
};
|
||||
|
||||
const decreaseLikes = (count) => {
|
||||
const token = localStorage.getItem(window.location.href);
|
||||
const decreaseLikes = (count, permalink) => {
|
||||
const token = localStorage.getItem(permalink);
|
||||
if (token === null || token === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
count.textContent = Number.parseInt(count.textContent) - 1;
|
||||
fetch('/likes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify({ action: 'decrease', token: token }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ likes }) => {
|
||||
count.textContent = likes;
|
||||
localStorage.removeItem(window.location.href);
|
||||
actions.like.safe({ action: 'decrease', key: permalink, token }).then(({ data, error }) => {
|
||||
if (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
count.textContent = data.likes;
|
||||
localStorage.removeItem(permalink);
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof likeButton !== 'undefined' && likeButton !== null) {
|
||||
const permalink = likeButton.dataset.permalink;
|
||||
|
||||
// Change the like state if it has been liked.
|
||||
const token = localStorage.getItem(window.location.href);
|
||||
const token = localStorage.getItem(permalink);
|
||||
if (token !== null && token !== '') {
|
||||
likeButton.classList.add('current');
|
||||
}
|
||||
@ -307,10 +317,10 @@ if (typeof likeButton !== 'undefined' && likeButton !== null) {
|
||||
// Increase the likes and set liked before submitting.
|
||||
if (likeButton.classList.contains('current')) {
|
||||
likeButton.classList.remove('current');
|
||||
decreaseLikes(count);
|
||||
decreaseLikes(count, permalink);
|
||||
} else {
|
||||
likeButton.classList.add('current');
|
||||
increaseLikes(count);
|
||||
increaseLikes(count, permalink);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2244,10 +2244,6 @@ a:hover .overlay {
|
||||
/*--------------------------------------------------------------
|
||||
error content
|
||||
--------------------------------------------------------------*/
|
||||
.nice-popup-error {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.nice-popup-error .nice-popup-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
import { getConfig, loadComments } from '@/components/comment/artalk';
|
||||
import { commentConfig, loadComments } from '@/components/comment/artalk';
|
||||
import Comment from '@/components/comment/Comment.astro';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
@ -11,7 +11,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const { commentKey, title } = Astro.props;
|
||||
const config = await getConfig();
|
||||
const config = await commentConfig();
|
||||
const comments = config != null ? await loadComments(commentKey, title, 0, config) : null;
|
||||
---
|
||||
|
||||
|
@ -20,7 +20,7 @@ import sanitize from 'ultrahtml/transformers/sanitize';
|
||||
// Access the artalk in internal docker host when it was deployed on zeabur.
|
||||
const server = options.isProd() ? `http://${ARTALK_HOST}:23366` : options.settings.comments.server;
|
||||
|
||||
export const getConfig = async (): Promise<CommentConfig | null> => {
|
||||
export const commentConfig = async (): Promise<CommentConfig | null> => {
|
||||
const data = await fetch(urlJoin(server, '/api/v2/conf'))
|
||||
.then((response) => response.json())
|
||||
.catch((e) => {
|
||||
@ -79,7 +79,11 @@ export const createComment = async (req: CommentReq): Promise<ErrorResp | Commen
|
||||
return (await response.json()) as ErrorResp;
|
||||
}
|
||||
|
||||
return (await response.json()) as CommentResp;
|
||||
// Parse comment content.
|
||||
const commentResp = (await response.json()) as CommentResp;
|
||||
commentResp.content = await parseContent(commentResp.content);
|
||||
|
||||
return commentResp;
|
||||
};
|
||||
|
||||
export const parseComments = async (comments: Comment[]): Promise<CommentItem[]> => {
|
||||
|
@ -1,28 +1,18 @@
|
||||
// The configuration in artalk.
|
||||
export interface CommentConfig {
|
||||
frontend_conf: FrontendConf;
|
||||
}
|
||||
|
||||
export interface FrontendConf {
|
||||
frontend_conf: {
|
||||
flatMode: boolean;
|
||||
gravatar: Gravatar;
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
export interface Gravatar {
|
||||
gravatar: {
|
||||
mirror: string;
|
||||
params: string;
|
||||
}
|
||||
|
||||
export interface Pagination {
|
||||
};
|
||||
pagination: {
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Comments {
|
||||
comments: Comment[];
|
||||
count: number;
|
||||
roots_count: number;
|
||||
}
|
||||
|
||||
// The single comment.
|
||||
export interface Comment {
|
||||
id: number;
|
||||
content: string;
|
||||
@ -33,17 +23,19 @@ export interface Comment {
|
||||
rid: number;
|
||||
}
|
||||
|
||||
export interface PV {
|
||||
page_key: string;
|
||||
page_title: string;
|
||||
site_name: string;
|
||||
}
|
||||
|
||||
// Grouping the comments into parent child structure.
|
||||
export interface CommentItem extends Comment {
|
||||
children?: CommentItem[];
|
||||
}
|
||||
|
||||
// Create comment request
|
||||
// The comment list.
|
||||
export interface Comments {
|
||||
comments: Comment[];
|
||||
count: number;
|
||||
roots_count: number;
|
||||
}
|
||||
|
||||
// Create comment request.
|
||||
export interface CommentReq {
|
||||
page_key: string;
|
||||
name: string;
|
||||
@ -53,11 +45,12 @@ export interface CommentReq {
|
||||
rid?: number;
|
||||
}
|
||||
|
||||
// Create comment response
|
||||
// Create comment response.
|
||||
export interface CommentResp extends Comment {
|
||||
is_pending: boolean;
|
||||
}
|
||||
|
||||
// Error response in creating comment.
|
||||
export interface ErrorResp {
|
||||
msg: string;
|
||||
}
|
||||
|
@ -1,17 +1,21 @@
|
||||
---
|
||||
import { queryLikes } from '@/helpers/db/query';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
|
||||
interface Props {
|
||||
post: Post;
|
||||
permalink: string;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const likes = await queryLikes(post.slug);
|
||||
const { permalink } = Astro.props;
|
||||
const likes = await queryLikes(permalink);
|
||||
---
|
||||
|
||||
<div class="post-action text-center mt-5">
|
||||
<button class="post-like btn btn-secondary btn-lg btn-rounded" title="Do you like me?" type="button">
|
||||
<button
|
||||
class="post-like btn btn-secondary btn-lg btn-rounded"
|
||||
title="Do you like me?"
|
||||
type="button"
|
||||
data-permalink={permalink}
|
||||
>
|
||||
<i class="text-lg iconfont icon-heart-fill me-1"></i>
|
||||
<span class="like-count">{likes}</span>
|
||||
</button>
|
||||
|
@ -1,13 +1,12 @@
|
||||
---
|
||||
import { queryLikesAndViews } from '@/helpers/db/query';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
|
||||
interface Props {
|
||||
post: Post;
|
||||
permalink: string;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const [likes, view] = await queryLikesAndViews(post.slug);
|
||||
const { permalink } = Astro.props;
|
||||
const [likes, view] = await queryLikesAndViews(permalink);
|
||||
---
|
||||
|
||||
<div class="list-like d-inline-block">
|
||||
|
@ -1,13 +1,12 @@
|
||||
---
|
||||
import { queryLikes } from '@/helpers/db/query';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
|
||||
interface Props {
|
||||
post: Post;
|
||||
permalink: string;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const likes = await queryLikes(post.slug);
|
||||
const { permalink } = Astro.props;
|
||||
const likes = await queryLikes(permalink);
|
||||
---
|
||||
|
||||
<div>
|
||||
|
@ -1,38 +0,0 @@
|
||||
---
|
||||
import type { Friend } from '@/helpers/schema';
|
||||
|
||||
interface Props extends Friend {}
|
||||
|
||||
const { website, description, homepage, poster, favicon } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="list-item block">
|
||||
<div class="media media-3x1">
|
||||
<div
|
||||
class="media-content"
|
||||
style={{
|
||||
backgroundImage: `url('${poster}')`,
|
||||
backgroundSize: 'cover',
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div class="list-body">
|
||||
<div class="list-title h6 h-1x">
|
||||
{website}
|
||||
<div
|
||||
class="list-favicon"
|
||||
style={{
|
||||
backgroundImage: `url('${favicon}')`,
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-secondary h-2x mt-1">{description ? description : ' '}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href={homepage} target="_blank" class="list-gogogo"></a>
|
||||
</div>
|
||||
</div>
|
@ -1,28 +0,0 @@
|
||||
---
|
||||
import FriendCard from '@/components/page/friend/FriendCard.astro';
|
||||
import { friends } from '@/helpers/schema';
|
||||
import _ from 'lodash';
|
||||
|
||||
const list = _.shuffle(friends);
|
||||
---
|
||||
|
||||
{
|
||||
list.length > 0 ? (
|
||||
<div class="list-bookmarks px-3 px-md-0">
|
||||
<h2 class="text-muted mb-4 mb-md-3">
|
||||
左邻右舍 <span class="text-primary text-sm mb-4 mb-md-3">排名不分前后</span>
|
||||
</h2>
|
||||
<div class="row g-md-4 list-grouped">
|
||||
{list.map((friend) => (
|
||||
<FriendCard {...friend} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="data-null">
|
||||
<div class="my-auto">
|
||||
<div>还没有友链呢...😭</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
54
src/components/page/friend/Friends.astro
Normal file
54
src/components/page/friend/Friends.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
import { friends } from '@/helpers/schema';
|
||||
import _ from 'lodash';
|
||||
|
||||
const list = _.shuffle(friends);
|
||||
---
|
||||
|
||||
{
|
||||
list.length > 0 ? (
|
||||
<div class="list-bookmarks px-3 px-md-0">
|
||||
<h2 class="text-muted mb-4 mb-md-3">
|
||||
左邻右舍 <span class="text-primary text-sm mb-4 mb-md-3">排名不分前后</span>
|
||||
</h2>
|
||||
<div class="row g-md-4 list-grouped">
|
||||
{list.map((friend) => (
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="list-item block">
|
||||
<div class="media media-3x1">
|
||||
<div
|
||||
class="media-content"
|
||||
style={{
|
||||
backgroundImage: `url('${friend.poster}')`,
|
||||
backgroundSize: 'cover',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div class="list-body">
|
||||
<div class="list-title h6 h-1x">
|
||||
{friend.website}
|
||||
<div
|
||||
class="list-favicon"
|
||||
style={{
|
||||
backgroundImage: `url('${friend.favicon}')`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-secondary h-2x mt-1">{friend.description ? friend.description : ' '}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href={friend.homepage} target="_blank" class="list-gogogo" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="data-null">
|
||||
<div class="my-auto">
|
||||
<div>还没有友链呢...😭</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
---
|
||||
import Image from '@/components/image/Image.astro';
|
||||
import LikeIcon from '@/components/like/LikeIcon.astro';
|
||||
import { formatShowDate } from '@/helpers/formatter';
|
||||
import { getCategory, type Post } from '@/helpers/schema';
|
||||
|
||||
interface Props {
|
||||
post: Post;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const category = getCategory(post.category, undefined);
|
||||
---
|
||||
|
||||
<div class="list-item block">
|
||||
<div class="media media-3x2 col-6 col-md-5">
|
||||
<a href={post.permalink} class="media-content">
|
||||
<Image {...post.cover} alt={post.title} width={600} height={400} />
|
||||
</a>
|
||||
<div class="media-overlay overlay-top">
|
||||
<a class="d-none d-md-inline-block badge badge-md bg-white-overlay" href={category ? category.permalink : ''}>
|
||||
{post.category}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div class="list-body">
|
||||
<a href={post.permalink} class="list-title h5">
|
||||
<div class="h-2x">{post.title}</div>
|
||||
</a>
|
||||
<div class="d-none d-md-block list-desc text-secondary text-md mt-3">
|
||||
<div class="h-2x">{post.summary ?? ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-footer">
|
||||
<div class="d-flex flex-fill align-items-center text-muted text-sm">
|
||||
<div class="flex-fill d-none d-md-block">{formatShowDate(post.date)}</div>
|
||||
<LikeIcon {post} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,9 +1,10 @@
|
||||
---
|
||||
import Image from '@/components/image/Image.astro';
|
||||
import LikeIcon from '@/components/like/LikeIcon.astro';
|
||||
import Pagination from '@/components/page/pagination/Pagination.astro';
|
||||
import { slicePosts } from '@/helpers/formatter';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
import { formatShowDate, slicePosts } from '@/helpers/formatter';
|
||||
import { getCategory, type Post } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
import PostCard from './PostCard.astro';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
@ -20,6 +21,42 @@ const { currentPosts, totalPage } = results;
|
||||
---
|
||||
|
||||
<div class="content-wrapper content-wrapper col-12 col-xl-9">
|
||||
<div class="list-grid">{currentPosts.map((post) => <PostCard post={post} />)}</div>
|
||||
<div class="list-grid">
|
||||
{
|
||||
currentPosts.map((post) => (
|
||||
<div class="list-item block">
|
||||
<div class="media media-3x2 col-6 col-md-5">
|
||||
<a href={post.permalink} class="media-content">
|
||||
<Image {...post.cover} alt={post.title} width={600} height={400} />
|
||||
</a>
|
||||
<div class="media-overlay overlay-top">
|
||||
<a
|
||||
class="d-none d-md-inline-block badge badge-md bg-white-overlay"
|
||||
href={getCategory(post.category, undefined)?.permalink || ''}
|
||||
>
|
||||
{post.category}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div class="list-body">
|
||||
<a href={post.permalink} class="list-title h5">
|
||||
<div class="h-2x">{post.title}</div>
|
||||
</a>
|
||||
<div class="d-none d-md-block list-desc text-secondary text-md mt-3">
|
||||
<div class="h-2x">{post.summary ?? ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-footer">
|
||||
<div class="d-flex flex-fill align-items-center text-muted text-sm">
|
||||
<div class="flex-fill d-none d-md-block">{formatShowDate(post.date)}</div>
|
||||
<LikeIcon permalink={post.permalink} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Pagination current={pageNum} total={totalPage} rootPath={'/'} />
|
||||
</div>
|
||||
|
@ -26,7 +26,7 @@ const { post, first } = Astro.props;
|
||||
<div class="list-meta font-number d-flex flex-fill text-muted text-sm">
|
||||
<span class="d-inline-block">{formatShowDate(post.date)}</span>
|
||||
<div class="flex-fill"></div>
|
||||
<LikeIconSmall {post} />
|
||||
<LikeIconSmall permalink={post.permalink} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -558,9 +558,5 @@
|
||||
slug: algorithm
|
||||
- name: 面试
|
||||
slug: interview
|
||||
- name: 阿里
|
||||
slug: alibaba
|
||||
- name: 腾讯
|
||||
slug: tencent
|
||||
- name: 读书
|
||||
slug: reading
|
||||
|
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
/// <reference path="../.astro/actions.d.ts" />
|
||||
/// <reference path="../.astro/env.d.ts" />
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
@ -44,8 +44,6 @@ 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)
|
||||
@ -55,8 +53,10 @@ export const increaseViews = async (pageKey: string) => {
|
||||
.where(eq(atk_pages.key, sql`${pageKey}`));
|
||||
};
|
||||
|
||||
export const increaseLikes = async (slug: string): Promise<{ likes: number; token: string }> => {
|
||||
const pageKey = generateKey(slug);
|
||||
const generatePageKey = (permalink: string): string => urlJoin(options.website, permalink, '/');
|
||||
|
||||
export const increaseLikes = async (permalink: string): Promise<{ likes: number; token: string }> => {
|
||||
const pageKey = generatePageKey(permalink);
|
||||
const token = makeToken(250);
|
||||
// Save the token
|
||||
await db.insert(atk_likes).values({
|
||||
@ -74,11 +74,11 @@ export const increaseLikes = async (slug: string): Promise<{ likes: number; toke
|
||||
})
|
||||
.where(eq(atk_pages.key, sql`${pageKey}`));
|
||||
|
||||
return { likes: await queryLikes(slug), token: token };
|
||||
return { likes: await queryLikes(permalink), token: token };
|
||||
};
|
||||
|
||||
export const decreaseLikes = async (slug: string, token: string) => {
|
||||
const pageKey = generateKey(slug);
|
||||
export const decreaseLikes = async (permalink: string, token: string) => {
|
||||
const pageKey = generatePageKey(permalink);
|
||||
const results = await db
|
||||
.select({ id: atk_likes.id })
|
||||
.from(atk_likes)
|
||||
@ -108,8 +108,8 @@ export const decreaseLikes = async (slug: string, token: string) => {
|
||||
.where(eq(atk_pages.key, sql`${pageKey}`));
|
||||
};
|
||||
|
||||
export const queryLikes = async (slug: string): Promise<number> => {
|
||||
const pageKey = generateKey(slug);
|
||||
export const queryLikes = async (permalink: string): Promise<number> => {
|
||||
const pageKey = generatePageKey(permalink);
|
||||
const results = await db
|
||||
.select({ like: atk_pages.vote_up })
|
||||
.from(atk_pages)
|
||||
@ -119,8 +119,8 @@ export const queryLikes = async (slug: string): Promise<number> => {
|
||||
return results.length > 0 ? results[0].like ?? 0 : 0;
|
||||
};
|
||||
|
||||
export const queryLikesAndViews = async (slug: string): Promise<[number, number]> => {
|
||||
const pageKey = generateKey(slug);
|
||||
export const queryLikesAndViews = async (permalink: string): Promise<[number, number]> => {
|
||||
const pageKey = generatePageKey(permalink);
|
||||
const results = await db
|
||||
.select({ like: atk_pages.vote_up, view: atk_pages.pv })
|
||||
.from(atk_pages)
|
||||
|
@ -1,8 +1,9 @@
|
||||
---
|
||||
import Comments from '@/components/comment/Comments.astro';
|
||||
import Image from '@/components/image/Image.astro';
|
||||
import LikeButton from '@/components/like/LikeButton.astro';
|
||||
import PageMeta from '@/components/meta/PageMeta.astro';
|
||||
import FriendLinks from '@/components/page/friend/FriendLinks.astro';
|
||||
import Friends from '@/components/page/friend/Friends.astro';
|
||||
import MusicPlayer from '@/components/player/MusicPlayer.astro';
|
||||
import type { Page } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
@ -29,7 +30,8 @@ const { Content } = await page.render();
|
||||
<Content components={{ MusicPlayer: MusicPlayer, Image: Image }} />
|
||||
</div>
|
||||
</div>
|
||||
{page.friend && <FriendLinks />}
|
||||
{page.friend && <Friends />}
|
||||
<LikeButton permalink={page.permalink} />
|
||||
{page.comments && <Comments commentKey={urlJoin(options.website, page.permalink, '/')} title={page.title} />}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
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';
|
||||
import LikeShare from '@/components/like/LikeShare.astro';
|
||||
import PostMeta from '@/components/meta/PostMeta.astro';
|
||||
import MusicPlayer from '@/components/player/MusicPlayer.astro';
|
||||
import Sidebar from '@/components/sidebar/Sidebar.astro';
|
||||
@ -48,8 +48,8 @@ const { Content } = await post.render();
|
||||
<div class="nav-links"></div>
|
||||
</nav>
|
||||
</div>
|
||||
<LikeButton {post} />
|
||||
<Share {post} />
|
||||
<LikeButton permalink={post.permalink} />
|
||||
<LikeShare {post} />
|
||||
{
|
||||
post.comments && (
|
||||
<Comments commentKey={urlJoin(options.website, post.permalink, '/')} title={post.title} />
|
||||
|
@ -1,20 +0,0 @@
|
||||
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`));
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
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, null, Number(offset), config);
|
||||
if (comments === null) {
|
||||
return new Response('');
|
||||
}
|
||||
|
||||
const html = await partialRender(Comment, { props: { comments: comments, config: config } });
|
||||
return new Response(html);
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
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,30 +0,0 @@
|
||||
import { decreaseLikes, increaseLikes, queryLikes } from '@/helpers/db/query';
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const POST: APIRoute = async ({ params, request }) => {
|
||||
const { slug } = params;
|
||||
const resp = await request.json();
|
||||
|
||||
// Increase.
|
||||
if (resp.action === 'increase') {
|
||||
if (typeof slug === 'undefined') {
|
||||
return Response.json({ likes: 0, token: '' });
|
||||
}
|
||||
|
||||
const { likes, token } = await increaseLikes(slug);
|
||||
return Response.json({ likes: likes, token: token });
|
||||
}
|
||||
|
||||
// Decrease.
|
||||
if (resp.action === 'decrease' && resp.token !== '') {
|
||||
if (typeof slug === 'undefined') {
|
||||
return Response.json({ likes: 0 });
|
||||
}
|
||||
|
||||
await decreaseLikes(slug, resp.token);
|
||||
const likes = await queryLikes(slug);
|
||||
return Response.json({ likes: likes });
|
||||
}
|
||||
|
||||
return Response.json({ likes: 0 });
|
||||
};
|
Loading…
Reference in New Issue
Block a user