migration of comments
This commit is contained in:
388
src/components/blog/CommentBlock.tsx
Normal file
388
src/components/blog/CommentBlock.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import {
|
||||
createSignal,
|
||||
createEffect,
|
||||
For,
|
||||
Show,
|
||||
onMount,
|
||||
onCleanup
|
||||
} from "solid-js";
|
||||
import { useLocation } from "@solidjs/router";
|
||||
import type {
|
||||
CommentBlockProps,
|
||||
CommentReaction,
|
||||
UserPublicData
|
||||
} from "~/types/comment";
|
||||
import { debounce } from "~/lib/comment-utils";
|
||||
import UserDefaultImage from "~/components/icons/UserDefaultImage";
|
||||
import ReplyIcon from "~/components/icons/ReplyIcon";
|
||||
import TrashIcon from "~/components/icons/TrashIcon";
|
||||
import EditIcon from "~/components/icons/EditIcon";
|
||||
import ThumbsUpEmoji from "~/components/icons/emojis/ThumbsUp";
|
||||
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||
import CommentInputBlock from "./CommentInputBlock";
|
||||
import ReactionBar from "./ReactionBar";
|
||||
|
||||
export default function CommentBlock(props: CommentBlockProps) {
|
||||
const location = useLocation();
|
||||
|
||||
// State signals
|
||||
const [commentCollapsed, setCommentCollapsed] = createSignal(false);
|
||||
const [showingReactionOptions, setShowingReactionOptions] =
|
||||
createSignal(false);
|
||||
const [replyBoxShowing, setReplyBoxShowing] = createSignal(false);
|
||||
const [toggleHeight, setToggleHeight] = createSignal(0);
|
||||
const [reactions, setReactions] = createSignal<CommentReaction[]>([]);
|
||||
const [windowWidth, setWindowWidth] = createSignal(0);
|
||||
const [deletionLoading, setDeletionLoading] = createSignal(false);
|
||||
const [userData, setUserData] = createSignal<UserPublicData | null>(null);
|
||||
|
||||
// Refs
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
let commentInputRef: HTMLDivElement | undefined;
|
||||
|
||||
// Auto-collapse at level 4+
|
||||
createEffect(() => {
|
||||
setCommentCollapsed(props.level >= 4);
|
||||
});
|
||||
|
||||
// Window resize handler
|
||||
onMount(() => {
|
||||
const handleResize = debounce(() => {
|
||||
setWindowWidth(window.innerWidth);
|
||||
}, 200);
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
});
|
||||
|
||||
// Find user data from comment map
|
||||
createEffect(() => {
|
||||
if (props.userCommentMap) {
|
||||
props.userCommentMap.forEach((commentIds, user) => {
|
||||
if (commentIds.includes(props.comment.id)) {
|
||||
setUserData(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update toggle height based on container size
|
||||
createEffect(() => {
|
||||
if (containerRef) {
|
||||
const correction = showingReactionOptions() ? 80 : 48;
|
||||
setToggleHeight(containerRef.clientHeight + correction);
|
||||
}
|
||||
// Trigger on these dependencies
|
||||
windowWidth();
|
||||
showingReactionOptions();
|
||||
});
|
||||
|
||||
// Update reactions from map
|
||||
createEffect(() => {
|
||||
setReactions(props.reactionMap.get(props.comment.id) || []);
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
const collapseCommentToggle = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCommentCollapsed(!commentCollapsed());
|
||||
};
|
||||
|
||||
const showingReactionOptionsToggle = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowingReactionOptions(!showingReactionOptions());
|
||||
};
|
||||
|
||||
const toggleCommentReplyBox = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setReplyBoxShowing(!replyBoxShowing());
|
||||
};
|
||||
|
||||
const deleteCommentTrigger = async (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setDeletionLoading(true);
|
||||
const user = userData();
|
||||
props.toggleModification(
|
||||
props.comment.id,
|
||||
props.comment.commenter_id,
|
||||
props.comment.body,
|
||||
"delete",
|
||||
user?.image,
|
||||
user?.email,
|
||||
user?.display_name
|
||||
);
|
||||
setDeletionLoading(false);
|
||||
};
|
||||
|
||||
const editCommentTrigger = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const user = userData();
|
||||
props.toggleModification(
|
||||
props.comment.id,
|
||||
props.comment.commenter_id,
|
||||
props.comment.body,
|
||||
"edit",
|
||||
user?.image,
|
||||
user?.email,
|
||||
user?.display_name
|
||||
);
|
||||
};
|
||||
|
||||
// Computed values
|
||||
const upvoteCount = () =>
|
||||
reactions().filter((r) => r.type === "upVote").length;
|
||||
|
||||
const downvoteCount = () =>
|
||||
reactions().filter((r) => r.type === "downVote").length;
|
||||
|
||||
const hasUpvoted = () =>
|
||||
reactions().some(
|
||||
(r) => r.type === "upVote" && r.user_id === props.currentUserID
|
||||
);
|
||||
|
||||
const hasDownvoted = () =>
|
||||
reactions().some(
|
||||
(r) => r.type === "downVote" && r.user_id === props.currentUserID
|
||||
);
|
||||
|
||||
const canDelete = () =>
|
||||
props.currentUserID === props.comment.commenter_id ||
|
||||
props.privilegeLevel === "admin";
|
||||
|
||||
const canEdit = () => props.currentUserID === props.comment.commenter_id;
|
||||
|
||||
const isAnonymous = () => props.privilegeLevel === "anonymous";
|
||||
|
||||
const replyIconColor = () =>
|
||||
location.pathname.split("/")[1] === "blog" ? "#fb923c" : "#60a5fa";
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Collapsed state */}
|
||||
<Show when={commentCollapsed()}>
|
||||
<button
|
||||
onClick={collapseCommentToggle}
|
||||
class="ml-5 w-full px-2 lg:w-3/4"
|
||||
>
|
||||
<div class="my-auto mt-1 mr-2 h-8 border-l-2 border-black dark:border-white" />
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
{/* Expanded state */}
|
||||
<Show when={!commentCollapsed()}>
|
||||
<div class="z-[500] transition-all duration-300 ease-in-out">
|
||||
<div class="my-4 flex w-full overflow-x-hidden overflow-y-hidden lg:w-3/4">
|
||||
{/* Vote buttons column */}
|
||||
<div
|
||||
class="flex flex-col justify-between"
|
||||
style={{ height: `${toggleHeight()}px` }}
|
||||
>
|
||||
{/* Upvote */}
|
||||
<button
|
||||
onClick={() =>
|
||||
props.commentReaction("upVote", props.comment.id)
|
||||
}
|
||||
>
|
||||
<div
|
||||
class={`h-5 w-5 ${
|
||||
hasUpvoted()
|
||||
? "fill-emerald-500"
|
||||
: `fill-black hover:fill-emerald-500 dark:fill-white ${
|
||||
isAnonymous() ? "tooltip z-50" : ""
|
||||
}`
|
||||
}`}
|
||||
>
|
||||
<ThumbsUpEmoji />
|
||||
<Show when={isAnonymous()}>
|
||||
<div class="tooltip-text -ml-16 w-32 text-white">
|
||||
You must be logged in
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Vote count */}
|
||||
<div class="mx-auto">{upvoteCount() - downvoteCount()}</div>
|
||||
|
||||
{/* Downvote */}
|
||||
<button
|
||||
onClick={() =>
|
||||
props.commentReaction("downVote", props.comment.id)
|
||||
}
|
||||
>
|
||||
<div
|
||||
class={`h-5 w-5 ${
|
||||
hasDownvoted()
|
||||
? "fill-rose-500"
|
||||
: `fill-black hover:fill-rose-500 dark:fill-white ${
|
||||
isAnonymous() ? "tooltip z-50" : ""
|
||||
}`
|
||||
}`}
|
||||
>
|
||||
<div class="rotate-180">
|
||||
<ThumbsUpEmoji />
|
||||
</div>
|
||||
<Show when={isAnonymous()}>
|
||||
<div class="tooltip-text -ml-16 w-32">
|
||||
You must be logged in
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Collapse toggle line */}
|
||||
<button onClick={collapseCommentToggle} class="z-0 px-2">
|
||||
<div
|
||||
class="border-l-2 border-black transition-all duration-300 ease-in-out dark:border-white"
|
||||
style={{ height: `${toggleHeight()}px` }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Comment content */}
|
||||
<div
|
||||
class="w-3/4"
|
||||
onClick={showingReactionOptionsToggle}
|
||||
id={props.comment.id.toString()}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="overflow-x-hidden overflow-y-hidden select-text"
|
||||
>
|
||||
<div class="max-w-[90%] md:max-w-[75%]">
|
||||
{props.comment.body}
|
||||
</div>
|
||||
<Show when={props.comment.edited}>
|
||||
<div class="pb-0.5 text-xs italic">Edited</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* User info */}
|
||||
<div class="flex pl-2">
|
||||
<Show
|
||||
when={userData()?.image}
|
||||
fallback={
|
||||
<UserDefaultImage strokeWidth={1} height={24} width={24} />
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={userData()!.image}
|
||||
height={24}
|
||||
width={24}
|
||||
alt="user-image"
|
||||
class="h-6 w-6 rounded-full object-cover object-center"
|
||||
/>
|
||||
</Show>
|
||||
<div class="px-1">
|
||||
{userData()?.display_name || userData()?.email || "[removed]"}
|
||||
</div>
|
||||
|
||||
{/* Delete button */}
|
||||
<Show when={canDelete()}>
|
||||
<button onClick={deleteCommentTrigger}>
|
||||
<Show
|
||||
when={!deletionLoading()}
|
||||
fallback={<LoadingSpinner height={24} width={24} />}
|
||||
>
|
||||
<TrashIcon
|
||||
height={24}
|
||||
width={24}
|
||||
stroke="red"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Edit and Reply buttons */}
|
||||
<div class="absolute flex">
|
||||
<Show when={canEdit()}>
|
||||
<button onClick={editCommentTrigger} class="px-2">
|
||||
<EditIcon strokeWidth={1} height={24} width={24} />
|
||||
</button>
|
||||
</Show>
|
||||
<button onClick={toggleCommentReplyBox} class="z-30">
|
||||
<ReplyIcon color={replyIconColor()} height={24} width={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Reaction bar */}
|
||||
<div
|
||||
class={`${
|
||||
showingReactionOptions() || reactions().length > 0
|
||||
? ""
|
||||
: "opacity-0"
|
||||
} ml-16`}
|
||||
>
|
||||
<ReactionBar
|
||||
commentID={props.comment.id}
|
||||
currentUserID={props.currentUserID}
|
||||
reactions={reactions()}
|
||||
showingReactionOptions={showingReactionOptions()}
|
||||
privilegeLevel={props.privilegeLevel}
|
||||
commentReaction={props.commentReaction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reply box */}
|
||||
<Show when={replyBoxShowing()}>
|
||||
<div
|
||||
ref={commentInputRef}
|
||||
class="fade-in lg:w-2/3"
|
||||
style={{ "margin-left": `${-24 * props.recursionCount}px` }}
|
||||
>
|
||||
<CommentInputBlock
|
||||
isReply={true}
|
||||
privilegeLevel={props.privilegeLevel}
|
||||
parent_id={props.comment.id}
|
||||
type={props.category}
|
||||
post_id={props.projectID}
|
||||
currentUserID={props.currentUserID}
|
||||
socket={props.socket}
|
||||
newComment={props.newComment}
|
||||
commentSubmitLoading={props.commentSubmitLoading}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Recursive child comments */}
|
||||
<div class="pl-2 sm:pl-4 md:pl-8 lg:pl-12">
|
||||
<For each={props.child_comments}>
|
||||
{(childComment) => (
|
||||
<CommentBlock
|
||||
comment={childComment}
|
||||
category={props.category}
|
||||
projectID={props.projectID}
|
||||
recursionCount={1}
|
||||
allComments={props.allComments}
|
||||
child_comments={props.allComments?.filter(
|
||||
(comment) => comment.parent_comment_id === childComment.id
|
||||
)}
|
||||
privilegeLevel={props.privilegeLevel}
|
||||
currentUserID={props.currentUserID}
|
||||
reactionMap={props.reactionMap}
|
||||
level={props.level + 1}
|
||||
socket={props.socket}
|
||||
userCommentMap={props.userCommentMap}
|
||||
toggleModification={props.toggleModification}
|
||||
newComment={props.newComment}
|
||||
commentSubmitLoading={props.commentSubmitLoading}
|
||||
commentReaction={props.commentReaction}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user