sodesu-comment
Version:
Comment system with SolidJS
214 lines (200 loc) • 6.42 kB
text/typescript
import type { WalineCommentData } from '@waline/api';
import type { ReactiveComment } from './commentListState';
import { addComment, updateComment } from '@waline/api';
import { createEffect, createMemo, createResource, createRoot, createSignal } from 'solid-js';
import { getWordNumber } from '../waline/utils/wordCount';
import commentListState, { makeDataReactive } from './commentListState';
import configProvider from './configProvider';
import userInfoState from './userInfoState';
const commentBoxState = createRoot(() => {
const [edit, setEdit] = createSignal<ReactiveComment | null>(null);
const [rootId, setRootId] = createSignal<number | undefined>();
const [replyId, setReplyId] = createSignal<number | undefined>();
const [replyUser, setReplyUser] = createSignal<string | undefined>();
const [content, setContent] = createSignal('');
const [wordCount, setWordCount] = createSignal(0);
const [isWordCountLegal, setIsWordCountLegal] = createSignal(false);
const [wordLimit, setWordLimit] = createSignal(0);
const [isSubmitting, setIsSubmitting] = createSignal(false);
const [showPreview, setShowPreview] = createSignal(false);
const { config } = configProvider;
const previewSignal = createMemo(() => ({ showPreview: showPreview(), content: content() }));
const [previewText, { mutate: setPreviewText }] = createResource(
previewSignal,
async (preSign: { content: string; showPreview: boolean }) => {
if (!preSign.showPreview) return '';
return config().renderPreview(preSign.content);
},
);
const limit = createMemo(() => config().wordLimit);
let editorRef: HTMLTextAreaElement | undefined;
createEffect(() => {
setWordCount(getWordNumber(content()));
});
createEffect(() => {
if (limit()) {
const limit2 = limit() as [number, number];
if (wordCount() < limit2[0] && limit2[0] !== 0) {
setWordLimit(limit2[0]);
setIsWordCountLegal(false);
} else if (wordCount() > limit2[1]) {
setWordLimit(limit2[1]);
setIsWordCountLegal(false);
} else {
setWordLimit(limit2[1]);
setIsWordCountLegal(true);
}
} else {
setWordLimit(0);
setIsWordCountLegal(true);
}
});
createEffect(() => {
if (edit()) {
setContent(edit()!.orig() || '');
}
});
return {
edit,
rootId,
replyId,
replyUser,
content,
wordCount,
isWordCountLegal,
wordLimit,
isSubmitting,
previewText,
editorRef,
showPreview,
setEdit,
setReplyId,
setReplyUser,
setRootId,
setContent,
setWordCount,
setIsSubmitting,
setPreviewText,
setShowPreview,
};
});
export const userMetaState = createRoot(() => {
const [userMeta, setUserMeta] = createSignal({ nick: '', mail: '', link: '' });
const inputRefs: Record<'nick' | 'mail' | 'link', HTMLInputElement | undefined> = {
nick: undefined,
mail: undefined,
link: undefined,
};
return { userMeta, setUserMeta, inputRefs };
});
export function clearReplyState() {
const { setReplyId, setReplyUser, setRootId } = commentBoxState;
setReplyId(undefined);
setReplyUser(undefined);
setRootId(undefined);
}
export function submitComment() {
const { config, locale } = configProvider;
const {
edit,
content,
wordCount,
isWordCountLegal,
rootId,
replyId,
replyUser,
editorRef,
setIsSubmitting,
setEdit,
setContent,
setPreviewText,
} = commentBoxState;
const { userMeta, inputRefs } = userMetaState;
const { userInfo } = userInfoState;
const { serverURL, lang, login, wordLimit, requiredMeta } = config();
const { data, setData } = commentListState;
// let token = '' //preserved for Recaptcha
const comment: WalineCommentData = {
comment: content(),
nick: userMeta().nick,
mail: userMeta().mail,
link: userMeta().link,
ua: navigator.userAgent,
url: config().path,
recaptchaV3: '',
};
if (userInfo()?.token) {
// login user
comment.nick = userInfo()!.display_name;
comment.mail = userInfo()!.email;
comment.link = userInfo()!.url;
} else {
if (login === 'force') return null;
// check nick
if (requiredMeta.includes('nick') && !comment.nick) {
inputRefs.nick?.focus();
return alert(locale().nickError);
}
// check mail
if (
(requiredMeta.includes('mail') && !comment.mail) ||
(comment.mail && !/^\w(?:[\w.\-]*\w)?@(?:\w(?:[\w-]*\w)?\.)*\w+$/.exec(comment.mail))
) {
inputRefs.mail?.focus();
return alert(locale().mailError);
}
// check comment
if (!comment.comment) {
editorRef?.focus();
return null;
}
if (!comment.nick) comment.nick = locale().anonymous;
}
if (!isWordCountLegal()) {
return alert(
locale()
.wordHint.replace('$0', (wordLimit as [number, number])[0].toString())
.replace('$1', (wordLimit as [number, number])[1].toString())
.replace('$2', wordCount().toString()),
);
}
if (replyId() && rootId()) {
comment.pid = replyId();
comment.rid = rootId();
comment.at = replyUser();
}
setIsSubmitting(true);
const options = { serverURL, lang, token: userInfo()?.token || '', comment };
return (edit() ? updateComment({ objectId: edit()!.objectId, ...options }) : addComment(options))
.then((res) => {
setIsSubmitting(false);
if (res.errmsg) return alert(res.errmsg);
const resComment = res.data!;
if (edit()) {
const target = data().find((item) => item.objectId === edit()!.objectId);
if (!target) return null;
target.setComment(resComment.comment);
if (resComment.orig) target.setOrig(resComment.orig);
setEdit(null);
clearReplyState();
} else if ('rid' in resComment) {
const target = data().find((item) => item.objectId === resComment.rid);
if (!target) return null;
target.setChildren((prev) => [...prev, makeDataReactive(resComment)]);
clearReplyState();
} else {
setData((dat) => [makeDataReactive(resComment), ...dat]);
}
setContent('');
setPreviewText('');
if (replyId()) {
clearReplyState();
}
return null;
})
.catch((err: TypeError) => {
setIsSubmitting(false);
alert(err.message);
});
}
export default commentBoxState;