feat: init
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
// lib
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { ReactionSelector } from "@/components/ui";
|
||||
// helpers
|
||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
|
||||
type IssueEmojiReactionsProps = {
|
||||
anchor: string;
|
||||
issueIdFromProps?: string;
|
||||
size?: "md" | "sm";
|
||||
};
|
||||
|
||||
export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer((props) => {
|
||||
const { anchor, issueIdFromProps, size = "md" } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
// query params
|
||||
const peekId = searchParams.get("peekId") || undefined;
|
||||
const board = searchParams.get("board") || undefined;
|
||||
const state = searchParams.get("state") || undefined;
|
||||
const priority = searchParams.get("priority") || undefined;
|
||||
const labels = searchParams.get("labels") || undefined;
|
||||
// store hooks
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const { data: user } = useUser();
|
||||
|
||||
const issueId = issueIdFromProps ?? issueDetailsStore.peekId;
|
||||
const reactions = issueDetailsStore.details[issueId ?? ""]?.reaction_items ?? [];
|
||||
const groupedReactions = groupReactions(reactions, "reaction");
|
||||
|
||||
const userReactions = reactions.filter((r) => r.actor_details?.id === user?.id);
|
||||
|
||||
const handleAddReaction = (reactionHex: string) => {
|
||||
if (!issueId) return;
|
||||
issueDetailsStore.addIssueReaction(anchor, issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleRemoveReaction = (reactionHex: string) => {
|
||||
if (!issueId) return;
|
||||
issueDetailsStore.removeIssueReaction(anchor, issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
const userReaction = userReactions?.find((r) => r.actor_details?.id === user?.id && r.reaction === reactionHex);
|
||||
if (userReaction) handleRemoveReaction(reactionHex);
|
||||
else handleAddReaction(reactionHex);
|
||||
};
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
const reactionDimensions = size === "sm" ? "h-6 px-2 py-1" : "h-full px-2 py-1";
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactionSelector
|
||||
onSelect={(value) => {
|
||||
if (user) handleReactionClick(value);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size={size}
|
||||
/>
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
const REACTIONS_LIMIT = 1000;
|
||||
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
?.map((r) => r?.actor_details?.display_name)
|
||||
?.splice(0, REACTIONS_LIMIT)
|
||||
?.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (user) handleReactionClick(reaction);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex items-center gap-1 rounded-md text-sm text-custom-text-100 ${
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
} ${reactionDimensions}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type TIssueVotes = {
|
||||
anchor: string;
|
||||
issueIdFromProps?: string;
|
||||
size?: "md" | "sm";
|
||||
};
|
||||
|
||||
export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
const { anchor, issueIdFromProps, size = "md" } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
// query params
|
||||
const peekId = searchParams.get("peekId") || undefined;
|
||||
const board = searchParams.get("board") || undefined;
|
||||
const state = searchParams.get("state") || undefined;
|
||||
const priority = searchParams.get("priority") || undefined;
|
||||
const labels = searchParams.get("labels") || undefined;
|
||||
// store hooks
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const { data: user } = useUser();
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const issueId = issueIdFromProps ?? issueDetailsStore.peekId;
|
||||
|
||||
const votes = issueDetailsStore.details[issueId ?? ""]?.vote_items ?? [];
|
||||
|
||||
const allUpVotes = votes.filter((vote) => vote.vote === 1);
|
||||
const allDownVotes = votes.filter((vote) => vote.vote === -1);
|
||||
|
||||
const isUpVotedByUser = allUpVotes.some((vote) => vote.actor_details?.id === user?.id);
|
||||
const isDownVotedByUser = allDownVotes.some((vote) => vote.actor_details?.id === user?.id);
|
||||
|
||||
const handleVote = async (e: any, voteValue: 1 | -1) => {
|
||||
if (!issueId) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const actionPerformed = votes?.find((vote) => vote.actor_details?.id === user?.id && vote.vote === voteValue);
|
||||
|
||||
if (actionPerformed) await issueDetailsStore.removeIssueVote(anchor, issueId);
|
||||
else {
|
||||
await issueDetailsStore.addIssueVote(anchor, issueId, {
|
||||
vote: voteValue,
|
||||
});
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const VOTES_LIMIT = 1000;
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
const votingDimensions = size === "sm" ? "px-1 h-6 min-w-9" : "px-2 h-7";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* upvote button 👇 */}
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<div>
|
||||
{allUpVotes.length > 0 ? (
|
||||
<>
|
||||
{allUpVotes
|
||||
.map((r) => r.actor_details?.display_name)
|
||||
.splice(0, VOTES_LIMIT)
|
||||
.join(", ")}
|
||||
{allUpVotes.length > VOTES_LIMIT && " and " + (allUpVotes.length - VOTES_LIMIT) + " more"}
|
||||
</>
|
||||
) : (
|
||||
"No upvotes yet"
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleVote(e, 1);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border focus:outline-none bg-custom-background-100",
|
||||
votingDimensions,
|
||||
{
|
||||
"border-custom-primary-200 text-custom-primary-200": isUpVotedByUser,
|
||||
"border-custom-border-300": !isUpVotedByUser,
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-rounded !m-0 !p-0 text-base">arrow_upward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allUpVotes.length}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{/* downvote button 👇 */}
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<div>
|
||||
{allDownVotes.length > 0 ? (
|
||||
<>
|
||||
{allDownVotes
|
||||
.map((r) => r.actor_details.display_name)
|
||||
.splice(0, VOTES_LIMIT)
|
||||
.join(", ")}
|
||||
{allDownVotes.length > VOTES_LIMIT && " and " + (allDownVotes.length - VOTES_LIMIT) + " more"}
|
||||
</>
|
||||
) : (
|
||||
"No downvotes yet"
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
onClick={(e) => {
|
||||
if (isInIframe) return;
|
||||
if (user) handleVote(e, -1);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border focus:outline-none bg-custom-background-100",
|
||||
votingDimensions,
|
||||
{
|
||||
"border-red-600 text-red-600": isDownVotedByUser,
|
||||
"border-custom-border-300": !isDownVotedByUser,
|
||||
"cursor-default": isInIframe,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-rounded !m-0 !p-0 text-base">arrow_downward_alt</span>
|
||||
<span className="text-sm font-normal transition-opacity ease-in-out">{allDownVotes.length}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user