From f99d6efaf9fc1c3685cc5d5d622f9f3355f3507e Mon Sep 17 00:00:00 2001 From: yohlo Date: Fri, 19 Sep 2025 14:08:36 -0500 Subject: [PATCH] reactions, match sse, etc --- src/app/routes/_authed/index.tsx | 7 +- src/app/routes/api/events.$.ts | 4 +- .../matches/components/match-card.tsx | 50 +++++-- src/features/matches/queries.ts | 18 +++ src/features/matches/server.ts | 123 +++++++++++++++++- .../players/components/player-list.tsx | 2 +- .../reactions/components/emoji-bar.tsx | 63 +++++---- src/features/reactions/queries.ts | 34 +++++ .../teams/components/team-profile/index.tsx | 15 ++- .../components/started-tournament/header.tsx | 67 ++++++++++ .../components/started-tournament/index.tsx | 79 +++++++++++ src/hooks/use-server-events.ts | 37 ++---- src/lib/events/emitter.ts | 8 +- src/lib/pocketbase/client.ts | 5 +- src/lib/pocketbase/services/matches.ts | 4 +- src/lib/pocketbase/services/players.ts | 4 +- src/lib/pocketbase/services/reactions.ts | 35 +++++ src/lib/pocketbase/services/teams.ts | 3 - src/lib/pocketbase/services/tournaments.ts | 6 +- src/lib/pocketbase/util/transform-types.ts | 10 ++ 20 files changed, 474 insertions(+), 100 deletions(-) create mode 100644 src/features/matches/queries.ts create mode 100644 src/features/reactions/queries.ts create mode 100644 src/features/tournaments/components/started-tournament/header.tsx create mode 100644 src/features/tournaments/components/started-tournament/index.tsx create mode 100644 src/lib/pocketbase/services/reactions.ts diff --git a/src/app/routes/_authed/index.tsx b/src/app/routes/_authed/index.tsx index 853cc4e..aa9c643 100644 --- a/src/app/routes/_authed/index.tsx +++ b/src/app/routes/_authed/index.tsx @@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries"; import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament"; import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure"; +import StartedTournament from "@/features/tournaments/components/started-tournament"; export const Route = createFileRoute("/_authed/")({ component: Home, @@ -12,7 +13,7 @@ export const Route = createFileRoute("/_authed/")({ return { tournament } }, loader: ({ context }) => ({ - withPadding: true, + withPadding: false, header: { title: context.tournament.name || "FLXN" } @@ -22,9 +23,9 @@ export const Route = createFileRoute("/_authed/")({ function Home() { const { data: tournament } = useCurrentTournament(); - if (!tournament.matches || tournament.matches.length !== 0) { + if (!tournament.matches || tournament.matches.length === 0) { return ; } - return

Started Tournament

