reactions, match sse, etc
This commit is contained in:
@@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
||||||
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
|
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/")({
|
export const Route = createFileRoute("/_authed/")({
|
||||||
component: Home,
|
component: Home,
|
||||||
@@ -12,7 +13,7 @@ export const Route = createFileRoute("/_authed/")({
|
|||||||
return { tournament }
|
return { tournament }
|
||||||
},
|
},
|
||||||
loader: ({ context }) => ({
|
loader: ({ context }) => ({
|
||||||
withPadding: true,
|
withPadding: false,
|
||||||
header: {
|
header: {
|
||||||
title: context.tournament.name || "FLXN"
|
title: context.tournament.name || "FLXN"
|
||||||
}
|
}
|
||||||
@@ -22,9 +23,9 @@ export const Route = createFileRoute("/_authed/")({
|
|||||||
function Home() {
|
function Home() {
|
||||||
const { data: tournament } = useCurrentTournament();
|
const { data: tournament } = useCurrentTournament();
|
||||||
|
|
||||||
if (!tournament.matches || tournament.matches.length !== 0) {
|
if (!tournament.matches || tournament.matches.length === 0) {
|
||||||
return <UpcomingTournament tournament={tournament} />;
|
return <UpcomingTournament tournament={tournament} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p>Started Tournament</p>
|
return <StartedTournament tournament={tournament} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
|
|||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
// Send initial connection messages
|
|
||||||
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
|
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
|
||||||
controller.enqueue(new TextEncoder().encode(connectMessage));
|
controller.enqueue(new TextEncoder().encode(connectMessage));
|
||||||
|
|
||||||
// Listen for events and broadcast to all connections
|
|
||||||
const handleEvent = (event: ServerEvent) => {
|
const handleEvent = (event: ServerEvent) => {
|
||||||
logger.info('ServerEvents | Event received', event);
|
logger.info('ServerEvents | Event received', event);
|
||||||
const message = `data: ${JSON.stringify(event)}\n\n`;
|
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("test", handleEvent);
|
||||||
|
serverEvents.on("match", handleEvent);
|
||||||
|
|
||||||
// Keep alive ping every 30 seconds
|
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`;
|
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(match);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Indicator
|
<Indicator
|
||||||
disabled={!isStarted}
|
disabled={!isStarted}
|
||||||
@@ -89,7 +91,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Text
|
<Text
|
||||||
size="md"
|
size="sm"
|
||||||
fw={600}
|
fw={600}
|
||||||
lineClamp={1}
|
lineClamp={1}
|
||||||
style={{ minWidth: 0, flex: 1 }}
|
style={{ minWidth: 0, flex: 1 }}
|
||||||
@@ -97,9 +99,21 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
|||||||
{match.home?.name!}
|
{match.home?.name!}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="xl" fw={700} c={"dimmed"}>
|
<Text
|
||||||
|
size="xl"
|
||||||
|
fw={700}
|
||||||
|
c={"dimmed"}
|
||||||
|
display={match.status === "ended" ? undefined : "none"}
|
||||||
|
>
|
||||||
{match.home_cups}
|
{match.home_cups}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Stack gap={1}>
|
||||||
|
{match.home?.players.map((p) => (
|
||||||
|
<Text size="xs" fw={600} c="dimmed" ta="right">
|
||||||
|
{p.first_name} {p.last_name}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
@@ -133,7 +147,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Text
|
<Text
|
||||||
size="md"
|
size="sm"
|
||||||
fw={600}
|
fw={600}
|
||||||
lineClamp={1}
|
lineClamp={1}
|
||||||
style={{ minWidth: 0, flex: 1 }}
|
style={{ minWidth: 0, flex: 1 }}
|
||||||
@@ -141,9 +155,21 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
|||||||
{match.away?.name}
|
{match.away?.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="xl" fw={700} c={"dimmed"}>
|
<Text
|
||||||
|
size="xl"
|
||||||
|
fw={700}
|
||||||
|
c={"dimmed"}
|
||||||
|
display={match.status === "ended" ? undefined : "none"}
|
||||||
|
>
|
||||||
{match.away_cups}
|
{match.away_cups}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Stack gap={1}>
|
||||||
|
{match.away?.players.map((p) => (
|
||||||
|
<Text size="xs" fw={600} c="dimmed" ta="right">
|
||||||
|
{p.first_name} {p.last_name}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -163,7 +189,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
|||||||
border: "1px solid var(--mantine-color-default-border)",
|
border: "1px solid var(--mantine-color-default-border)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EmojiBar />
|
<EmojiBar matchId={match.id} />
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Indicator>
|
</Indicator>
|
||||||
|
|||||||
18
src/features/matches/queries.ts
Normal file
18
src/features/matches/queries.ts
Normal file
@@ -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));
|
||||||
@@ -6,6 +6,9 @@ import { z } from "zod";
|
|||||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||||
import brackets from "@/features/bracket/utils";
|
import brackets from "@/features/bracket/utils";
|
||||||
import { MatchInput } from "@/features/matches/types";
|
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({
|
const orderedTeamsSchema = z.object({
|
||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
@@ -150,6 +153,13 @@ export const startMatch = createServerFn()
|
|||||||
status: "started",
|
status: "started",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('emitting start match...')
|
||||||
|
serverEvents.emit("match", {
|
||||||
|
type: "match",
|
||||||
|
matchId: match.id,
|
||||||
|
tournamentId: match.tournament.id
|
||||||
|
});
|
||||||
|
|
||||||
return match;
|
return match;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -184,14 +194,9 @@ export const endMatch = createServerFn()
|
|||||||
const matchLoser = home_cups < away_cups ? match.home : match.away;
|
const matchLoser = home_cups < away_cups ? match.home : match.away;
|
||||||
if (!matchWinner || !matchLoser) throw new Error("Something went wrong");
|
if (!matchWinner || !matchLoser) throw new Error("Something went wrong");
|
||||||
|
|
||||||
console.log(matchWinner)
|
|
||||||
console.log(matchLoser)
|
|
||||||
|
|
||||||
// winner -> where to send match winner to, loser same
|
// winner -> where to send match winner to, loser same
|
||||||
const { winner, loser } = await pbAdmin.getChildMatches(matchId);
|
const { winner, loser } = await pbAdmin.getChildMatches(matchId);
|
||||||
|
|
||||||
console.log(winner, loser)
|
|
||||||
|
|
||||||
// reset match check
|
// reset match check
|
||||||
if (winner && winner.reset) {
|
if (winner && winner.reset) {
|
||||||
const awayTeamWon = match.away === matchWinner;
|
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;
|
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<string, any>);
|
||||||
|
|
||||||
|
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<string, any>);
|
||||||
|
|
||||||
|
const reactions = Object.values(reactionsByEmoji);
|
||||||
|
|
||||||
|
return reactions as Reaction[]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -5,43 +5,27 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef, useCallback } from "react";
|
||||||
import Sheet from "@/components/sheet/sheet";
|
import Sheet from "@/components/sheet/sheet";
|
||||||
import { PlayerInfo } from "@/features/players/types";
|
|
||||||
import PlayerList from "@/features/players/components/player-list";
|
import PlayerList from "@/features/players/components/player-list";
|
||||||
import EmojiPicker from "./emoji-picker";
|
import EmojiPicker from "./emoji-picker";
|
||||||
|
import { useMatchReactions, useToggleMatchReaction } from "../queries";
|
||||||
interface Reaction {
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
emoji: string;
|
import { Reaction } from "@/features/matches/server";
|
||||||
count: number;
|
|
||||||
players: PlayerInfo[];
|
|
||||||
hasReacted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EmojiBarProps {
|
interface EmojiBarProps {
|
||||||
reactions?: Reaction[];
|
matchId: string;
|
||||||
onReactionPress?: (emoji: string) => void;
|
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 = ({
|
const EmojiBar = ({
|
||||||
reactions = EXAMPLE_DATA,
|
matchId,
|
||||||
onReactionPress,
|
onReactionPress,
|
||||||
}: EmojiBarProps) => {
|
}: EmojiBarProps) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { data: reactions } = useMatchReactions(matchId);
|
||||||
|
const toggleReaction = useToggleMatchReaction(matchId);
|
||||||
|
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(null);
|
const [activeTab, setActiveTab] = useState<string | null>(null);
|
||||||
const longPressTimeout = useRef<NodeJS.Timeout | null>(null);
|
const longPressTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -61,10 +45,20 @@ const EmojiBar = ({
|
|||||||
|
|
||||||
const handleReactionClick = (emoji: string) => {
|
const handleReactionClick = (emoji: string) => {
|
||||||
handleLongPressEnd();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -73,8 +67,9 @@ const EmojiBar = ({
|
|||||||
{reactions.map((reaction) => (
|
{reactions.map((reaction) => (
|
||||||
<Button
|
<Button
|
||||||
key={reaction.emoji}
|
key={reaction.emoji}
|
||||||
variant={reaction.hasReacted ? "filled" : "light"}
|
variant={hasReacted(reaction) ? "filled" : "light"}
|
||||||
color={reaction.hasReacted ? "var(--mantine-primary-color-filled)" : "gray"}
|
color="gray"
|
||||||
|
bd={hasReacted(reaction) ? "1px solid var(--mantine-primary-color-filled)" : undefined}
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onMouseDown={() => handleLongPressStart(reaction.emoji)}
|
onMouseDown={() => handleLongPressStart(reaction.emoji)}
|
||||||
@@ -90,18 +85,18 @@ const EmojiBar = ({
|
|||||||
msUserSelect: "none",
|
msUserSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group gap={4} align="center">
|
<Group gap={2} align="center">
|
||||||
<Text size="10px" style={{ lineHeight: 1 }}>
|
<Text size="xs" style={{ lineHeight: 1 }}>
|
||||||
{reaction.emoji}
|
{reaction.emoji}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="10px" fw={600}>
|
<Text size="xs" fw={600}>
|
||||||
{reaction.count}
|
{reaction.count}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
<EmojiPicker onSelect={onReactionPress || (() => {})} />
|
<EmojiPicker onSelect={onReactionPress || ((emoji) => toggleReaction.mutate({ data: { matchId, emoji } }))} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Sheet title="Reactions" opened={opened} onChange={() => close()}>
|
<Sheet title="Reactions" opened={opened} onChange={() => close()}>
|
||||||
|
|||||||
34
src/features/reactions/queries.ts
Normal file
34
src/features/reactions/queries.ts
Normal file
@@ -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)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Box, Text } from "@mantine/core";
|
import { Box, Divider, Text, Stack } from "@mantine/core";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
import SwipeableTabs from "@/components/swipeable-tabs";
|
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||||
import TournamentList from "@/features/tournaments/components/tournament-list";
|
import TournamentList from "@/features/tournaments/components/tournament-list";
|
||||||
import StatsOverview from "@/shared/components/stats-overview";
|
import StatsOverview from "@/shared/components/stats-overview";
|
||||||
import { useTeam, useTeamMatches, useTeamStats } from "../../queries";
|
import { useTeam, useTeamMatches, useTeamStats } from "../../queries";
|
||||||
import MatchList from "@/features/matches/components/match-list";
|
import MatchList from "@/features/matches/components/match-list";
|
||||||
|
import PlayerList from "@/features/players/components/player-list";
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,7 +20,17 @@ const TeamProfile = ({ id }: ProfileProps) => {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Overview",
|
label: "Overview",
|
||||||
content: <StatsOverview statsData={statsError ? null : stats || null} isLoading={statsLoading} />,
|
content: <>
|
||||||
|
<Stack px="md">
|
||||||
|
<Text size="md" fw={700}>Players</Text>
|
||||||
|
<PlayerList players={team.players} />
|
||||||
|
</Stack>
|
||||||
|
<Divider my="md" />
|
||||||
|
<Stack>
|
||||||
|
<Text px="md" size="md" fw={700}>Statistics</Text>
|
||||||
|
<StatsOverview statsData={statsError ? null : stats || null} isLoading={statsLoading} />
|
||||||
|
</Stack>
|
||||||
|
</>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Matches",
|
label: "Matches",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Stack align="center" gap={0}>
|
||||||
|
<Avatar
|
||||||
|
name={tournament.name}
|
||||||
|
src={
|
||||||
|
tournament.logo
|
||||||
|
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
radius="md"
|
||||||
|
size={200}
|
||||||
|
px="xs"
|
||||||
|
withBorder={false}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={24} />
|
||||||
|
</Avatar>
|
||||||
|
<Flex gap="xs" direction="row" wrap="wrap" justify="space-around">
|
||||||
|
{tournament.location && (
|
||||||
|
<Group gap="xs">
|
||||||
|
<ThemeIcon size="sm" variant="light" radius="sm">
|
||||||
|
<MapPinIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{tournament.location}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<ThemeIcon size="sm" variant="light" radius="sm">
|
||||||
|
<CalendarIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{tournamentStart.toLocaleDateString(undefined, {
|
||||||
|
weekday: "short",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}{" "}
|
||||||
|
at{" "}
|
||||||
|
{tournamentStart.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@@ -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 (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Header tournament={tournament} />
|
||||||
|
|
||||||
|
{startedMatches.length > 0 ? (
|
||||||
|
<Box>
|
||||||
|
<Carousel
|
||||||
|
slideSize="95%"
|
||||||
|
slideGap="xs"
|
||||||
|
withControls={false}
|
||||||
|
>
|
||||||
|
{startedMatches.map((match, index) => (
|
||||||
|
<Carousel.Slide key={match.id}>
|
||||||
|
<Box pl={index === 0 ? "xl" : undefined } pr={index === startedMatches.length - 1 ? "xl" : undefined}>
|
||||||
|
<MatchCard match={match} />
|
||||||
|
</Box>
|
||||||
|
</Carousel.Slide>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Card withBorder radius="lg" p="xl" mx="md">
|
||||||
|
<Center>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<ClockIcon size={48} color="var(--mantine-color-dimmed)" />
|
||||||
|
<Text size="lg" fw={500} c="dimmed">
|
||||||
|
No active matches
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Divider />
|
||||||
|
{isAdmin && (
|
||||||
|
<ListLink
|
||||||
|
label={`Manage ${tournament.name}`}
|
||||||
|
to={`/admin/tournaments/${tournament.id}`}
|
||||||
|
Icon={UsersIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ListLink
|
||||||
|
label={`View Bracket`}
|
||||||
|
to={`/tournaments/${tournament.id}/bracket`}
|
||||||
|
Icon={TreeStructureIcon}
|
||||||
|
/>
|
||||||
|
<RulesListButton tournamentId={tournament.id} />
|
||||||
|
<TeamListButton teams={tournament.teams || []} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartedTournament;
|
||||||
@@ -2,19 +2,13 @@ import { useEffect, useRef } from "react";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Logger } from "@/lib/logger";
|
import { Logger } from "@/lib/logger";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
/*
|
import { tournamentKeys, tournamentQueries } from "@/features/tournaments/queries";
|
||||||
import { authClient } from "~/lib/auth-client";
|
|
||||||
import { getIdeaFn } from "~/routes/-fn/getIdeaFn";
|
|
||||||
import { useSession } from "~/lib/sessionContext";
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Global state for new ideas notification
|
|
||||||
let newIdeasAvailable = false;
|
let newIdeasAvailable = false;
|
||||||
let newIdeasCallbacks: (() => void)[] = [];
|
let newIdeasCallbacks: (() => void)[] = [];
|
||||||
|
|
||||||
const logger = new Logger('ServerEvents');
|
const logger = new Logger('ServerEvents');
|
||||||
|
|
||||||
// Event handler types for better type safety
|
|
||||||
type SSEEvent = {
|
type SSEEvent = {
|
||||||
type: string;
|
type: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@@ -22,29 +16,26 @@ type SSEEvent = {
|
|||||||
|
|
||||||
type EventHandler = (event: SSEEvent, queryClient: ReturnType<typeof useQueryClient>, currentSessionId?: string) => void;
|
type EventHandler = (event: SSEEvent, queryClient: ReturnType<typeof useQueryClient>, currentSessionId?: string) => void;
|
||||||
|
|
||||||
// Event handlers map - add new handlers here for easy extension
|
|
||||||
const eventHandlers: Record<string, EventHandler> = {
|
const eventHandlers: Record<string, EventHandler> = {
|
||||||
"connected": () => {
|
"connected": () => {
|
||||||
logger.info("ServerEvents | New Connection");
|
logger.info("ServerEvents | New Connection");
|
||||||
},
|
},
|
||||||
|
|
||||||
"ping": () => {
|
"ping": () => {},
|
||||||
// Keep-alive ping, no action needed
|
|
||||||
},
|
|
||||||
|
|
||||||
"test": (event, queryClient) => {
|
"test": (event, queryClient) => {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Add new event handlers here:
|
"match": (event, queryClient) => {
|
||||||
// "idea-updated": (event, queryClient) => {
|
console.log(event);
|
||||||
// queryClient.invalidateQueries({ queryKey: ["ideas"] });
|
|
||||||
// },
|
queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId))
|
||||||
// "vote-changed": (event, queryClient) => {
|
queryClient.invalidateQueries(tournamentQueries.current())
|
||||||
// queryClient.invalidateQueries({ queryKey: ["idea", event.ideaId] });
|
}
|
||||||
// },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Functions to manage new ideas notification state
|
|
||||||
export function getNewIdeasAvailable(): boolean {
|
export function getNewIdeasAvailable(): boolean {
|
||||||
return newIdeasAvailable;
|
return newIdeasAvailable;
|
||||||
}
|
}
|
||||||
@@ -55,7 +46,6 @@ export function clearNewIdeasAvailable(): void {
|
|||||||
|
|
||||||
export function subscribeToNewIdeas(callback: () => void): () => void {
|
export function subscribeToNewIdeas(callback: () => void): () => void {
|
||||||
newIdeasCallbacks.push(callback);
|
newIdeasCallbacks.push(callback);
|
||||||
// Return unsubscribe function
|
|
||||||
return () => {
|
return () => {
|
||||||
const index = newIdeasCallbacks.indexOf(callback);
|
const index = newIdeasCallbacks.indexOf(callback);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@@ -84,7 +74,6 @@ export function useServerEvents() {
|
|||||||
const eventSource = new EventSource(`/api/events/$`);
|
const eventSource = new EventSource(`/api/events/$`);
|
||||||
|
|
||||||
eventSource.onopen = () => {
|
eventSource.onopen = () => {
|
||||||
// Reset retry count on successful connection
|
|
||||||
retryCountRef.current = 0;
|
retryCountRef.current = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,7 +82,6 @@ export function useServerEvents() {
|
|||||||
const data: SSEEvent = JSON.parse(event.data);
|
const data: SSEEvent = JSON.parse(event.data);
|
||||||
logger.info("Event received", data);
|
logger.info("Event received", data);
|
||||||
|
|
||||||
// Use the event handler pattern for extensible event processing
|
|
||||||
const handler = eventHandlers[data.type];
|
const handler = eventHandlers[data.type];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
handler(data, queryClient, user?.id);
|
handler(data, queryClient, user?.id);
|
||||||
@@ -109,13 +97,12 @@ export function useServerEvents() {
|
|||||||
logger.error("SSE connection error", error);
|
logger.error("SSE connection error", error);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
|
|
||||||
// Only retry if we should still be connecting and haven't exceeded max retries
|
|
||||||
if (shouldConnectRef.current && retryCountRef.current < 5) {
|
if (shouldConnectRef.current && retryCountRef.current < 5) {
|
||||||
retryCountRef.current += 1;
|
retryCountRef.current += 1;
|
||||||
const delay = Math.min(
|
const delay = Math.min(
|
||||||
1000 * Math.pow(2, retryCountRef.current - 1),
|
1000 * Math.pow(2, retryCountRef.current - 1),
|
||||||
30000
|
30000
|
||||||
); // Cap at 30 seconds
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`SSE reconnection attempt ${retryCountRef.current}/5 in ${delay}ms`
|
`SSE reconnection attempt ${retryCountRef.current}/5 in ${delay}ms`
|
||||||
|
|||||||
@@ -7,4 +7,10 @@ export type TestEvent = {
|
|||||||
playerId: string;
|
playerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerEvent = TestEvent;
|
export type MatchEvent = {
|
||||||
|
type: "match";
|
||||||
|
matchId: string;
|
||||||
|
tournamentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerEvent = TestEvent | MatchEvent;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createPlayersService } from "./services/players";
|
|||||||
import { createTournamentsService } from "./services/tournaments";
|
import { createTournamentsService } from "./services/tournaments";
|
||||||
import { createTeamsService } from "./services/teams";
|
import { createTeamsService } from "./services/teams";
|
||||||
import { createMatchesService } from "./services/matches";
|
import { createMatchesService } from "./services/matches";
|
||||||
|
import { createReactionsService } from "./services/reactions";
|
||||||
|
|
||||||
class PocketBaseAdminClient {
|
class PocketBaseAdminClient {
|
||||||
private pb: PocketBase;
|
private pb: PocketBase;
|
||||||
@@ -31,6 +32,7 @@ class PocketBaseAdminClient {
|
|||||||
Object.assign(this, createTeamsService(this.pb));
|
Object.assign(this, createTeamsService(this.pb));
|
||||||
Object.assign(this, createTournamentsService(this.pb));
|
Object.assign(this, createTournamentsService(this.pb));
|
||||||
Object.assign(this, createMatchesService(this.pb));
|
Object.assign(this, createMatchesService(this.pb));
|
||||||
|
Object.assign(this, createReactionsService(this.pb));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +51,8 @@ interface AdminClient
|
|||||||
ReturnType<typeof createPlayersService>,
|
ReturnType<typeof createPlayersService>,
|
||||||
ReturnType<typeof createTeamsService>,
|
ReturnType<typeof createTeamsService>,
|
||||||
ReturnType<typeof createTournamentsService>,
|
ReturnType<typeof createTournamentsService>,
|
||||||
ReturnType<typeof createMatchesService> {
|
ReturnType<typeof createMatchesService>,
|
||||||
|
ReturnType<typeof createReactionsService> {
|
||||||
authPromise: Promise<void>;
|
authPromise: Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { transformMatch } from "../util/transform-types";
|
|||||||
export function createMatchesService(pb: PocketBase) {
|
export function createMatchesService(pb: PocketBase) {
|
||||||
return {
|
return {
|
||||||
async getMatch(id: string): Promise<Match | null> {
|
async getMatch(id: string): Promise<Match | null> {
|
||||||
logger.info("PocketBase | Getting match", id);
|
|
||||||
const result = await pb.collection("matches").getOne(id, {
|
const result = await pb.collection("matches").getOne(id, {
|
||||||
expand: "tournament, home, away",
|
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
|
// 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 }> {
|
async getChildMatches(matchId: string): Promise<{ winner: Match | undefined, loser: Match | undefined }> {
|
||||||
logger.info("PocketBase | Getting child matches", matchId);
|
|
||||||
const match = await this.getMatch(matchId);
|
const match = await this.getMatch(matchId);
|
||||||
if (!match) throw new Error("Match not found")
|
if (!match) throw new Error("Match not found")
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ export function createMatchesService(pb: PocketBase) {
|
|||||||
async updateMatch(id: string, data: Partial<MatchInput>): Promise<Match> {
|
async updateMatch(id: string, data: Partial<MatchInput>): Promise<Match> {
|
||||||
logger.info("PocketBase | Updating match", { id, data });
|
logger.info("PocketBase | Updating match", { id, data });
|
||||||
const result = await pb.collection("matches").update<Match>(id, data, {
|
const result = await pb.collection("matches").update<Match>(id, data, {
|
||||||
expand: 'home, away'
|
expand: 'home, away, tournament'
|
||||||
});
|
});
|
||||||
return transformMatch(result);
|
return transformMatch(result);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ export function createPlayersService(pb: PocketBase) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async getPlayerMatches(playerId: string): Promise<Match[]> {
|
async getPlayerMatches(playerId: string): Promise<Match[]> {
|
||||||
const player = await pb.collection("players").getOne(playerId, {
|
console.log('----------------')
|
||||||
|
console.log(playerId)
|
||||||
|
const player = await pb.collection("players").getOne(playerId.trim(), {
|
||||||
expand: "teams",
|
expand: "teams",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
35
src/lib/pocketbase/services/reactions.ts
Normal file
35
src/lib/pocketbase/services/reactions.ts
Normal file
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,7 +7,6 @@ import { Match } from "@/features/matches/types";
|
|||||||
export function createTeamsService(pb: PocketBase) {
|
export function createTeamsService(pb: PocketBase) {
|
||||||
return {
|
return {
|
||||||
async getTeamInfo(id: string): Promise<TeamInfo> {
|
async getTeamInfo(id: string): Promise<TeamInfo> {
|
||||||
logger.info("PocketBase | Getting team info", id);
|
|
||||||
const result = await pb.collection("teams").getOne(id, {
|
const result = await pb.collection("teams").getOne(id, {
|
||||||
fields: "id,name,primary_color,accent_color,logo",
|
fields: "id,name,primary_color,accent_color,logo",
|
||||||
expand: "players"
|
expand: "players"
|
||||||
@@ -25,7 +24,6 @@ export function createTeamsService(pb: PocketBase) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async getTeam(id: string): Promise<Team | null> {
|
async getTeam(id: string): Promise<Team | null> {
|
||||||
logger.info("PocketBase | Getting team", id);
|
|
||||||
const result = await pb.collection("teams").getOne(id, {
|
const result = await pb.collection("teams").getOne(id, {
|
||||||
expand: "players, tournaments",
|
expand: "players, tournaments",
|
||||||
});
|
});
|
||||||
@@ -84,7 +82,6 @@ export function createTeamsService(pb: PocketBase) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async getTeamStats(id: string): Promise<TeamStats | null> {
|
async getTeamStats(id: string): Promise<TeamStats | null> {
|
||||||
logger.info("PocketBase | Getting team stats", id);
|
|
||||||
try {
|
try {
|
||||||
const result = await pb.collection("team_stats").getFirstListItem(`team_id="${id}"`);
|
const result = await pb.collection("team_stats").getFirstListItem(`team_id="${id}"`);
|
||||||
return result as unknown as TeamStats;
|
return result as unknown as TeamStats;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function createTournamentsService(pb: PocketBase) {
|
|||||||
.collection("tournaments")
|
.collection("tournaments")
|
||||||
.getFirstListItem('',
|
.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",
|
sort: "-created",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -104,10 +104,6 @@ export function createTournamentsService(pb: PocketBase) {
|
|||||||
},
|
},
|
||||||
async getUnenrolledTeams(tournamentId: string): Promise<Team[]> {
|
async getUnenrolledTeams(tournamentId: string): Promise<Team[]> {
|
||||||
try {
|
try {
|
||||||
logger.info(
|
|
||||||
"PocketBase | Getting unenrolled teams for tournament",
|
|
||||||
tournamentId
|
|
||||||
);
|
|
||||||
const tournament = await pb
|
const tournament = await pb
|
||||||
.collection("tournaments")
|
.collection("tournaments")
|
||||||
.getOne(tournamentId, {
|
.getOne(tournamentId, {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Reaction } from "@/features/matches/server";
|
||||||
import { Match } from "@/features/matches/types";
|
import { Match } from "@/features/matches/types";
|
||||||
import { Player, PlayerInfo } from "@/features/players/types";
|
import { Player, PlayerInfo } from "@/features/players/types";
|
||||||
import { Team, TeamInfo } from "@/features/teams/types";
|
import { Team, TeamInfo } from "@/features/teams/types";
|
||||||
@@ -149,3 +150,12 @@ export function transformTournament(record: any): Tournament {
|
|||||||
matches
|
matches
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transformReaction(record: any) {
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
emoji: record.emoji,
|
||||||
|
player: transformPlayerInfo(record.expand.player),
|
||||||
|
match: record.match
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user