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:
Yufan Sheng 2024-06-22 22:45:20 +08:00 committed by GitHub
parent f3623ab452
commit cd7863c557
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 368 additions and 371 deletions

View File

@ -18,6 +18,7 @@ export default defineConfig({
service: !options.isProd() ? { entrypoint: './plugins/resize', config: {} } : undefined, service: !options.isProd() ? { entrypoint: './plugins/resize', config: {} } : undefined,
}, },
experimental: { experimental: {
actions: true,
env: { env: {
schema: { schema: {
// Postgres Database // Postgres Database

40
package-lock.json generated
View File

@ -29,7 +29,7 @@
"@napi-rs/canvas": "^0.1.53", "@napi-rs/canvas": "^0.1.53",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.14.7", "@types/node": "^20.14.8",
"@types/pg": "^8.11.6", "@types/pg": "^8.11.6",
"@types/qrcode-svg": "^1.1.4", "@types/qrcode-svg": "^1.1.4",
"@types/unist": "^3.0.2", "@types/unist": "^3.0.2",
@ -3581,9 +3581,9 @@
] ]
}, },
"node_modules/@shikijs/core": { "node_modules/@shikijs/core": {
"version": "1.7.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.9.0.tgz",
"integrity": "sha512-O6j27b7dGmJbR3mjwh/aHH8Ld+GQvA0OQsNO43wKWnqbAae3AYXrhFyScHGX8hXZD6vX2ngjzDFkZY5srtIJbQ==", "integrity": "sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@smithy/abort-controller": { "node_modules/@smithy/abort-controller": {
@ -4309,9 +4309,9 @@
} }
}, },
"node_modules/@smithy/util-waiter": { "node_modules/@smithy/util-waiter": {
"version": "3.1.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.1.tgz",
"integrity": "sha512-5OVcC5ZcmmutY208ADY/l2eB4H4DVXs+hPUo/M1spF4/YEmF9DdLkfwBvohej2dIeVJayKY7hMlD0X8j3F3/Uw==", "integrity": "sha512-xT+Bbpe5sSrC7cCWSElOreDdWzqovR1V+7xrp+fmwGAA+TPYBb78iasaXjO1pa+65sY6JjW5GtGeIoJwCK9B1g==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -4457,9 +4457,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.14.7", "version": "20.14.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.7.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
"integrity": "sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==", "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5740,9 +5740,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.807", "version": "1.4.810",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz",
"integrity": "sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==", "integrity": "sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/emmet": { "node_modules/emmet": {
@ -9766,12 +9766,12 @@
} }
}, },
"node_modules/shiki": { "node_modules/shiki": {
"version": "1.7.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.7.0.tgz", "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.9.0.tgz",
"integrity": "sha512-H5pMn4JA7ayx8H0qOz1k2qANq6mZVCMl1gKLK6kWIrv1s2Ial4EmD4s4jE8QB5Dw03d/oCQUxc24sotuyR5byA==", "integrity": "sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/core": "1.7.0" "@shikijs/core": "1.9.0"
} }
}, },
"node_modules/signal-exit": { "node_modules/signal-exit": {
@ -10179,9 +10179,9 @@
} }
}, },
"node_modules/typescript-auto-import-cache": { "node_modules/typescript-auto-import-cache": {
"version": "0.3.2", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.2.tgz", "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.3.tgz",
"integrity": "sha512-+laqe5SFL1vN62FPOOJSUDTZxtgsoOXjneYOXIpx5rQ4UMiN89NAtJLpqLqyebv9fgQ/IMeeTX+mQyRnwvJzvg==", "integrity": "sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -61,7 +61,7 @@
"@napi-rs/canvas": "^0.1.53", "@napi-rs/canvas": "^0.1.53",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.14.7", "@types/node": "^20.14.8",
"@types/pg": "^8.11.6", "@types/pg": "^8.11.6",
"@types/qrcode-svg": "^1.1.4", "@types/qrcode-svg": "^1.1.4",
"@types/unist": "^3.0.2", "@types/unist": "^3.0.2",

109
src/actions/index.ts Normal file
View 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 };
},
}),
};

View File

