feat: 添加评论功能的客户端信息显示选项,更新相关接口和组件

This commit is contained in:
2025-09-13 16:42:23 +08:00
parent 2d0e1a46e2
commit 4da06b931f
8 changed files with 218 additions and 174 deletions

View File

@ -1,22 +1,23 @@
package dto
type CommentDto struct {
ID uint `json:"id"`
TargetID uint `json:"target_id"`
TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
Content string `json:"content"`
ReplyID uint `json:"reply_id"` // 回复的评论ID
Depth int `json:"depth"` // 评论的层级深度
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
User UserDto `json:"user"` // 评论的
ReplyCount uint64 `json:"reply_count"` // 回复数量
LikeCount uint64 `json:"like_count"` // 点赞数量
IsLiked bool `json:"is_liked"` // 当前用户是否点赞
IsPrivate bool `json:"is_private"`
Location string `json:"location"` // 用户位置基于IP
OS string `json:"os"` // 用户操作系统基于User-Agent
Browser string `json:"browser"` // 用户浏览器基于User-Agent
ID uint `json:"id"`
TargetID uint `json:"target_id"`
TargetType string `json:"target_type"` // 目标类型,如 "post", "page"
Content string `json:"content"`
ReplyID uint `json:"reply_id"` // 回复的评论ID
Depth int `json:"depth"` // 评论的层级深度
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
User UserDto `json:"user"` // 评论的
ReplyCount uint64 `json:"reply_count"` // 回复数量
LikeCount uint64 `json:"like_count"` // 点赞数量
IsLiked bool `json:"is_liked"` // 当前用户是否点赞
IsPrivate bool `json:"is_private"`
Location string `json:"location"` // 用户位置基于IP
OS string `json:"os"` // 用户操作系统基于User-Agent
Browser string `json:"browser"` // 用户浏览器基于User-Agent
ShowClientInfo bool `json:"show_client_info"`
}
type CreateCommentReq struct {
@ -31,9 +32,10 @@ type CreateCommentReq struct {
}
type UpdateCommentReq struct {
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
Content string `json:"content" binding:"required"` // 评论内容
IsPrivate bool `json:"is_private"` // 是否私密
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
Content string `json:"content" binding:"required"` // 评论内容
IsPrivate bool `json:"is_private"` // 是否私密
ShowClientInfo bool `json:"show_client_info"` // 是否显示客户端信息
}
type GetCommentListReq struct {

View File

@ -127,7 +127,7 @@ func (cr *CommentRepo) UpdateComment(comment *model.Comment) error {
return errs.New(http.StatusBadRequest, "invalid comment ID", nil)
}
if err := GetDB().Select("IsPrivate", "Content").Updates(comment).Error; err != nil {
if err := GetDB().Select("IsPrivate", "ShowClientInfo", "Content").Updates(comment).Error; err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"strconv"
"github.com/sirupsen/logrus"
"github.com/snowykami/neo-blog/pkg/constant"
"github.com/snowykami/neo-blog/pkg/utils"
@ -59,6 +60,7 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm
if !ok {
return errs.ErrUnauthorized
}
logrus.Infof("UpdateComment: currentUser ID %d, req.CommentID %d", currentUser.ID, req.CommentID)
comment, err := repo.Comment.GetComment(strconv.Itoa(int(req.CommentID)))
if err != nil {
@ -71,13 +73,11 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm
comment.Content = req.Content
comment.IsPrivate = req.IsPrivate
comment.ShowClientInfo = req.ShowClientInfo
err = repo.Comment.UpdateComment(comment)
if err != nil {
return err
}
return nil
}
@ -102,7 +102,6 @@ func (cs *CommentService) DeleteComment(ctx context.Context, commentID string) e
if err := repo.Comment.DeleteComment(commentID); err != nil {
return err
}
return nil
}
@ -120,38 +119,7 @@ func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dt
if comment.IsPrivate && currentUserID != comment.UserID {
return nil, errs.ErrForbidden
}
isLiked := false
if currentUserID != 0 {
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
}
ua := utils.ParseUA(comment.UserAgent)
if !comment.ShowClientInfo {
comment.Location = ""
ua.OS = ""
ua.OSVersion = ""
ua.Browser = ""
ua.BrowserVer = ""
}
commentDto := dto.CommentDto{
ID: comment.ID,
Content: comment.Content,
TargetID: comment.TargetID,
TargetType: comment.TargetType,
ReplyID: comment.ReplyID,
CreatedAt: comment.CreatedAt.String(),
UpdatedAt: comment.UpdatedAt.String(),
Depth: comment.Depth,
User: comment.User.ToDto(),
ReplyCount: comment.CommentCount,
LikeCount: comment.LikeCount,
IsLiked: isLiked,
IsPrivate: comment.IsPrivate,
OS: ua.OS + " " + ua.OSVersion,
Browser: ua.Browser + " " + ua.BrowserVer,
Location: comment.Location,
}
commentDto := cs.toGetCommentDto(comment, currentUserID)
return &commentDto, err
}
@ -167,39 +135,46 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen
commentDtos := make([]dto.CommentDto, 0)
for _, comment := range comments {
//replyCount, _ := repo.Comment.CountReplyComments(currentUserID, comment.ID)
isLiked := false
if currentUserID != 0 {
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
}
ua := utils.ParseUA(comment.UserAgent)
commentDto := dto.CommentDto{
ID: comment.ID,
Content: comment.Content,
TargetID: comment.TargetID,
TargetType: comment.TargetType,
ReplyID: comment.ReplyID,
CreatedAt: comment.CreatedAt.String(),
UpdatedAt: comment.UpdatedAt.String(),
Depth: comment.Depth,
User: comment.User.ToDto(),
ReplyCount: comment.CommentCount,
LikeCount: comment.LikeCount,
IsLiked: isLiked,
IsPrivate: comment.IsPrivate,
OS: ua.OS + " " + ua.OSVersion,
Browser: ua.Browser + " " + ua.BrowserVer,
Location: comment.Location,
}
if !comment.ShowClientInfo {
commentDto.Location = ""
commentDto.OS = ""
commentDto.Browser = ""
}
commentDto := cs.toGetCommentDto(&comment, currentUserID)
commentDtos = append(commentDtos, commentDto)
}
return commentDtos, nil
}
func (cs *CommentService) toGetCommentDto(comment *model.Comment, currentUserID uint) dto.CommentDto {
isLiked := false
if currentUserID != 0 {
isLiked, _ = repo.Like.IsLiked(currentUserID, comment.ID, constant.TargetTypeComment)
}
ua := utils.ParseUA(comment.UserAgent)
if !comment.ShowClientInfo {
comment.Location = ""
ua.OS = ""
ua.OSVersion = ""
ua.Browser = ""
ua.BrowserVer = ""
}
return dto.CommentDto{
ID: comment.ID,
Content: comment.Content,
TargetID: comment.TargetID,
TargetType: comment.TargetType,
ReplyID: comment.ReplyID,
CreatedAt: comment.CreatedAt.String(),
UpdatedAt: comment.UpdatedAt.String(),
Depth: comment.Depth,
User: comment.User.ToDto(),
ReplyCount: comment.CommentCount,
LikeCount: comment.LikeCount,
IsLiked: isLiked,
IsPrivate: comment.IsPrivate,
OS: ua.OS + " " + ua.OSVersion,
Browser: ua.Browser + " " + ua.BrowserVer,
Location: comment.Location,
ShowClientInfo: comment.ShowClientInfo,
}
}
func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) {
switch targetType {
case constant.TargetTypePost:

View File

@ -36,17 +36,21 @@ export async function createComment(
export async function updateComment(
{
id, content,
isPrivate = false
id,
content,
isPrivate = false,
showClientInfo = true
}: {
id: number
content: string
isPrivate?: boolean // 可选字段,默认为 false
showClientInfo?: boolean
}
): Promise<BaseResponse<Comment>> {
const res = await axiosClient.put<BaseResponse<Comment>>(`/comment/c/${id}`, {
content,
isPrivate
isPrivate,
showClientInfo,
})
return res.data
}

View File

@ -17,7 +17,8 @@ export function CommentInput(
initContent = "",
initIsPrivate = false,
placeholder = "",
isUpdate = false
isUpdate = false,
initShowClientInfo = true
}: {
user: User | null,
onCommentSubmitted: ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => void,
@ -25,6 +26,7 @@ export function CommentInput(
initIsPrivate?: boolean,
placeholder?: string,
isUpdate?: boolean,
initShowClientInfo?: boolean
}
) {
const t = useTranslations('Comment')
@ -33,7 +35,7 @@ export function CommentInput(
const clickToUserProfile = useToUserProfile();
const [isPrivate, setIsPrivate] = useState(initIsPrivate);
const [showClientInfo, setShowClientInfo] = useState(true);
const [showClientInfo, setShowClientInfo] = useState(initShowClientInfo);
const [commentContent, setCommentContent] = useState(initContent);
const handleCommentSubmit = async () => {
@ -51,7 +53,7 @@ export function CommentInput(
toast.error(t("content_required"));
return;
}
if (initContent === commentContent.trim() && initIsPrivate === isPrivate) {
if (initContent === commentContent.trim() && initIsPrivate === isPrivate && initShowClientInfo === showClientInfo) {
toast.warning(t("comment_unchanged"));
return;
}

View File

@ -10,7 +10,7 @@ import { TargetType } from "@/models/types";
import { toggleLike } from "@/api/like";
import { useDoubleConfirm } from "@/hooks/use-double-confirm";
import { CommentInput } from "./comment-input";
import { createComment, deleteComment, listComments, updateComment } from "@/api/comment";
import { createComment, deleteComment, getComment, listComments, updateComment } from "@/api/comment";
import { OrderBy } from "@/models/common";
import { formatDateTime } from "@/utils/common/datetime";
@ -41,11 +41,11 @@ export function CommentItem(
const clickToLogin = useToLogin();
const { confirming, onClick, onBlur } = useDoubleConfirm();
const [likeCount, setLikeCount] = useState(comment.likeCount);
const [liked, setLiked] = useState(comment.isLiked);
const [commentState, setCommentState] = useState<Comment>(comment); // 用于更新评论内容
const [likeCount, setLikeCount] = useState(commentState.likeCount);
const [liked, setLiked] = useState(commentState.isLiked);
const [canClickLike, setCanClickLike] = useState(true);
const [isPrivate, setIsPrivate] = useState(comment.isPrivate);
const [replyCount, setReplyCount] = useState(comment.replyCount);
const [replyCount, setReplyCount] = useState(commentState.replyCount);
const [showReplies, setShowReplies] = useState(false);
const [replies, setReplies] = useState<Comment[]>([]);
const [repliesLoaded, setRepliesLoaded] = useState(false);
@ -74,7 +74,7 @@ export function CommentItem(
setLiked(prev => !prev);
setLikeCount(prev => prev + (likedPrev ? -1 : 1));
toggleLike(
{ targetType: TargetType.Comment, targetId: comment.id }
{ targetType: TargetType.Comment, targetId: commentState.id }
).then(res => {
toast.success(res.data.status ? t("like_success") : t("unlike_success"));
setCanClickLike(true);
@ -90,14 +90,14 @@ export function CommentItem(
const reloadReplies = () => {
listComments(
{
targetType: comment.targetType,
targetId: comment.targetId,
depth: comment.depth + 1,
targetType: commentState.targetType,
targetId: commentState.targetId,
depth: commentState.depth + 1,
orderBy: OrderBy.CreatedAt,
desc: false,
page: 1,
size: 999999,
commentId: comment.id
commentId: commentState.id
}
).then(response => {
setReplies(response.data.comments);
@ -112,11 +112,13 @@ export function CommentItem(
setShowReplies(!showReplies);
}
const onCommentEdit = ({ commentContent: newContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => {
updateComment({ id: comment.id, content: newContent, isPrivate }).then(() => {
const onCommentEdit = ({ commentContent: newContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
updateComment({ id: commentState.id, content: newContent, isPrivate, showClientInfo }).then(() => {
toast.success(t("edit_success"));
comment.content = newContent;
setIsPrivate(isPrivate);
getComment({ id: commentState.id }).then(response => {
setCommentState(response.data);
console.log(response.data);
});
setActiveInputId(null);
}).catch(error => {
toast.error(t("edit_failed") + ": " + error.message);
@ -125,10 +127,10 @@ export function CommentItem(
const onReply = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => {
createComment({
targetType: comment.targetType,
targetId: comment.targetId,
targetType: commentState.targetType,
targetId: commentState.targetId,
content: commentContent,
replyId: comment.id,
replyId: commentState.id,
isPrivate,
showClientInfo
}).then(() => {
@ -158,24 +160,24 @@ export function CommentItem(
return (
<div>
<div className="flex">
<div onClick={() => clickToUserProfile(comment.user.username)} className="cursor-pointer fade-in w-12 h-12">
<GravatarAvatar className="w-full h-full" url={comment.user.avatarUrl} email={comment.user.email} size={100} />
<div onClick={() => clickToUserProfile(commentState.user.username)} className="cursor-pointer fade-in w-12 h-12">
<GravatarAvatar className="w-full h-full" url={commentState.user.avatarUrl} email={commentState.user.email} size={100} />
</div>
<div className="flex-1 pl-2 fade-in-up">
<div className="flex gap-2 md:gap-4 items-center">
<div onClick={() => clickToUserProfile(comment.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up">
{comment.user.nickname}
<div onClick={() => clickToUserProfile(commentState.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up">
{commentState.user.nickname}
</div>
<span className="text-xs">{formatDateTime({
dateTimeString: comment.createdAt,
dateTimeString: commentState.createdAt,
locale,
convertShortAgo: true,
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
})}</span>
{comment.createdAt !== comment.updatedAt &&
{commentState.createdAt !== commentState.updatedAt &&
<span className="text-xs">{t("edit_at", {
time: formatDateTime({
dateTimeString: comment.updatedAt,
dateTimeString: commentState.updatedAt,
locale,
convertShortAgo: true,
unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") }
@ -184,105 +186,106 @@ export function CommentItem(
</div>
<p className="text-lg text-slate-600 dark:text-slate-400 fade-in">
{
isPrivate && <Lock className="inline w-4 h-4 mr-1 mb-1 text-slate-500 dark:text-slate-400" />
commentState.isPrivate && <Lock className="inline w-4 h-4 mr-1 mb-1 text-slate-500 dark:text-slate-400" />
}
{
parentComment &&
<>{t("reply")} <button onClick={() => clickToUserProfile(parentComment.user.nickname)} className="text-primary">{parentComment?.user.nickname}</button>: </>
}
{comment.content}
{commentState.content}
</p>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400 fade-in flex flex-col md:flex-row items-start md:items-center md:justify-between gap-2">
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400 fade-in flex flex-col md:flex-row items-start md:items-center md:justify-between gap-2">
<div className="flex items-center gap-2 flex-wrap">
{/* 用户地理,浏览器,系统信息 */}
{comment.location && <span title={comment.location} >{comment.location}</span>}
{comment.browser && <span title={comment.browser}>{comment.browser}</span>}
{comment.os && <span title={comment.os}>{comment.os}</span>}
{commentState.location && <span title={commentState.location} >{commentState.location}</span>}
{commentState.browser && <span title={commentState.browser}>{commentState.browser}</span>}
{commentState.os && <span title={commentState.os}>{commentState.os}</span>}
</div>
<div className="flex items-center gap-4 w-full md:w-auto">
{/* 回复按钮 */}
<button
title={t("reply")}
onClick={() => {
if (activeInput?.type === 'reply' && activeInput.id === comment.id) {
setActiveInputId(null);
} else {
setActiveInputId({ id: comment.id, type: 'reply' });
}
}}
className={`flex items-center justify-center px-2 py-1 h-5
title={t("reply")}
onClick={() => {
if (activeInput?.type === 'reply' && activeInput.id === commentState.id) {
setActiveInputId(null);
} else {
setActiveInputId({ id: commentState.id, type: 'reply' });
}
}}
className={`flex items-center justify-center px-2 py-1 h-5
text-primary-foreground dark:text-white text-xs
rounded ${activeInput?.type === 'reply' && activeInput.id === comment.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`}
rounded ${activeInput?.type === 'reply' && activeInput.id === commentState.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`}
>
<Reply className="w-3 h-3" />
<Reply className="w-3 h-3" />
</button>
{/* 点赞按钮 */}
<button
title={t(liked ? "unlike" : "like")}
onClick={handleToggleLike}
className={`flex items-center justify-center px-2 py-1 h-5 gap-1 text-xs rounded
title={t(liked ? "unlike" : "like")}
onClick={handleToggleLike}
className={`flex items-center justify-center px-2 py-1 h-5 gap-1 text-xs rounded
${liked ? 'bg-primary' : 'bg-slate-400 hover:bg-slate-600'}
text-primary-foreground dark:text-white dark:hover:bg-slate-500 fade-in`}
>
<Heart className="w-3 h-3" /> <div>{likeCount}</div>
<Heart className="w-3 h-3" /> <div>{likeCount}</div>
</button>
{/* 编辑和删除按钮 仅自己的评论可见 */}
{user?.id === comment.user.id && (
<>
<button
title={t("edit")}
onClick={() => {
if (activeInput?.type === 'edit' && activeInput.id === comment.id) {
setActiveInputId(null);
} else {
setActiveInputId({ id: comment.id, type: 'edit' });
}
}}
className={`
{user?.id === commentState.user.id && (
<>
<button
title={t("edit")}
onClick={() => {
if (activeInput?.type === 'edit' && activeInput.id === commentState.id) {
setActiveInputId(null);
} else {
setActiveInputId({ id: commentState.id, type: 'edit' });
}
}}
className={`
flex items-center justify-center px-2 py-1 h-5
text-primary-foreground dark:text-white text-xs
rounded ${activeInput?.type === 'edit' && activeInput.id === comment.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`}
>
<Pencil className="w-3 h-3" />
</button>
rounded ${activeInput?.type === 'edit' && activeInput.id === commentState.id ? "bg-slate-600" : "bg-slate-400"} hover:bg-slate-600 dark:hover:bg-slate-500 fade-in-up`}
>
<Pencil className="w-3 h-3" />
</button>
<button
title={t("delete")}
className={`flex items-center justify-center px-2 py-1 h-5 rounded
<button
title={t("delete")}
className={`flex items-center justify-center px-2 py-1 h-5 rounded
text-primary-foreground dark:text-white text-xs
${confirming ? 'bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600' : 'bg-slate-400 hover:bg-slate-600 dark:hover:bg-slate-500'} fade-in`}
onClick={() => onClick(() => { onCommentDelete({ commentId: comment.id }); })}
onBlur={onBlur}
>
<Trash className="w-3 h-3" />
{confirming && (
<span className="ml-1 confirm-delete-anim">{t("confirm_delete")}</span>
)}
</button>
</>
onClick={() => onClick(() => { onCommentDelete({ commentId: commentState.id }); })}
onBlur={onBlur}
>
<Trash className="w-3 h-3" />
{confirming && (
<span className="ml-1 confirm-delete-anim">{t("confirm_delete")}</span>
)}
</button>
</>
)}
{replyCount > 0 && (
<button onClick={toggleReplies} className="fade-in-up">
{!showReplies ? t("expand_replies", { count: replyCount }) : t("collapse_replies")}
</button>
<button onClick={toggleReplies} className="fade-in-up">
{!showReplies ? t("expand_replies", { count: replyCount }) : t("collapse_replies")}
</button>
)}
</div>
</div>
</div>
{/* 这俩输入框一次只能显示一个 */}
{activeInput && activeInput.type === 'reply' && activeInput.id === comment.id && <CommentInput
{activeInput && activeInput.type === 'reply' && activeInput.id === commentState.id && <CommentInput
user={user}
onCommentSubmitted={onReply}
initIsPrivate={isPrivate}
placeholder={`${t("reply")} ${comment.user.nickname} :`}
initIsPrivate={commentState.isPrivate}
placeholder={`${t("reply")} ${commentState.user.nickname} :`}
/>}
{activeInput && activeInput.type === 'edit' && activeInput.id === comment.id && <CommentInput
{activeInput && activeInput.type === 'edit' && activeInput.id === commentState.id && <CommentInput
user={user}
initContent={comment.content}
initIsPrivate={isPrivate}
initContent={commentState.content}
initIsPrivate={commentState.isPrivate}
onCommentSubmitted={onCommentEdit}
isUpdate={true}
initShowClientInfo={commentState.showClientInfo}
/>}
</div>
@ -294,7 +297,7 @@ export function CommentItem(
key={reply.id}
user={reply.user}
comment={reply}
parentComment={comment}
parentComment={commentState}
onCommentDelete={onReplyDelete}
activeInput={activeInput}
setActiveInputId={setActiveInputId}

View File

@ -59,6 +59,63 @@ export const markdownComponents = {
{...props}
/>
),
table: (props: React.ComponentPropsWithoutRef<"table">) => {
const { children, className, ...rest } = props;
return (
<div className="my-4 overflow-auto rounded-md shadow-sm">
<table
className={`min-w-full divide-y divide-gray-200 dark:divide-gray-700 ${className ?? ""}`}
{...rest}
>
{children}
</table>
</div>
);
},
thead: (props: React.ComponentPropsWithoutRef<"thead">) => {
const { children, className, ...rest } = props;
return (
<thead className={`bg-gray-50 dark:bg-gray-800 ${className ?? ""}`} {...rest}>
{children}
</thead>
);
},
tbody: (props: React.ComponentPropsWithoutRef<"tbody">) => {
const { children, className, ...rest } = props;
return (
<tbody className={`bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700 ${className ?? ""}`} {...rest}>
{children}
</tbody>
);
},
tr: (props: React.ComponentPropsWithoutRef<"tr">) => {
const { children, className, ...rest } = props;
return (
<tr className={`odd:bg-white even:bg-gray-50 dark:odd:bg-gray-900 dark:even:bg-gray-800 ${className ?? ""}`} {...rest}>
{children}
</tr>
);
},
th: (props: React.ComponentPropsWithoutRef<"th">) => {
const { children, className, ...rest } = props;
return (
<th
scope="col"
className={`px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400 ${className ?? ""}`}
{...rest}
>
{children}
</th>
);
},
td: (props: React.ComponentPropsWithoutRef<"td">) => {
const { children, className, ...rest } = props;
return (
<td className={`px-3 py-2 align-top text-sm text-gray-700 dark:text-gray-300 ${className ?? ""}`} {...rest}>
{children}
</td>
);
},
};
export function RenderMarkdown(props: Omit<MDXRemoteProps, "components">) {

View File

@ -18,4 +18,5 @@ export interface Comment {
os: string
browser: string
location: string
showClientInfo: boolean
}