reactions SSE!
This commit is contained in:
@@ -2,7 +2,9 @@ import {
|
||||
Group,
|
||||
Button,
|
||||
Text,
|
||||
Tabs,
|
||||
Stack,
|
||||
ScrollArea,
|
||||
Paper,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
@@ -27,12 +29,12 @@ const EmojiBar = ({
|
||||
const toggleReaction = useToggleMatchReaction(matchId);
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(null);
|
||||
const [selectedEmoji, setSelectedEmoji] = useState<string | null>(null);
|
||||
const longPressTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleLongPressStart = (emoji: string) => {
|
||||
longPressTimeout.current = setTimeout(() => {
|
||||
setActiveTab(emoji);
|
||||
setSelectedEmoji(emoji);
|
||||
open();
|
||||
}, 500);
|
||||
};
|
||||
@@ -54,17 +56,29 @@ const EmojiBar = ({
|
||||
|
||||
const hasReacted = useCallback((reaction: Reaction) => {
|
||||
return reaction.players.map(p => p.id).includes(user?.id || "");
|
||||
}, []);
|
||||
}, [user?.id]);
|
||||
|
||||
// Get emojis the current user has reacted to
|
||||
const userReactions = reactions?.filter(r => hasReacted(r)).map(r => r.emoji) || [];
|
||||
|
||||
if (!reactions) return;
|
||||
|
||||
console.log(reactions)
|
||||
// Sort reactions by count (descending)
|
||||
const sortedReactions = [...reactions].sort((a, b) => b.count - a.count);
|
||||
|
||||
// Group reactions: show first 3, group the rest
|
||||
const visibleReactions = sortedReactions.slice(0, 3);
|
||||
const groupedReactions = sortedReactions.slice(3);
|
||||
|
||||
const hasGrouped = groupedReactions.length > 0;
|
||||
const groupedCount = groupedReactions.reduce((sum, r) => sum + r.count, 0);
|
||||
const userHasReactedToGrouped = groupedReactions.some(r => hasReacted(r));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group gap="xs" wrap="wrap" justify="space-between">
|
||||
<Group gap="xs" wrap="wrap">
|
||||
{reactions.map((reaction) => (
|
||||
{visibleReactions.map((reaction) => (
|
||||
<Button
|
||||
key={reaction.emoji}
|
||||
variant={hasReacted(reaction) ? "filled" : "light"}
|
||||
@@ -95,31 +109,100 @@ const EmojiBar = ({
|
||||
</Group>
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{hasGrouped && (
|
||||
<Button
|
||||
variant={userHasReactedToGrouped ? "filled" : "light"}
|
||||
color="gray"
|
||||
bd={userHasReactedToGrouped ? "1px solid var(--mantine-primary-color-filled)" : undefined}
|
||||
size="compact-xs"
|
||||
radius="xl"
|
||||
onMouseDown={() => handleLongPressStart(groupedReactions[0]?.emoji || "")}
|
||||
onMouseUp={handleLongPressEnd}
|
||||
onMouseLeave={handleLongPressEnd}
|
||||
onTouchStart={() => handleLongPressStart(groupedReactions[0]?.emoji || "")}
|
||||
onTouchEnd={handleLongPressEnd}
|
||||
onClick={() => {
|
||||
setSelectedEmoji(groupedReactions[0]?.emoji || "");
|
||||
open();
|
||||
}}
|
||||
style={{
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Group gap={2} align="center">
|
||||
<div style={{
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
alignItems: "center",
|
||||
fontSize: "10px",
|
||||
lineHeight: 1
|
||||
}}>
|
||||
{groupedReactions.slice(0, 2).map((reaction) => (
|
||||
<span key={reaction.emoji}>{reaction.emoji}</span>
|
||||
))}
|
||||
{groupedReactions.length > 2 && (
|
||||
<Text size="8px" fw={600} c="dimmed">
|
||||
+{groupedReactions.length - 2}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Text size="xs" fw={600}>
|
||||
{groupedCount}
|
||||
</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
<EmojiPicker onSelect={onReactionPress || ((emoji) => toggleReaction.mutate({ data: { matchId, emoji } }))} />
|
||||
<EmojiPicker
|
||||
onSelect={onReactionPress || ((emoji) => toggleReaction.mutate({ data: { matchId, emoji } }))}
|
||||
userReactions={userReactions}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Sheet title="Reactions" opened={opened} onChange={() => close()}>
|
||||
<Tabs value={activeTab || reactions[0]?.emoji} onChange={setActiveTab}>
|
||||
<Tabs.List grow>
|
||||
{reactions.map((reaction) => (
|
||||
<Tabs.Tab key={reaction.emoji} value={reaction.emoji}>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="lg">{reaction.emoji}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{reaction.count}
|
||||
</Text>
|
||||
</Group>
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
<Stack gap="md">
|
||||
<ScrollArea w="100%" offsetScrollbars>
|
||||
<Group gap="xs" wrap="nowrap" px="xs">
|
||||
{sortedReactions.map((reaction) => (
|
||||
<Button
|
||||
key={reaction.emoji}
|
||||
variant={selectedEmoji === reaction.emoji ? "filled" : "light"}
|
||||
color="gray"
|
||||
size="compact-sm"
|
||||
radius="xl"
|
||||
onClick={() => setSelectedEmoji(reaction.emoji)}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Group gap={4} align="center">
|
||||
<Text size="sm">{reaction.emoji}</Text>
|
||||
<Text size="xs" fw={600}>
|
||||
{reaction.count}
|
||||
</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
|
||||
{reactions.map((reaction) => (
|
||||
<Tabs.Panel key={reaction.emoji} value={reaction.emoji} pt="md">
|
||||
<PlayerList players={reaction.players} />
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
{selectedEmoji && (
|
||||
<Paper p="md" withBorder radius="md">
|
||||
<Group gap="sm" mb="md">
|
||||
<Text size="2xl">{selectedEmoji}</Text>
|
||||
<div>
|
||||
<Text size="lg" fw={600}>
|
||||
{sortedReactions.find(r => r.emoji === selectedEmoji)?.count || 0}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<PlayerList players={sortedReactions.find(r => r.emoji === selectedEmoji)?.players || []} />
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,20 +5,28 @@ import { useState } from "react";
|
||||
interface EmojiPickerProps {
|
||||
onSelect: (emoji: string) => void;
|
||||
disabled?: boolean;
|
||||
userReactions?: string[];
|
||||
}
|
||||
|
||||
const EMOJIS = [
|
||||
{ emoji: "😊", label: "smile" },
|
||||
{ emoji: "😢", label: "cry" },
|
||||
{ emoji: "🫡", label: "salute" },
|
||||
{ emoji: "😭", label: "crying" },
|
||||
{ emoji: "🫦", label: "lip" },
|
||||
{ emoji: "🏗️", label: "crane" },
|
||||
{ emoji: "👀", label: "eyes" },
|
||||
{ emoji: "🔥", label: "fire" },
|
||||
{ emoji: "❤️", label: "heart" },
|
||||
{ emoji: "👑", label: "crown" },
|
||||
{ emoji: "😱", label: "scream" },
|
||||
{ emoji: "🥹", label: "owo" },
|
||||
{ emoji: "🤣", label: "rofl" },
|
||||
{ emoji: "🤪", label: "crazy" },
|
||||
{ emoji: "🤓", label: "nerd" },
|
||||
{ emoji: "🥵", label: "hot" },
|
||||
{ emoji: "🥶", label: "cold" },
|
||||
];
|
||||
|
||||
const EmojiPicker = ({
|
||||
onSelect,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
userReactions = []
|
||||
}: EmojiPickerProps) => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
@@ -50,36 +58,43 @@ const EmojiPicker = ({
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown p="xs">
|
||||
<SimpleGrid cols={6} spacing={0}>
|
||||
{EMOJIS.map(({ emoji, label }) => (
|
||||
<UnstyledButton
|
||||
key={emoji}
|
||||
onClick={() => handleEmojiSelect(emoji)}
|
||||
style={{
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 36,
|
||||
minWidth: 36,
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mantine-color-gray-1)',
|
||||
<SimpleGrid cols={6} spacing={4}>
|
||||
{EMOJIS.map(({ emoji, label }) => {
|
||||
const hasReacted = userReactions.includes(emoji);
|
||||
return (
|
||||
<UnstyledButton
|
||||
key={emoji}
|
||||
onClick={() => handleEmojiSelect(emoji)}
|
||||
style={{
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 36,
|
||||
minWidth: 36,
|
||||
backgroundColor: hasReacted ? 'var(--mantine-primary-color-light)' : undefined,
|
||||
border: hasReacted ? '1px solid var(--mantine-primary-color-filled)' : undefined,
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
'&:hover': {
|
||||
backgroundColor: hasReacted
|
||||
? 'var(--mantine-primary-color-light-hover)'
|
||||
: 'var(--mantine-color-gray-1)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
aria-label={label}
|
||||
>
|
||||
<Text size="lg" style={{ lineHeight: 1 }}>
|
||||
{emoji}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
}}
|
||||
aria-label={label}
|
||||
>
|
||||
<Text size="lg" style={{ lineHeight: 1 }}>
|
||||
{emoji}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useServerQuery, useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { getMatchReactions, toggleMatchReaction } from "@/features/matches/server";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { matchKeys } from "@/features/matches/queries";
|
||||
|
||||
export const reactionKeys = {
|
||||
match: (matchId: string) => ['reactions', 'match', matchId] as const,
|
||||
@@ -26,9 +25,6 @@ export const useToggleMatchReaction = (matchId: string) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: reactionKeys.match(matchId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: matchKeys.reactions(matchId)
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user