@ -1,6 +1,29 @@
import Aplayer from 'aplayer/dist/APlayer.min.js'; import Aplayer from 'aplayer/dist/APlayer.min.js';
import { actions, isInputError } from 'astro:actions';
import stickySidebar from './sticky-sidebar.js'; 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. // Menu toggle.
const menuBody = document.querySelector('.site-aside'); const menuBody = document.querySelector('.site-aside');
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
@ -119,12 +142,12 @@ if (typeof comments !== 'undefined' && comments !== null) {
const email = event.target.value; const email = event.target.value;
if (email !== '' && email.includes('@')) { if (email !== '' && email.includes('@')) {
// Replace the avatar after typing the email. // Replace the avatar after typing the email.
fetch(`/comments/avatar?email=${email}`) actions.avatar.safe({ email }).then(({ data, error }) => {
.then((res) => res.text()) if (error) {
.then((link) => { return handleActionError(error);
avatar.src = link; }
}) avatar.src = data.avatar;
.catch((e) => console.log(e)); });
} else { } else {
avatar.src = avatar.dataset.src; avatar.src = avatar.dataset.src;
} }
@ -135,19 +158,19 @@ if (typeof comments !== 'undefined' && comments !== null) {
// Loading more comments from server. // Loading more comments from server.
if (event.target === comments.querySelector('#comments-next-button')) { if (event.target === comments.querySelector('#comments-next-button')) {
const { size, offset, key } = event.target.dataset; const { size, offset, key } = event.target.dataset;
const html = await fetch(`/comments/list?key=${key}&offset=${offset}`) const { data, error } = await actions.comments.safe({ offset: Number(offset), page_key: key });
.then((res) => res.text()) if (error) {
.catch((e) => { return handleActionError(error);
console.log(e); }
return '';
}); const { content } = data;
if (html === '') { if (content === '') {
// Remove the load more button. // Remove the load more button.
event.target.remove(); event.target.remove();
} else { } else {
// Append the comments into the list. // Append the comments into the list.
event.target.dataset.offset = Number(offset) + Number(size); 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(); event.stopPropagation();
const formData = new FormData(event.target); const formData = new FormData(event.target);
const data = {}; const request = {};
for (const [key, value] of formData) { for (const [key, value] of formData) {
data[key] = value; request[key] = value;
} }
request.rid = request.rid === undefined ? 0 : Number(request.rid);
const resp = await fetch('/comments/new', { actions.comment.safe(request).then(({ data, error }) => {
method: 'POST', if (error) {
headers: { return handleActionError(error);
'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') { const { content } = data;
replyForm.insertAdjacentHTML('beforebegin', resp); if (request.rid !== '0') {
} else { replyForm.insertAdjacentHTML('beforebegin', content);
const list = comments.querySelector('.comment-list'); } else {
list.insertAdjacentHTML('afterbegin', resp); const list = comments.querySelector('.comment-list');
} list.insertAdjacentHTML('afterbegin', content);
}
});
cancelReply(); cancelReply();
}); });
@ -230,8 +248,8 @@ const scrollIntoView = (elem) => {
window.scroll(scrollOptions); window.scroll(scrollOptions);
}; };
// Highlighting the selected comment.
const focusComment = () => { const focusComment = () => {
// Highlighting the selected comment.
if (location.hash.startsWith('#atk-comment-')) { if (location.hash.startsWith('#atk-comment-')) {
for (const li of document.querySelectorAll('.comment-body')) { for (const li of document.querySelectorAll('.comment-body')) {
li.classList.remove('active'); li.classList.remove('active');
@ -250,46 +268,38 @@ window.addEventListener('load', focusComment);
// Add like button for updating likes. // Add like button for updating likes.
const likeButton = document.querySelector('button.post-like'); const likeButton = document.querySelector('button.post-like');
const increaseLikes = (count) => { const increaseLikes = (count, permalink) => {
count.textContent = Number.parseInt(count.textContent) + 1; count.textContent = Number.parseInt(count.textContent) + 1;
fetch('/likes', { actions.like.safe({ action: 'increase', key: permalink }).then(({ data, error }) => {
method: 'POST', if (error) {
headers: { return handleActionError(error);
'Content-type': 'application/json; charset=UTF-8', }
}, const { likes, token } = data;
body: JSON.stringify({ action: 'increase' }), count.textContent = likes;
}) localStorage.setItem(permalink, token);
.then((res) => res.json()) });
.then(({ likes, token }) => {
count.textContent = likes;
localStorage.setItem(window.location.href, token);
});
}; };
const decreaseLikes = (count) => { const decreaseLikes = (count, permalink) => {
const token = localStorage.getItem(window.location.href); const token = localStorage.getItem(permalink);
if (token === null || token === '') { if (token === null || token === '') {
return; return;
} }
count.textContent = Number.parseInt(count.textContent) - 1; count.textContent = Number.parseInt(count.textContent) - 1;
fetch('/likes', { actions.like.safe({ action: 'decrease', key: permalink, token }).then(({ data, error }) => {
method: 'POST', if (error) {
headers: { return handleActionError(error);
'Content-type': 'application/json; charset=UTF-8', }
}, count.textContent = data.likes;
body: JSON.stringify({ action: 'decrease', token: token }), localStorage.removeItem(permalink);
}) });
.then((res) => res.json())
.then(({ likes }) => {
count.textContent = likes;
localStorage.removeItem(window.location.href);
});
}; };
if (typeof likeButton !== 'undefined' && likeButton !== null) { if (typeof likeButton !== 'undefined' && likeButton !== null) {
const permalink = likeButton.dataset.permalink;
// Change the like state if it has been liked. // 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 !== '') { if (token !== null && token !== '') {
likeButton.classList.add('current'); likeButton.classList.add('current');
} }
@ -307,10 +317,10 @@ if (typeof likeButton !== 'undefined' && likeButton !== null) {
// Increase the likes and set liked before submitting. // Increase the likes and set liked before submitting.
if (likeButton.classList.contains('current')) { if (likeButton.classList.contains('current')) {
likeButton.classList.remove('current'); likeButton.classList.remove('current');
decreaseLikes(count); decreaseLikes(count, permalink);
} else { } else {
likeButton.classList.add('current'); likeButton.classList.add('current');
increaseLikes(count); increaseLikes(count, permalink);
} }
}); });
} }

View File

@ -2244,10 +2244,6 @@ a:hover .overlay {
/*-------------------------------------------------------------- /*--------------------------------------------------------------
error content error content
--------------------------------------------------------------*/ --------------------------------------------------------------*/
.nice-popup-error {
align-items: flex-start;
}
.nice-popup-error .nice-popup-content { .nice-popup-error .nice-popup-content {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -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 Comment from '@/components/comment/Comment.astro';
import { urlJoin } from '@/helpers/tools'; import { urlJoin } from '@/helpers/tools';
import options from '@/options'; import options from '@/options';
@ -11,7 +11,7 @@ interface Props {
} }
const { commentKey, title } = Astro.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; const comments = config != null ? await loadComments(commentKey, title, 0, config) : null;
--- ---

View File

@ -20,7 +20,7 @@ import sanitize from 'ultrahtml/transformers/sanitize';
// Access the artalk in internal docker host when it was deployed on zeabur. // 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; 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')) const data = await fetch(urlJoin(server, '/api/v2/conf'))
.then((response) => response.json()) .then((response) => response.json())
.catch((e) => { .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 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[]> => { export const parseComments = async (comments: Comment[]): Promise<CommentItem[]> => {

View File

@ -1,28 +1,18 @@
// The configuration in artalk.
export interface CommentConfig { export interface CommentConfig {
frontend_conf: FrontendConf; frontend_conf: {
} flatMode: boolean;
gravatar: {
export interface FrontendConf { mirror: string;
flatMode: boolean; params: string;
gravatar: Gravatar; };
pagination: Pagination; pagination: {
} pageSize: number;
};
export interface Gravatar { };
mirror: string;
params: string;
}
export interface Pagination {
pageSize: number;
}
export interface Comments {
comments: Comment[];
count: number;
roots_count: number;
} }
// The single comment.
export interface Comment { export interface Comment {
id: number; id: number;
content: string; content: string;
@ -33,17 +23,19 @@ export interface Comment {
rid: number; rid: number;
} }
export interface PV { // Grouping the comments into parent child structure.
page_key: string;
page_title: string;
site_name: string;
}
export interface CommentItem extends Comment { export interface CommentItem extends Comment {
children?: CommentItem[]; children?: CommentItem[];
} }
// Create comment request // The comment list.
export interface Comments {
comments: Comment[];
count: number;
roots_count: number;
}
// Create comment request.
export interface CommentReq { export interface CommentReq {
page_key: string; page_key: string;
name: string; name: string;
@ -53,11 +45,12 @@ export interface CommentReq {
rid?: number; rid?: number;
} }
// Create comment response // Create comment response.
export interface CommentResp extends Comment { export interface CommentResp extends Comment {
is_pending: boolean; is_pending: boolean;
} }
// Error response in creating comment.
export interface ErrorResp { export interface ErrorResp {
msg: string; msg: string;
} }

View File

@ -1,17 +1,21 @@
--- ---
import { queryLikes } from '@/helpers/db/query'; import { queryLikes } from '@/helpers/db/query';
import type { Post } from '@/helpers/schema';
interface Props { interface Props {
post: Post; permalink: string;
} }
const { post } = Astro.props; const { permalink } = Astro.props;
const likes = await queryLikes(post.slug); const likes = await queryLikes(permalink);
--- ---
<div class="post-action text-center mt-5"> <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> <i class="text-lg iconfont icon-heart-fill me-1"></i>
<span class="like-count">{likes}</span> <span class="like-count">{likes}</span>
</button> </button>

View File

@ -1,13 +1,12 @@
--- ---
import { queryLikesAndViews } from '@/helpers/db/query'; import { queryLikesAndViews } from '@/helpers/db/query';
import type { Post } from '@/helpers/schema';
interface Props { interface Props {
post: Post; permalink: string;
} }
const { post } = Astro.props; const { permalink } = Astro.props;
const [likes, view] = await queryLikesAndViews(post.slug); const [likes, view] = await queryLikesAndViews(permalink);
--- ---
<div class="list-like d-inline-block"> <div class="list-like d-inline-block">

View File

@ -1,13 +1,12 @@
--- ---
import { queryLikes } from '@/helpers/db/query'; import { queryLikes } from '@/helpers/db/query';
import type { Post } from '@/helpers/schema';
interface Props { interface Props {
post: Post; permalink: string;
} }
const { post } = Astro.props; const { permalink } = Astro.props;
const likes = await queryLikes(post.slug); const likes = await queryLikes(permalink);
--- ---
<div> <div>

View File

@ -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>

View File

@ -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>
)
}

View 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>
)
}

View File

@ -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>

View File

@ -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 Pagination from '@/components/page/pagination/Pagination.astro';
import { slicePosts } from '@/helpers/formatter'; import { formatShowDate, slicePosts } from '@/helpers/formatter';
import type { Post } from '@/helpers/schema'; import { getCategory, type Post } from '@/helpers/schema';
import options from '@/options'; import options from '@/options';
import PostCard from './PostCard.astro';
interface Props { interface Props {
posts: Post[]; posts: Post[];
@ -20,6 +21,42 @@ const { currentPosts, totalPage } = results;
--- ---
<div class="content-wrapper content-wrapper col-12 col-xl-9"> <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={'/'} /> <Pagination current={pageNum} total={totalPage} rootPath={'/'} />
</div> </div>

View File

@ -26,7 +26,7 @@ const { post, first } = Astro.props;
<div class="list-meta font-number d-flex flex-fill text-muted text-sm"> <div class="list-meta font-number d-flex flex-fill text-muted text-sm">
<span class="d-inline-block">{formatShowDate(post.date)}</span> <span class="d-inline-block">{formatShowDate(post.date)}</span>
<div class="flex-fill"></div> <div class="flex-fill"></div>
<LikeIconSmall {post} /> <LikeIconSmall permalink={post.permalink} />
</div> </div>
</a> </a>
</div> </div>

View File

@ -558,9 +558,5 @@
slug: algorithm slug: algorithm
- name: 面试 - name: 面试
slug: interview slug: interview
- name: 阿里
slug: alibaba
- name: 腾讯
slug: tencent
- name: 读书 - name: 读书
slug: reading slug: reading

1
src/env.d.ts vendored
View File

@ -1,3 +1,4 @@
/// <reference path="../.astro/actions.d.ts" />
/// <reference path="../.astro/env.d.ts" /> /// <reference path="../.astro/env.d.ts" />
/// <reference path="../.astro/types.d.ts" /> /// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />

View File

@ -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) => { export const increaseViews = async (pageKey: string) => {
await db await db
.update(atk_pages) .update(atk_pages)
@ -55,8 +53,10 @@ export const increaseViews = async (pageKey: string) => {
.where(eq(atk_pages.key, sql`${pageKey}`)); .where(eq(atk_pages.key, sql`${pageKey}`));
}; };
export const increaseLikes = async (slug: string): Promise<{ likes: number; token: string }> => { const generatePageKey = (permalink: string): string => urlJoin(options.website, permalink, '/');
const pageKey = generateKey(slug);
export const increaseLikes = async (permalink: string): Promise<{ likes: number; token: string }> => {
const pageKey = generatePageKey(permalink);
const token = makeToken(250); const token = makeToken(250);
// Save the token // Save the token
await db.insert(atk_likes).values({ 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}`)); .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) => { export const decreaseLikes = async (permalink: string, token: string) => {
const pageKey = generateKey(slug); const pageKey = generatePageKey(permalink);
const results = await db const results = await db
.select({ id: atk_likes.id }) .select({ id: atk_likes.id })
.from(atk_likes) .from(atk_likes)
@ -108,8 +108,8 @@ export const decreaseLikes = async (slug: string, token: string) => {
.where(eq(atk_pages.key, sql`${pageKey}`)); .where(eq(atk_pages.key, sql`${pageKey}`));
}; };
export const queryLikes = async (slug: string): Promise<number> => { export const queryLikes = async (permalink: string): Promise<number> => {
const pageKey = generateKey(slug); const pageKey = generatePageKey(permalink);
const results = await db const results = await db
.select({ like: atk_pages.vote_up }) .select({ like: atk_pages.vote_up })
.from(atk_pages) .from(atk_pages)
@ -119,8 +119,8 @@ export const queryLikes = async (slug: string): Promise<number> => {
return results.length > 0 ? results[0].like ?? 0 : 0; return results.length > 0 ? results[0].like ?? 0 : 0;
}; };
export const queryLikesAndViews = async (slug: string): Promise<[number, number]> => { export const queryLikesAndViews = async (permalink: string): Promise<[number, number]> => {
const pageKey = generateKey(slug); const pageKey = generatePageKey(permalink);
const results = await db const results = await db
.select({ like: atk_pages.vote_up, view: atk_pages.pv }) .select({ like: atk_pages.vote_up, view: atk_pages.pv })
.from(atk_pages) .from(atk_pages)

View File

@ -1,8 +1,9 @@
--- ---
import Comments from '@/components/comment/Comments.astro'; import Comments from '@/components/comment/Comments.astro';
import Image from '@/components/image/Image.astro'; import Image from '@/components/image/Image.astro';
import LikeButton from '@/components/like/LikeButton.astro';
import PageMeta from '@/components/meta/PageMeta.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 MusicPlayer from '@/components/player/MusicPlayer.astro';
import type { Page } from '@/helpers/schema'; import type { Page } from '@/helpers/schema';
import { urlJoin } from '@/helpers/tools'; import { urlJoin } from '@/helpers/tools';
@ -29,7 +30,8 @@ const { Content } = await page.render();
<Content components={{ MusicPlayer: MusicPlayer, Image: Image }} /> <Content components={{ MusicPlayer: MusicPlayer, Image: Image }} />
</div> </div>
</div> </div>
{page.friend && <FriendLinks />} {page.friend && <Friends />}
<LikeButton permalink={page.permalink} />
{page.comments && <Comments commentKey={urlJoin(options.website, page.permalink, '/')} title={page.title} />} {page.comments && <Comments commentKey={urlJoin(options.website, page.permalink, '/')} title={page.title} />}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
import Comments from '@/components/comment/Comments.astro'; import Comments from '@/components/comment/Comments.astro';
import Image from '@/components/image/Image.astro'; import Image from '@/components/image/Image.astro';
import LikeButton from '@/components/like/LikeButton.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 PostMeta from '@/components/meta/PostMeta.astro';
import MusicPlayer from '@/components/player/MusicPlayer.astro'; import MusicPlayer from '@/components/player/MusicPlayer.astro';
import Sidebar from '@/components/sidebar/Sidebar.astro'; import Sidebar from '@/components/sidebar/Sidebar.astro';
@ -48,8 +48,8 @@ const { Content } = await post.render();
<div class="nav-links"></div> <div class="nav-links"></div>
</nav> </nav>
</div> </div>
<LikeButton {post} /> <LikeButton permalink={post.permalink} />
<Share {post} /> <LikeShare {post} />
{ {
post.comments && ( post.comments && (
<Comments commentKey={urlJoin(options.website, post.permalink, '/')} title={post.title} /> <Comments commentKey={urlJoin(options.website, post.permalink, '/')} title={post.title} />

View File

@ -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`));
};

View File

@ -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);
};

View File

@ -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);
};

View File

@ -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 });
};