+ return } diff --git a/src/app/routes/api/events.$.ts b/src/app/routes/api/events.$.ts index b67c086..0481370 100644 --- a/src/app/routes/api/events.$.ts +++ b/src/app/routes/api/events.$.ts @@ -9,11 +9,9 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su const stream = new ReadableStream({ start(controller) { - // Send initial connection messages const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`; controller.enqueue(new TextEncoder().encode(connectMessage)); - // Listen for events and broadcast to all connections const handleEvent = (event: ServerEvent) => { logger.info('ServerEvents | Event received', event); const message = `data: ${JSON.stringify(event)}\n\n`; @@ -25,8 +23,8 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su }; serverEvents.on("test", handleEvent); + serverEvents.on("match", handleEvent); - // Keep alive ping every 30 seconds const pingInterval = setInterval(() => { try { const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`; diff --git a/src/features/matches/components/match-card.tsx b/src/features/matches/components/match-card.tsx index 6f1fa3e..5958293 100644 --- a/src/features/matches/components/match-card.tsx +++ b/src/features/matches/components/match-card.tsx @@ -29,6 +29,8 @@ const MatchCard = ({ match }: MatchCardProps) => { } }; + console.log(match); + return ( { )} - - {match.home?.name!} - + + {match.home?.name!} + - + {match.home_cups} + + {match.home?.players.map((p) => ( + + {p.first_name} {p.last_name} + + ))} + @@ -133,7 +147,7 @@ const MatchCard = ({ match }: MatchCardProps) => { )} { {match.away?.name} - + {match.away_cups} + + {match.away?.players.map((p) => ( + + {p.first_name} {p.last_name} + + ))} + @@ -163,7 +189,7 @@ const MatchCard = ({ match }: MatchCardProps) => { border: "1px solid var(--mantine-color-default-border)", }} > - + diff --git a/src/features/matches/queries.ts b/src/features/matches/queries.ts new file mode 100644 index 0000000..7d90237 --- /dev/null +++ b/src/features/matches/queries.ts @@ -0,0 +1,18 @@ +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)); \ No newline at end of file diff --git a/src/features/matches/server.ts b/src/features/matches/server.ts index aa7ed9e..8306f69 100644 --- a/src/features/matches/server.ts +++ b/src/features/matches/server.ts @@ -6,6 +6,9 @@ import { z } from "zod"; import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result"; import brackets from "@/features/bracket/utils"; import { MatchInput } from "@/features/matches/types"; +import { serverEvents } from "@/lib/events/emitter"; +import { superTokensFunctionMiddleware } from "@/utils/supertokens"; +import { PlayerInfo } from "../players/types"; const orderedTeamsSchema = z.object({ tournamentId: z.string(), @@ -150,6 +153,13 @@ export const startMatch = createServerFn() status: "started", }); + console.log('emitting start match...') + serverEvents.emit("match", { + type: "match", + matchId: match.id, + tournamentId: match.tournament.id + }); + return match; }) ); @@ -184,14 +194,9 @@ export const endMatch = createServerFn() const matchLoser = home_cups < away_cups ? match.home : match.away; if (!matchWinner || !matchLoser) throw new Error("Something went wrong"); - console.log(matchWinner) - console.log(matchLoser) - // winner -> where to send match winner to, loser same const { winner, loser } = await pbAdmin.getChildMatches(matchId); - console.log(winner, loser) - // reset match check if (winner && winner.reset) { const awayTeamWon = match.away === matchWinner; @@ -232,8 +237,114 @@ export const endMatch = createServerFn() }); } - // TODO: send SSE + serverEvents.emit("match", { + type: "match", + matchId: match.id, + tournamentId: match.tournament.id + }); return match; }) ); + +const toggleReactionSchema = z.object({ + matchId: z.string(), + emoji: z.string(), +}); + +export const toggleMatchReaction = createServerFn() + .validator(toggleReactionSchema) + .middleware([superTokensFunctionMiddleware]) + .handler(async ({ data: { matchId, emoji }, context }) => + toServerResult(async () => { + const user = await pbAdmin.getPlayerByAuthId(context.userAuthId); + const userId = user?.id; + if (!userId) return; + + const match = await pbAdmin.getMatch(matchId); + if (!match) { + throw new Error("Match not found"); + } + + const existingReaction = await pbAdmin.getUserReaction(matchId, userId, emoji); + if (existingReaction) { + await pbAdmin.deleteReaction(existingReaction.id); + logger.info("Removed reaction", { matchId, emoji, userId }); + } else { + await pbAdmin.createReaction(matchId, userId, emoji); + logger.info("Added reaction", { matchId, emoji, userId }); + } + + const all = await pbAdmin.getReactionsForMatch(matchId); + + const reactionsByEmoji = all.reduce((acc, reaction) => { + const emoji = reaction.emoji; + if (!acc[emoji]) { + acc[emoji] = { + emoji, + count: 0, + players: [], + }; + } + acc[emoji].count++; + acc[emoji].players.push({ + id: reaction?.player?.id, + first_name: reaction.player?.first_name, + last_name: reaction.player?.last_name, + }); + return acc; + }, {} as Record); + + const reactions = Object.values(reactionsByEmoji); + + serverEvents.emit("reaction", { + type: "reaction", + matchId, + tournamentId: match.tournament.id, + reactions, + }); + + return reactions as Reaction[] + }) + ); + +export interface Reaction { + emoji: string; + count: number; + players: PlayerInfo[]; +} +export const getMatchReactions = createServerFn() + .validator(z.string()) + .middleware([superTokensFunctionMiddleware]) + .handler(async ({ data: matchId, context }) => + toServerResult(async () => { + const match = await pbAdmin.getMatch(matchId); + if (!match) { + throw new Error("Match not found"); + } + + const all = await pbAdmin.getReactionsForMatch(matchId); + + const reactionsByEmoji = all.reduce((acc, reaction) => { + const emoji = reaction.emoji; + if (!acc[emoji]) { + acc[emoji] = { + emoji, + count: 0, + players: [], + }; + } + acc[emoji].count++; + acc[emoji].players.push({ + id: reaction?.player?.id, + first_name: reaction.player?.first_name, + last_name: reaction.player?.last_name, + }); + return acc; + }, {} as Record); + + const reactions = Object.values(reactionsByEmoji); + + return reactions as Reaction[] + }) + ); diff --git a/src/features/players/components/player-list.tsx b/src/features/players/components/player-list.tsx index 5c2425d..c475c6d 100644 --- a/src/features/players/components/player-list.tsx +++ b/src/features/players/components/player-list.tsx @@ -13,7 +13,7 @@ const PlayerList = ({ players, loading = false }: PlayerListProps) => { const navigate = useNavigate(); const handleClick = useCallback((playerId: string) => - navigate({ to: `/profile/${playerId} `}), [navigate]); + navigate({ to: `/profile/${playerId}`}), [navigate]); if (loading) return {Array.from({ length: 10 }).map((_, i) => ( diff --git a/src/features/reactions/components/emoji-bar.tsx b/src/features/reactions/components/emoji-bar.tsx index 9bae1ba..6f011c4 100644 --- a/src/features/reactions/components/emoji-bar.tsx +++ b/src/features/reactions/components/emoji-bar.tsx @@ -5,43 +5,27 @@ import { Tabs, } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import { useState, useRef } from "react"; +import { useState, useRef, useCallback } from "react"; import Sheet from "@/components/sheet/sheet"; -import { PlayerInfo } from "@/features/players/types"; import PlayerList from "@/features/players/components/player-list"; import EmojiPicker from "./emoji-picker"; - -interface Reaction { - emoji: string; - count: number; - players: PlayerInfo[]; - hasReacted: boolean; -} +import { useMatchReactions, useToggleMatchReaction } from "../queries"; +import { useAuth } from "@/contexts/auth-context"; +import { Reaction } from "@/features/matches/server"; interface EmojiBarProps { - reactions?: Reaction[]; + matchId: string; onReactionPress?: (emoji: string) => void; } -const EXAMPLE_DATA: Reaction[] = [ - { - emoji: "👍", - count: 1, - players: [{ id: "dasfasdf", first_name: "Kyle", last_name: "Yohler" }], - hasReacted: true, - }, - { - emoji: "❤️", - count: 1, - players: [{ id: "f3234f", first_name: "Salah", last_name: "Atiyeh" }], - hasReacted: false, - }, -]; - const EmojiBar = ({ - reactions = EXAMPLE_DATA, + matchId, onReactionPress, }: EmojiBarProps) => { + const { user } = useAuth(); + const { data: reactions } = useMatchReactions(matchId); + const toggleReaction = useToggleMatchReaction(matchId); + const [opened, { open, close }] = useDisclosure(false); const [activeTab, setActiveTab] = useState(null); const longPressTimeout = useRef(null); @@ -61,10 +45,20 @@ const EmojiBar = ({ const handleReactionClick = (emoji: string) => { handleLongPressEnd(); - onReactionPress?.(emoji); + if (onReactionPress) { + onReactionPress(emoji); + } else { + toggleReaction.mutate({ data: { matchId, emoji } }); + } }; - if (!reactions.length) return null; + const hasReacted = useCallback((reaction: Reaction) => { + return reaction.players.map(p => p.id).includes(user?.id || ""); + }, []); + + if (!reactions) return; + + console.log(reactions) return ( <> @@ -73,8 +67,9 @@ const EmojiBar = ({ {reactions.map((reaction) => ( ))} - {})} /> + toggleReaction.mutate({ data: { matchId, emoji } }))} /> close()}> diff --git a/src/features/reactions/queries.ts b/src/features/reactions/queries.ts new file mode 100644 index 0000000..4b0b978 --- /dev/null +++ b/src/features/reactions/queries.ts @@ -0,0 +1,34 @@ +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, +}; + +export const reactionQueries = { + match: (matchId: string) => ({ + queryKey: reactionKeys.match(matchId), + queryFn: () => getMatchReactions({ data: matchId }), + }), +}; + +export const useMatchReactions = (matchId: string) => + useServerQuery(reactionQueries.match(matchId)); + +export const useToggleMatchReaction = (matchId: string) => { + const queryClient = useQueryClient(); + + return useServerMutation({ + mutationFn: toggleMatchReaction, + onSuccess: () => { + 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-profile/index.tsx b/src/features/teams/components/team-profile/index.tsx index 9e7d1eb..37889d7 100644 --- a/src/features/teams/components/team-profile/index.tsx +++ b/src/features/teams/components/team-profile/index.tsx @@ -1,10 +1,11 @@ -import { Box, Text } from "@mantine/core"; +import { Box, Divider, Text, Stack } from "@mantine/core"; import Header from "./header"; import SwipeableTabs from "@/components/swipeable-tabs"; import TournamentList from "@/features/tournaments/components/tournament-list"; import StatsOverview from "@/shared/components/stats-overview"; import { useTeam, useTeamMatches, useTeamStats } from "../../queries"; import MatchList from "@/features/matches/components/match-list"; +import PlayerList from "@/features/players/components/player-list"; interface ProfileProps { id: string; @@ -19,7 +20,17 @@ const TeamProfile = ({ id }: ProfileProps) => { const tabs = [ { label: "Overview", - content: , + content: <> + + Players + + + + + Statistics + + + , }, { label: "Matches", diff --git a/src/features/tournaments/components/started-tournament/header.tsx b/src/features/tournaments/components/started-tournament/header.tsx new file mode 100644 index 0000000..ef15152 --- /dev/null +++ b/src/features/tournaments/components/started-tournament/header.tsx @@ -0,0 +1,67 @@ +import { Group, Stack, ThemeIcon, Text, Flex } from "@mantine/core"; +import { Tournament } from "../../types"; +import Avatar from "@/components/avatar"; +import { + CalendarIcon, + MapPinIcon, + TrophyIcon, +} from "@phosphor-icons/react"; +import { useMemo } from "react"; + +const Header = ({ tournament }: { tournament: Tournament }) => { + const tournamentStart = useMemo( + () => new Date(tournament.start_time), + [tournament.start_time] + ); + + return ( + + + + + + {tournament.location && ( + + + + + + {tournament.location} + + + )} + + + + + + + {tournamentStart.toLocaleDateString(undefined, { + weekday: "short", + month: "short", + day: "numeric", + })}{" "} + at{" "} + {tournamentStart.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + + + + + ); +}; + +export default Header; \ No newline at end of file diff --git a/src/features/tournaments/components/started-tournament/index.tsx b/src/features/tournaments/components/started-tournament/index.tsx new file mode 100644 index 0000000..8bdb75a --- /dev/null +++ b/src/features/tournaments/components/started-tournament/index.tsx @@ -0,0 +1,79 @@ +import { useMemo } from "react"; +import { Tournament } from "../../types"; +import { useAuth } from "@/contexts/auth-context"; +import { Box, Divider, Stack, Text, Card, Center } from "@mantine/core"; +import { Carousel } from "@mantine/carousel"; +import ListLink from "@/components/list-link"; +import { TreeStructureIcon, UsersIcon, ClockIcon } from "@phosphor-icons/react"; +import TeamListButton from "../upcoming-tournament/team-list-button"; +import RulesListButton from "../upcoming-tournament/rules-list-button"; +import MatchCard from "@/features/matches/components/match-card"; +import Header from "./header"; + +const StartedTournament: React.FC<{ tournament: Tournament }> = ({ + tournament, +}) => { + const { roles } = useAuth(); + + const isAdmin = useMemo(() => roles.includes("Admin"), [roles]); + + const startedMatches = useMemo(() => + tournament.matches?.filter(match => match.status === "started") || [], + [tournament.matches] + ); + + return ( + +
+ + {startedMatches.length > 0 ? ( + + + {startedMatches.map((match, index) => ( + + + + + + ))} + + + ) : ( + +
+ + + + No active matches + + +
+
+ )} + + + + {isAdmin && ( + + )} + + + + + + ); +}; + +export default StartedTournament; \ No newline at end of file diff --git a/src/hooks/use-server-events.ts b/src/hooks/use-server-events.ts index b4af930..7fe36ad 100644 --- a/src/hooks/use-server-events.ts +++ b/src/hooks/use-server-events.ts @@ -2,19 +2,13 @@ import { useEffect, useRef } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Logger } from "@/lib/logger"; import { useAuth } from "@/contexts/auth-context"; -/* -import { authClient } from "~/lib/auth-client"; -import { getIdeaFn } from "~/routes/-fn/getIdeaFn"; -import { useSession } from "~/lib/sessionContext"; -*/ +import { tournamentKeys, tournamentQueries } from "@/features/tournaments/queries"; -// Global state for new ideas notification let newIdeasAvailable = false; let newIdeasCallbacks: (() => void)[] = []; const logger = new Logger('ServerEvents'); -// Event handler types for better type safety type SSEEvent = { type: string; [key: string]: any; @@ -22,29 +16,26 @@ type SSEEvent = { type EventHandler = (event: SSEEvent, queryClient: ReturnType, currentSessionId?: string) => void; -// Event handlers map - add new handlers here for easy extension const eventHandlers: Record = { "connected": () => { logger.info("ServerEvents | New Connection"); }, - "ping": () => { - // Keep-alive ping, no action needed - }, + "ping": () => {}, "test": (event, queryClient) => { + }, - - // Add new event handlers here: - // "idea-updated": (event, queryClient) => { - // queryClient.invalidateQueries({ queryKey: ["ideas"] }); - // }, - // "vote-changed": (event, queryClient) => { - // queryClient.invalidateQueries({ queryKey: ["idea", event.ideaId] }); - // }, + + "match": (event, queryClient) => { + console.log(event); + + queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId)) + queryClient.invalidateQueries(tournamentQueries.current()) + } + }; -// Functions to manage new ideas notification state export function getNewIdeasAvailable(): boolean { return newIdeasAvailable; } @@ -55,7 +46,6 @@ export function clearNewIdeasAvailable(): void { export function subscribeToNewIdeas(callback: () => void): () => void { newIdeasCallbacks.push(callback); - // Return unsubscribe function return () => { const index = newIdeasCallbacks.indexOf(callback); if (index > -1) { @@ -84,7 +74,6 @@ export function useServerEvents() { const eventSource = new EventSource(`/api/events/$`); eventSource.onopen = () => { - // Reset retry count on successful connection retryCountRef.current = 0; }; @@ -93,7 +82,6 @@ export function useServerEvents() { const data: SSEEvent = JSON.parse(event.data); logger.info("Event received", data); - // Use the event handler pattern for extensible event processing const handler = eventHandlers[data.type]; if (handler) { handler(data, queryClient, user?.id); @@ -109,13 +97,12 @@ export function useServerEvents() { logger.error("SSE connection error", error); eventSource.close(); - // Only retry if we should still be connecting and haven't exceeded max retries if (shouldConnectRef.current && retryCountRef.current < 5) { retryCountRef.current += 1; const delay = Math.min( 1000 * Math.pow(2, retryCountRef.current - 1), 30000 - ); // Cap at 30 seconds + ); logger.info( `SSE reconnection attempt ${retryCountRef.current}/5 in ${delay}ms` diff --git a/src/lib/events/emitter.ts b/src/lib/events/emitter.ts index 53e5959..c84f6b6 100644 --- a/src/lib/events/emitter.ts +++ b/src/lib/events/emitter.ts @@ -7,4 +7,10 @@ export type TestEvent = { playerId: string; }; -export type ServerEvent = TestEvent; +export type MatchEvent = { + type: "match"; + matchId: string; + tournamentId: string; +} + +export type ServerEvent = TestEvent | MatchEvent; diff --git a/src/lib/pocketbase/client.ts b/src/lib/pocketbase/client.ts index a409f72..f4e3b3f 100644 --- a/src/lib/pocketbase/client.ts +++ b/src/lib/pocketbase/client.ts @@ -3,6 +3,7 @@ import { createPlayersService } from "./services/players"; import { createTournamentsService } from "./services/tournaments"; import { createTeamsService } from "./services/teams"; import { createMatchesService } from "./services/matches"; +import { createReactionsService } from "./services/reactions"; class PocketBaseAdminClient { private pb: PocketBase; @@ -31,6 +32,7 @@ class PocketBaseAdminClient { Object.assign(this, createTeamsService(this.pb)); Object.assign(this, createTournamentsService(this.pb)); Object.assign(this, createMatchesService(this.pb)); + Object.assign(this, createReactionsService(this.pb)); }); } @@ -49,7 +51,8 @@ interface AdminClient ReturnType, ReturnType, ReturnType, - ReturnType { + ReturnType, + ReturnType { authPromise: Promise; } diff --git a/src/lib/pocketbase/services/matches.ts b/src/lib/pocketbase/services/matches.ts index de7dc04..d57721b 100644 --- a/src/lib/pocketbase/services/matches.ts +++ b/src/lib/pocketbase/services/matches.ts @@ -6,7 +6,6 @@ import { transformMatch } from "../util/transform-types"; export function createMatchesService(pb: PocketBase) { return { async getMatch(id: string): Promise { - logger.info("PocketBase | Getting match", id); const result = await pb.collection("matches").getOne(id, { expand: "tournament, home, away", }); @@ -15,7 +14,6 @@ export function createMatchesService(pb: PocketBase) { // match Ids where the current lid is home_from_lid or away_from_lid async getChildMatches(matchId: string): Promise<{ winner: Match | undefined, loser: Match | undefined }> { - logger.info("PocketBase | Getting child matches", matchId); const match = await this.getMatch(matchId); if (!match) throw new Error("Match not found") @@ -52,7 +50,7 @@ export function createMatchesService(pb: PocketBase) { async updateMatch(id: string, data: Partial): Promise { logger.info("PocketBase | Updating match", { id, data }); const result = await pb.collection("matches").update(id, data, { - expand: 'home, away' + expand: 'home, away, tournament' }); return transformMatch(result); }, diff --git a/src/lib/pocketbase/services/players.ts b/src/lib/pocketbase/services/players.ts index 940f80a..da705c5 100644 --- a/src/lib/pocketbase/services/players.ts +++ b/src/lib/pocketbase/services/players.ts @@ -80,7 +80,9 @@ export function createPlayersService(pb: PocketBase) { }, async getPlayerMatches(playerId: string): Promise { - const player = await pb.collection("players").getOne(playerId, { + console.log('----------------') + console.log(playerId) + const player = await pb.collection("players").getOne(playerId.trim(), { expand: "teams", }); diff --git a/src/lib/pocketbase/services/reactions.ts b/src/lib/pocketbase/services/reactions.ts new file mode 100644 index 0000000..2c75b2b --- /dev/null +++ b/src/lib/pocketbase/services/reactions.ts @@ -0,0 +1,35 @@ +import PocketBase from "pocketbase"; +import { transformReaction } from "../util/transform-types"; + +export const createReactionsService = (pb: PocketBase) => ({ + async getReactionsForMatch(matchId: string) { + const reactions = await pb.collection('reactions').getFullList({ + filter: `match="${matchId}"`, + expand: 'player', + }); + return reactions.map(transformReaction) + }, + + async getUserReaction(matchId: string, userId: string, emoji: string) { + try { + return await pb.collection('reactions').getFirstListItem(`match="${matchId}" && player="${userId}" && emoji="${emoji}"`); + } catch (error) { + return; + } + }, + + async createReaction(matchId: string, userId: string, emoji: string) { + const reaction = await pb.collection('reactions').create({ + match: matchId, + player: userId, + emoji: emoji, + }, { + expand: 'player' + }); + return transformReaction(reaction) + }, + + async deleteReaction(reactionId: string) { + return await pb.collection('reactions').delete(reactionId); + }, +}); \ No newline at end of file diff --git a/src/lib/pocketbase/services/teams.ts b/src/lib/pocketbase/services/teams.ts index 08ac0fc..948d980 100644 --- a/src/lib/pocketbase/services/teams.ts +++ b/src/lib/pocketbase/services/teams.ts @@ -7,7 +7,6 @@ import { Match } from "@/features/matches/types"; export function createTeamsService(pb: PocketBase) { return { async getTeamInfo(id: string): Promise { - logger.info("PocketBase | Getting team info", id); const result = await pb.collection("teams").getOne(id, { fields: "id,name,primary_color,accent_color,logo", expand: "players" @@ -25,7 +24,6 @@ export function createTeamsService(pb: PocketBase) { }, async getTeam(id: string): Promise { - logger.info("PocketBase | Getting team", id); const result = await pb.collection("teams").getOne(id, { expand: "players, tournaments", }); @@ -84,7 +82,6 @@ export function createTeamsService(pb: PocketBase) { }, async getTeamStats(id: string): Promise { - logger.info("PocketBase | Getting team stats", id); try { const result = await pb.collection("team_stats").getFirstListItem(`team_id="${id}"`); return result as unknown as TeamStats; diff --git a/src/lib/pocketbase/services/tournaments.ts b/src/lib/pocketbase/services/tournaments.ts index d4d4619..9e00a5a 100644 --- a/src/lib/pocketbase/services/tournaments.ts +++ b/src/lib/pocketbase/services/tournaments.ts @@ -23,7 +23,7 @@ export function createTournamentsService(pb: PocketBase) { .collection("tournaments") .getFirstListItem('', { - expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away", + expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away, matches.home.players, matches.away.players", sort: "-created", } ); @@ -104,10 +104,6 @@ export function createTournamentsService(pb: PocketBase) { }, async getUnenrolledTeams(tournamentId: string): Promise { try { - logger.info( - "PocketBase | Getting unenrolled teams for tournament", - tournamentId - ); const tournament = await pb .collection("tournaments") .getOne(tournamentId, { diff --git a/src/lib/pocketbase/util/transform-types.ts b/src/lib/pocketbase/util/transform-types.ts index c7789e7..31b950d 100644 --- a/src/lib/pocketbase/util/transform-types.ts +++ b/src/lib/pocketbase/util/transform-types.ts @@ -1,3 +1,4 @@ +import { Reaction } from "@/features/matches/server"; import { Match } from "@/features/matches/types"; import { Player, PlayerInfo } from "@/features/players/types"; import { Team, TeamInfo } from "@/features/teams/types"; @@ -149,3 +150,12 @@ export function transformTournament(record: any): Tournament { matches }; } + +export function transformReaction(record: any) { + return { + id: record.id, + emoji: record.emoji, + player: transformPlayerInfo(record.expand.player), + match: record.match + }; +}