reactions SSE!
This commit is contained in:
@@ -24,6 +24,7 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
|
||||
|
||||
serverEvents.on("test", handleEvent);
|
||||
serverEvents.on("match", handleEvent);
|
||||
serverEvents.on("reaction", handleEvent);
|
||||
|
||||
const pingInterval = setInterval(() => {
|
||||
try {
|
||||
|
||||
@@ -29,8 +29,6 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
console.log(match);
|
||||
|
||||
return (
|
||||
<Indicator
|
||||
disabled={!isStarted}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useServerSuspenseQuery, useServerQuery } from "@/lib/tanstack-query/hooks";
|
||||
import { getMatchReactions } from "./server";
|
||||
|
||||
export const matchKeys = {
|
||||
list: ['matches', 'list'] as const,
|
||||
details: (id: string) => ['matches', 'details', id] as const,
|
||||
reactions: (id: string) => ['matches', 'reactions', id] as const,
|
||||
};
|
||||
|
||||
export const matchQueries = {
|
||||
reactions: (matchId: string) => ({
|
||||
queryKey: matchKeys.reactions(matchId),
|
||||
queryFn: () => getMatchReactions({ data: matchId }),
|
||||
}),
|
||||
};
|
||||
|
||||
export const useMatchReactions = (matchId: string) =>
|
||||
useServerQuery(matchQueries.reactions(matchId));
|
||||
@@ -153,7 +153,6 @@ export const startMatch = createServerFn()
|
||||
status: "started",
|
||||
});
|
||||
|
||||
console.log('emitting start match...')
|
||||
serverEvents.emit("match", {
|
||||
type: "match",
|
||||
matchId: match.id,
|
||||
@@ -300,7 +299,6 @@ export const toggleMatchReaction = createServerFn()
|
||||
serverEvents.emit("reaction", {
|
||||
type: "reaction",
|
||||
matchId,
|
||||
tournamentId: match.tournament.id,
|
||||
reactions,
|
||||
});
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ const Profile = ({ id }: ProfileProps) => {
|
||||
const { data: matches } = usePlayerMatches(id);
|
||||
const { data: stats, isLoading: statsLoading } = usePlayerStats(id);
|
||||
|
||||
console.log(player.teams)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Overview",
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -17,8 +17,6 @@ interface TeamCardProps {
|
||||
const TeamCard = ({ teamId }: TeamCardProps) => {
|
||||
const { data: team, error } = useTeam(teamId);
|
||||
|
||||
console.log(team)
|
||||
|
||||
if (error || !team) {
|
||||
return (
|
||||
<Paper p="sm" withBorder radius="md">
|
||||
|
||||
@@ -106,7 +106,6 @@ const TeamForm = ({
|
||||
|
||||
mutation(teamData, {
|
||||
onSuccess: async (team: any) => {
|
||||
console.log(team)
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.list });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: teamKeys.details(team.id),
|
||||
@@ -130,7 +129,6 @@ const TeamForm = ({
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
console.log('here for some reason', result)
|
||||
|
||||
queryClient.setQueryData(
|
||||
tournamentKeys.details(result.team!.id),
|
||||
|
||||
@@ -15,7 +15,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack align="center" gap={0}>
|
||||
<Stack px="sm" align="center" gap={0}>
|
||||
<Avatar
|
||||
name={tournament.name}
|
||||
src={
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Logger } from "@/lib/logger";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { tournamentKeys, tournamentQueries } from "@/features/tournaments/queries";
|
||||
import { reactionKeys, reactionQueries } from "@/features/reactions/queries";
|
||||
|
||||
let newIdeasAvailable = false;
|
||||
let newIdeasCallbacks: (() => void)[] = [];
|
||||
@@ -28,10 +29,13 @@ const eventHandlers: Record<string, EventHandler> = {
|
||||
},
|
||||
|
||||
"match": (event, queryClient) => {
|
||||
console.log(event);
|
||||
|
||||
queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId))
|
||||
queryClient.invalidateQueries(tournamentQueries.current())
|
||||
},
|
||||
|
||||
"reaction": (event, queryClient) => {
|
||||
queryClient.invalidateQueries(reactionQueries.match(event.matchId));
|
||||
queryClient.setQueryData(reactionKeys.match(event.matchId), () => event.reactions);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -13,4 +13,9 @@ export type MatchEvent = {
|
||||
tournamentId: string;
|
||||
}
|
||||
|
||||
export type ServerEvent = TestEvent | MatchEvent;
|
||||
export type ReactionEvent = {
|
||||
type: "reaction";
|
||||
matchId: string;
|
||||
}
|
||||
|
||||
export type ServerEvent = TestEvent | MatchEvent | ReactionEvent;
|
||||
|
||||
@@ -80,8 +80,6 @@ export function createPlayersService(pb: PocketBase) {
|
||||
},
|
||||
|
||||
async getPlayerMatches(playerId: string): Promise<Match[]> {
|
||||
console.log('----------------')
|
||||
console.log(playerId)
|
||||
const player = await pb.collection("players").getOne(playerId.trim(), {
|
||||
expand: "teams",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user