From 5e20b94a1f92db7f2d9cf55717789cdfa0553d6e Mon Sep 17 00:00:00 2001 From: yohlo Date: Fri, 19 Sep 2025 20:53:05 -0500 Subject: [PATCH] reactions SSE! --- src/app/routes/api/events.$.ts | 1 + .../matches/components/match-card.tsx | 2 - src/features/matches/queries.ts | 18 --- src/features/matches/server.ts | 2 - .../players/components/profile/index.tsx | 2 - .../reactions/components/emoji-bar.tsx | 135 ++++++++++++++---- .../reactions/components/emoji-picker.tsx | 85 ++++++----- src/features/reactions/queries.ts | 4 - src/features/teams/components/team-card.tsx | 2 - .../teams/components/team-form/index.tsx | 2 - .../components/started-tournament/header.tsx | 2 +- src/hooks/use-server-events.ts | 8 +- src/lib/events/emitter.ts | 7 +- src/lib/pocketbase/services/players.ts | 2 - 14 files changed, 173 insertions(+), 99 deletions(-) delete mode 100644 src/features/matches/queries.ts diff --git a/src/app/routes/api/events.$.ts b/src/app/routes/api/events.$.ts index 0481370..74381ef 100644 --- a/src/app/routes/api/events.$.ts +++ b/src/app/routes/api/events.$.ts @@ -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 { diff --git a/src/features/matches/components/match-card.tsx b/src/features/matches/components/match-card.tsx index 5958293..f138c30 100644 --- a/src/features/matches/components/match-card.tsx +++ b/src/features/matches/components/match-card.tsx @@ -29,8 +29,6 @@ const MatchCard = ({ match }: MatchCardProps) => { } }; - console.log(match); - return ( ['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)); \ No newline at end of file diff --git a/src/features/matches/server.ts b/src/features/matches/server.ts index 8306f69..9ba39f2 100644 --- a/src/features/matches/server.ts +++ b/src/features/matches/server.ts @@ -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, }); diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx index f5cf2e9..479022b 100644 --- a/src/features/players/components/profile/index.tsx +++ b/src/features/players/components/profile/index.tsx @@ -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", diff --git a/src/features/reactions/components/emoji-bar.tsx b/src/features/reactions/components/emoji-bar.tsx index 6f011c4..39a5f29 100644 --- a/src/features/reactions/components/emoji-bar.tsx +++ b/src/features/reactions/components/emoji-bar.tsx @@ -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(null); + const [selectedEmoji, setSelectedEmoji] = useState(null); const longPressTimeout = useRef(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 ( <> - {reactions.map((reaction) => ( + {visibleReactions.map((reaction) => ( ))} + + {hasGrouped && ( + + )} - toggleReaction.mutate({ data: { matchId, emoji } }))} /> + toggleReaction.mutate({ data: { matchId, emoji } }))} + userReactions={userReactions} + /> close()}> - - - {reactions.map((reaction) => ( - - - {reaction.emoji} - - {reaction.count} - - - - ))} - + + + + {sortedReactions.map((reaction) => ( + + ))} + + - {reactions.map((reaction) => ( - - - - ))} - + {selectedEmoji && ( + + + {selectedEmoji} +
+ + {sortedReactions.find(r => r.emoji === selectedEmoji)?.count || 0} + +
+
+ r.emoji === selectedEmoji)?.players || []} /> +
+ )} +
); diff --git a/src/features/reactions/components/emoji-picker.tsx b/src/features/reactions/components/emoji-picker.tsx index a4ce88b..74eaf46 100644 --- a/src/features/reactions/components/emoji-picker.tsx +++ b/src/features/reactions/components/emoji-picker.tsx @@ -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 = ({ - - {EMOJIS.map(({ emoji, label }) => ( - 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)', + + {EMOJIS.map(({ emoji, label }) => { + const hasReacted = userReactions.includes(emoji); + return ( + 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} - > - - {emoji} - - - ))} + }} + aria-label={label} + > + + {emoji} + + + ); + })} diff --git a/src/features/reactions/queries.ts b/src/features/reactions/queries.ts index 4b0b978..f92ee60 100644 --- a/src/features/reactions/queries.ts +++ b/src/features/reactions/queries.ts @@ -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) - }); }, }); }; \ No newline at end of file diff --git a/src/features/teams/components/team-card.tsx b/src/features/teams/components/team-card.tsx index ae415f5..e7cd00a 100644 --- a/src/features/teams/components/team-card.tsx +++ b/src/features/teams/components/team-card.tsx @@ -17,8 +17,6 @@ interface TeamCardProps { const TeamCard = ({ teamId }: TeamCardProps) => { const { data: team, error } = useTeam(teamId); - console.log(team) - if (error || !team) { return ( diff --git a/src/features/teams/components/team-form/index.tsx b/src/features/teams/components/team-form/index.tsx index 6bd189a..17d7c44 100644 --- a/src/features/teams/components/team-form/index.tsx +++ b/src/features/teams/components/team-form/index.tsx @@ -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), diff --git a/src/features/tournaments/components/started-tournament/header.tsx b/src/features/tournaments/components/started-tournament/header.tsx index ef15152..902969f 100644 --- a/src/features/tournaments/components/started-tournament/header.tsx +++ b/src/features/tournaments/components/started-tournament/header.tsx @@ -15,7 +15,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => { ); return ( - + void)[] = []; @@ -28,10 +29,13 @@ const eventHandlers: Record = { }, "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); } }; diff --git a/src/lib/events/emitter.ts b/src/lib/events/emitter.ts index c84f6b6..d4846fa 100644 --- a/src/lib/events/emitter.ts +++ b/src/lib/events/emitter.ts @@ -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; diff --git a/src/lib/pocketbase/services/players.ts b/src/lib/pocketbase/services/players.ts index da705c5..e1652b8 100644 --- a/src/lib/pocketbase/services/players.ts +++ b/src/lib/pocketbase/services/players.ts @@ -80,8 +80,6 @@ export function createPlayersService(pb: PocketBase) { }, async getPlayerMatches(playerId: string): Promise { - console.log('----------------') - console.log(playerId) const player = await pb.collection("players").getOne(playerId.trim(), { expand: "teams", });