free agents
This commit is contained in:
@@ -51,6 +51,7 @@ services:
|
||||
- postgres
|
||||
environment:
|
||||
POSTGRESQL_CONNECTION_URI: postgresql://supertokens:password@postgres:5432/supertokens
|
||||
ACCESS_TOKEN_VALIDITY: 360000
|
||||
ports:
|
||||
- "3567:3567"
|
||||
env_file:
|
||||
|
||||
85
pb_migrations/1758379630_created_badges.js
Normal file
85
pb_migrations/1758379630_created_badges.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = new Collection({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1579384326",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1843675174",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "description",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_1340419796",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "badges",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1340419796");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
28
pb_migrations/1758380013_updated_players.js
Normal file
28
pb_migrations/1758380013_updated_players.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(5, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1340419796",
|
||||
"hidden": false,
|
||||
"id": "relation2029409178",
|
||||
"maxSelect": 999,
|
||||
"minSelect": 0,
|
||||
"name": "badges",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2029409178")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1758385120_updated_players.js
Normal file
28
pb_migrations/1758385120_updated_players.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(6, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1340419796",
|
||||
"hidden": false,
|
||||
"id": "relation2813965191",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "featured_badge",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2813965191")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
84
pb_migrations/1758388728_created_free_agents.js
Normal file
84
pb_migrations/1758388728_created_free_agents.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = new Collection({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_3072146508",
|
||||
"hidden": false,
|
||||
"id": "relation2551806565",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "player",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1146066909",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "phone",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_2929550049",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "free_agents",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
28
pb_migrations/1758402128_updated_free_agents.js
Normal file
28
pb_migrations/1758402128_updated_free_agents.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_340646327",
|
||||
"hidden": false,
|
||||
"id": "relation3177167065",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "tournament",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation3177167065")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1758402424_updated_tournaments.js
Normal file
28
pb_migrations/1758402424_updated_tournaments.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation1584152981")
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(11, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_3072146508",
|
||||
"hidden": false,
|
||||
"id": "relation1584152981",
|
||||
"maxSelect": 999,
|
||||
"minSelect": 0,
|
||||
"name": "free_agents",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
@@ -1,7 +1,3 @@
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/dates/styles.css";
|
||||
import "@mantine/carousel/styles.css";
|
||||
import '@mantine/tiptap/styles.css';
|
||||
import {
|
||||
HeadContent,
|
||||
Navigate,
|
||||
@@ -18,9 +14,12 @@ import Providers from "@/features/core/components/providers";
|
||||
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
|
||||
import { HeaderConfig } from "@/features/core/types/header-config";
|
||||
import { playerQueries } from "@/features/players/queries";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import FullScreenLoader from "@/components/full-screen-loader";
|
||||
import mantineCssUrl from '@mantine/core/styles.css?url'
|
||||
import mantineDatesCssUrl from '@mantine/dates/styles.css?url'
|
||||
import mantineCarouselCssUrl from '@mantine/carousel/styles.css?url'
|
||||
import mantineTiptapCssUrl from '@mantine/tiptap/styles.css?url'
|
||||
|
||||
export const Route = createRootRouteWithContext<{
|
||||
queryClient: QueryClient;
|
||||
@@ -61,6 +60,10 @@ export const Route = createRootRouteWithContext<{
|
||||
},
|
||||
{ rel: "manifest", href: "/site.webmanifest" },
|
||||
{ rel: "icon", href: "/favicon.ico" },
|
||||
{ rel: 'stylesheet', href: mantineCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineDatesCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineTiptapCssUrl }
|
||||
],
|
||||
}),
|
||||
errorComponent: (props) => {
|
||||
@@ -131,7 +134,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
<div className="app">{children}</div>
|
||||
<Scripts />
|
||||
<ReactQueryDevtools />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,13 @@ import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/
|
||||
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
||||
import { Suspense } from "react";
|
||||
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
|
||||
|
||||
export const Route = createFileRoute("/_authed/")({
|
||||
component: Home,
|
||||
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
|
||||
<Home />
|
||||
</Suspense>,
|
||||
beforeLoad: async ({ context }) => {
|
||||
const queryClient = context.queryClient;
|
||||
const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current())
|
||||
@@ -18,6 +22,7 @@ export const Route = createFileRoute("/_authed/")({
|
||||
title: context.tournament.name || "FLXN"
|
||||
}
|
||||
}),
|
||||
pendingComponent: () => <UpcomingTournamentSkeleton />
|
||||
});
|
||||
|
||||
function Home() {
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/_authed/stats")({
|
||||
component: Stats,
|
||||
beforeLoad: async ({ context }) => {
|
||||
const queryClient = context.queryClient;
|
||||
await ensureServerQueryData(queryClient, playerQueries.allStats());
|
||||
ensureServerQueryData(queryClient, playerQueries.allStats());
|
||||
},
|
||||
loader: () => ({
|
||||
withPadding: false,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import TeamProfile from "@/features/teams/components/team-profile";
|
||||
import { teamKeys, teamQueries } from "@/features/teams/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import { redirect, createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
const searchSchema = z.object({
|
||||
|
||||
@@ -14,6 +14,7 @@ export function RichTextEditor({
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content: value,
|
||||
immediatelyRender: false,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange(editor.getHTML());
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ const Drawer: React.FC<DrawerProps> = ({
|
||||
<VaulDrawer.Root open={opened} onOpenChange={onChange}>
|
||||
<VaulDrawer.Portal>
|
||||
<VaulDrawer.Overlay className={styles.drawerOverlay} />
|
||||
<VaulDrawer.Content className={styles.drawerContent}>
|
||||
<VaulDrawer.Content className={styles.drawerContent} aria-describedby="drawer">
|
||||
<Container flex={1} p="md">
|
||||
<Box
|
||||
mb="sm"
|
||||
|
||||
@@ -14,12 +14,14 @@ interface AuthData {
|
||||
user: Player | undefined;
|
||||
metadata: { accentColor: MantineColor; colorScheme: MantineColorScheme };
|
||||
roles: string[];
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export const defaultAuthData: AuthData = {
|
||||
user: undefined,
|
||||
metadata: { accentColor: "blue", colorScheme: "auto" },
|
||||
roles: [],
|
||||
phone: ""
|
||||
};
|
||||
|
||||
export interface AuthContextType extends AuthData {
|
||||
@@ -59,6 +61,7 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
user: data?.user || defaultAuthData.user,
|
||||
metadata: data?.metadata || defaultAuthData.metadata,
|
||||
roles: data?.roles || defaultAuthData.roles,
|
||||
phone: data?.phone || "",
|
||||
set,
|
||||
}),
|
||||
[data, defaultAuthData]
|
||||
|
||||
@@ -107,7 +107,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
||||
</Text>
|
||||
<Stack gap={1}>
|
||||
{match.home?.players.map((p) => (
|
||||
<Text size="xs" fw={600} c="dimmed" ta="right">
|
||||
<Text key={`match-card-p-${p.id}`} size="xs" fw={600} c="dimmed" ta="right">
|
||||
{p.first_name} {p.last_name}
|
||||
</Text>
|
||||
))}
|
||||
@@ -163,7 +163,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
|
||||
</Text>
|
||||
<Stack gap={1}>
|
||||
{match.away?.players.map((p) => (
|
||||
<Text size="xs" fw={600} c="dimmed" ta="right">
|
||||
<Text key={`match-card-p-${p.id}`} size="xs" fw={600} c="dimmed" ta="right">
|
||||
{p.first_name} {p.last_name}
|
||||
</Text>
|
||||
))}
|
||||
|
||||
@@ -21,11 +21,18 @@ export const fetchMe = createServerFn()
|
||||
return {
|
||||
user: result || undefined,
|
||||
roles: context.roles,
|
||||
metadata: context.metadata
|
||||
metadata: context.metadata,
|
||||
phone: context.phone
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.info('fetchMe: Session error', error.message);
|
||||
return { user: undefined, roles: [], metadata: {} };
|
||||
logger.info("FetchMe: Session error", error)
|
||||
if (error?.response?.status === 401) {
|
||||
const errorData = error?.response?.data;
|
||||
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return { user: undefined, roles: [], metadata: {}, phone: undefined };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -58,15 +58,12 @@ const EmojiBar = ({
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useServerQuery, useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { useServerMutation, useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
import { getMatchReactions, toggleMatchReaction } from "@/features/matches/server";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
@@ -14,7 +14,7 @@ export const reactionQueries = {
|
||||
};
|
||||
|
||||
export const useMatchReactions = (matchId: string) =>
|
||||
useServerQuery(reactionQueries.match(matchId));
|
||||
useServerSuspenseQuery(reactionQueries.match(matchId));
|
||||
|
||||
export const useToggleMatchReaction = (matchId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -69,6 +69,7 @@ const TeamList = ({ teams, loading = false }: TeamListProps) => {
|
||||
{teams?.map((team) => (
|
||||
<div key={team.id}>
|
||||
<ListItem
|
||||
key={`team-list-${team.id}`}
|
||||
p="xs"
|
||||
icon={
|
||||
<Avatar
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Box, Card, Center, Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||
|
||||
const StartedTournamentSkeleton = () => {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Header skeleton */}
|
||||
<Stack px="md">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Skeleton height={32} width="60%" mb="xs" />
|
||||
<Skeleton height={16} width="40%" />
|
||||
</Box>
|
||||
<Skeleton height={60} width={60} radius="md" />
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{/* Match carousel skeleton */}
|
||||
<Box>
|
||||
<Group gap="xs" px="xl">
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
withBorder
|
||||
radius="lg"
|
||||
p="lg"
|
||||
style={{ minWidth: "95%", flex: "0 0 auto" }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Match header */}
|
||||
<Group justify="space-between">
|
||||
<Skeleton height={14} width="30%" />
|
||||
<Skeleton height={20} width={60} radius="xl" />
|
||||
</Group>
|
||||
|
||||
{/* Teams */}
|
||||
<Stack gap="sm">
|
||||
<Group>
|
||||
<Skeleton height={32} width={32} radius="sm" />
|
||||
<Skeleton height={16} width="40%" />
|
||||
<Box ml="auto">
|
||||
<Skeleton height={24} width={30} />
|
||||
</Box>
|
||||
</Group>
|
||||
<Center>
|
||||
<Skeleton height={14} width={20} />
|
||||
</Center>
|
||||
<Group>
|
||||
<Skeleton height={32} width={32} radius="sm" />
|
||||
<Skeleton height={16} width="40%" />
|
||||
<Box ml="auto">
|
||||
<Skeleton height={24} width={30} />
|
||||
</Box>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
))}
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{/* Actions section skeleton */}
|
||||
<Box>
|
||||
<Divider />
|
||||
<Stack gap={0}>
|
||||
<Skeleton height={48} width="100%" />
|
||||
<Skeleton height={48} width="100%" />
|
||||
<Skeleton height={48} width="100%" />
|
||||
<Skeleton height={48} width="100%" />
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartedTournamentSkeleton;
|
||||
@@ -2,11 +2,23 @@ import Button from "@/components/button";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import { Text } from "@mantine/core";
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
import useEnrollFreeAgent from "../../hooks/use-enroll-free-agent";
|
||||
|
||||
const EnrollFreeAgent = () => {
|
||||
const EnrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => {
|
||||
const { open, isOpen, toggle } = useSheet();
|
||||
const { user } = useAuth();
|
||||
const { user, phone } = useAuth();
|
||||
|
||||
const { mutate: enrollFreeAgent, isPending: isEnrolling } = useEnrollFreeAgent();
|
||||
const handleEnroll = () => {
|
||||
console.log('enrolling...')
|
||||
enrollFreeAgent({ playerId: user!.id, tournamentId, phone }, {
|
||||
onSuccess: () => {
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="subtle" size="sm" onClick={open}>
|
||||
@@ -14,13 +26,19 @@ const EnrollFreeAgent = () => {
|
||||
</Button>
|
||||
|
||||
<Sheet title="Free Agent Enrollment" opened={isOpen} onChange={toggle}>
|
||||
<Text size="md" mb="md">
|
||||
<Stack gap="xs">
|
||||
<Text size="md">
|
||||
Enrolling as a free agent will enter you in a pool of players wanting to play but don't have a teammate yet.
|
||||
</Text>
|
||||
<Text size="sm" mb="md" c='dimmed'>
|
||||
You will be automatically paired with a partner before the tournament starts, and you will be able to see your new team and set a walkout song in the app.
|
||||
<Text size="sm" c='dimmed'>
|
||||
You will be able to see a list of other enrolled free agents, as well as their contact information for organizing your team and walkout song. By enrolling, your phone number will be visible to other free agents.
|
||||
</Text>
|
||||
<Button onClick={console.log}>Confirm</Button>
|
||||
<Text size="xs" c="dimmed">
|
||||
Note: this does not guarantee you a spot in the tournament. One person from your team must enroll in the app and choose a walkout song in order to secure a spot.
|
||||
</Text>
|
||||
<Button onClick={handleEnroll}>Confirm</Button>
|
||||
<Button variant="subtle" color="red" onClick={toggle}>Cancel</Button>
|
||||
</Stack>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Group, Stack, Text, Card, Badge, Box, ActionIcon } from "@mantine/core";
|
||||
import { UserIcon, PhoneIcon } from "@phosphor-icons/react";
|
||||
import { useFreeAgents } from "../../queries";
|
||||
import UnenrollFreeAgent from "./unenroll-free-agent";
|
||||
import toast from "@/lib/sonner";
|
||||
|
||||
const EnrolledFreeAgent: React.FC<{ tournamentId: string }> = ({
|
||||
tournamentId
|
||||
}) => {
|
||||
const { data: freeAgents } = useFreeAgents(tournamentId);
|
||||
|
||||
const copyToClipboard = async (phone: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(phone);
|
||||
toast.success("Phone number copied!");
|
||||
} catch (err) {
|
||||
toast.success("Failed to copy");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="xs" align="center">
|
||||
<UserIcon size={16} />
|
||||
<Text size="sm" fw={500}>
|
||||
Enrolled as Free Agent
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
You're on the free agent list. Other free agents looking for teams:
|
||||
</Text>
|
||||
|
||||
{freeAgents.length > 1 ? (
|
||||
<Card withBorder radius="md" p="sm">
|
||||
<Stack gap="xs">
|
||||
{freeAgents
|
||||
.filter(agent => agent.player)
|
||||
.map((agent) => (
|
||||
<Group key={agent.id} justify="space-between" align="center" wrap="nowrap">
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={500} truncate>
|
||||
{agent.player?.first_name} {agent.player?.last_name}
|
||||
</Text>
|
||||
</Box>
|
||||
{agent.phone && (
|
||||
<Group gap={4} align="center" style={{ flexShrink: 0 }}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(agent.phone!)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<PhoneIcon size={12} />
|
||||
</ActionIcon>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => copyToClipboard(agent.phone!)}
|
||||
>
|
||||
{agent.phone}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
))}
|
||||
|
||||
{freeAgents.length > 1 && (
|
||||
<Badge
|
||||
variant="light"
|
||||
size="xs"
|
||||
color="blue"
|
||||
style={{ alignSelf: 'flex-start', marginTop: '4px' }}
|
||||
>
|
||||
{freeAgents.length} free agents total
|
||||
</Badge>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
) : (
|
||||
<Card withBorder radius="md" p="sm">
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
You're the only free agent so far
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
<UnenrollFreeAgent tournamentId={tournamentId} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnrolledFreeAgent;
|
||||
@@ -14,10 +14,6 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
() => new Date(tournament.start_time),
|
||||
[tournament.start_time]
|
||||
);
|
||||
const teamCount = useMemo(
|
||||
() => tournament.teams?.length || 0,
|
||||
[tournament.teams]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack align="center" gap={0}>
|
||||
|
||||
@@ -15,8 +15,9 @@ import TeamCard from "@/features/teams/components/team-card";
|
||||
import UpdateTeam from "./update-team";
|
||||
import UnenrollTeam from "./unenroll-team";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { tournamentKeys } from "../../queries";
|
||||
import { tournamentKeys, useFreeAgents } from "../../queries";
|
||||
import RulesListButton from "./rules-list-button";
|
||||
import EnrolledFreeAgent from "./enrolled-free-agent";
|
||||
|
||||
const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
|
||||
tournament,
|
||||
@@ -40,12 +41,17 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleSubmit = () => {
|
||||
queryClient.invalidateQueries({ queryKey: tournamentKeys.current })
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: tournamentKeys.current });
|
||||
};
|
||||
|
||||
const { data: free_agents } = useFreeAgents(tournament.id);
|
||||
const isFreeAgent = useMemo(() => !isUserEnrolled && free_agents.find(a => a.player?.id === user?.id), [free_agents, isUserEnrolled]);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Header tournament={tournament} />
|
||||
|
||||
<Stack px="md">
|
||||
{tournament.desc && <Text size="sm">{tournament.desc}</Text>}
|
||||
|
||||
<Card withBorder radius="lg" p="lg">
|
||||
@@ -72,25 +78,42 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isUserEnrolled &&isEnrollmentOpen && (
|
||||
{!isUserEnrolled && isEnrollmentOpen && !isFreeAgent && (
|
||||
<>
|
||||
<EnrollTeam tournamentId={tournament.id} onSubmit={handleSubmit} />
|
||||
<EnrollTeam
|
||||
tournamentId={tournament.id}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
<Divider my={0} label="or" />
|
||||
<EnrollFreeAgent />
|
||||
<EnrollFreeAgent tournamentId={tournament.id} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
isUserEnrolled && <>
|
||||
{isUserEnrolled && (
|
||||
<>
|
||||
<Suspense fallback={<TeamCardSkeleton />}>
|
||||
<TeamCard teamId={userTeam.id} />
|
||||
</Suspense>
|
||||
<UpdateTeam tournamentId={tournament.id} teamId={userTeam.id} />
|
||||
{ isEnrollmentOpen && <UnenrollTeam tournamentId={tournament.id} teamId={userTeam.id} onSubmit={handleSubmit} />}
|
||||
{isEnrollmentOpen && (
|
||||
<UnenrollTeam
|
||||
tournamentId={tournament.id}
|
||||
teamId={userTeam.id}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
isFreeAgent && isEnrollmentOpen && (
|
||||
<EnrolledFreeAgent tournamentId={tournament.id} />
|
||||
)
|
||||
}
|
||||
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Divider />
|
||||
|
||||
@@ -20,6 +20,7 @@ const RulesListButton: React.FC<RulesListButtonProps> = ({ tournamentId }) => {
|
||||
extensions: [StarterKit],
|
||||
content: tournament?.rules || '',
|
||||
editable: false,
|
||||
immediatelyRender: false,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Box, Card, Divider, Flex, Group, Skeleton, Stack } from "@mantine/core";
|
||||
|
||||
const UpcomingTournamentSkeleton = () => {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Flex px="md" justify="center" w="100%">
|
||||
<Skeleton height={240} width={240} radius="md" />
|
||||
<Stack justify="space-between" align="flex-start">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Skeleton height={32} mb="xs" />
|
||||
<Skeleton height={16} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Flex>
|
||||
|
||||
<Stack px="md">
|
||||
<Skeleton height={14} width="80%" />
|
||||
|
||||
<Card withBorder radius="lg" p="lg">
|
||||
<Group mb="sm" gap="xs" align="center">
|
||||
<Skeleton height={16} width={16} />
|
||||
<Skeleton height={14} width="20%" />
|
||||
<Box ml="auto">
|
||||
<Skeleton height={20} width={80} radius="sm" />
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Divider />
|
||||
<Stack gap={0}>
|
||||
<Skeleton height={48} width="100%" />
|
||||
<Skeleton height={48} width="100%" />
|
||||
<Skeleton height={48} width="100%" />
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpcomingTournamentSkeleton;
|
||||
@@ -0,0 +1,40 @@
|
||||
import Button from "@/components/button";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
import useUnenrollFreeAgent from "../../hooks/use-unenroll-free-agent";
|
||||
|
||||
const UnenrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => {
|
||||
const { open, isOpen, toggle } = useSheet();
|
||||
const { user } = useAuth();
|
||||
|
||||
const { mutate: unenrollFreeAgent, isPending: isEnrolling } = useUnenrollFreeAgent();
|
||||
const handleUnenroll = () => {
|
||||
unenrollFreeAgent({ playerId: user!.id, tournamentId }, {
|
||||
onSuccess: () => {
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="subtle" size="sm" onClick={open}>
|
||||
Unenroll
|
||||
</Button>
|
||||
|
||||
<Sheet title="Are you sure?" opened={isOpen} onChange={toggle}>
|
||||
<Stack gap="xs">
|
||||
<Text size="md">
|
||||
This will remove you from the free agent list.
|
||||
</Text>
|
||||
<Button onClick={handleUnenroll}>Confirm</Button>
|
||||
<Button variant="subtle" color="red" onClick={toggle}>Cancel</Button>
|
||||
</Stack>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnenrollFreeAgent;
|
||||
20
src/features/tournaments/hooks/use-enroll-free-agent.ts
Normal file
20
src/features/tournaments/hooks/use-enroll-free-agent.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { enrollFreeAgent } from "@/features/tournaments/server";
|
||||
import { tournamentKeys } from "../queries";
|
||||
|
||||
const useEnrollFreeAgent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useServerMutation({
|
||||
mutationFn: (data: { tournamentId: string, playerId: string, phone: string }) => {
|
||||
return enrollFreeAgent({ data });
|
||||
},
|
||||
onSuccess: (data, { tournamentId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
||||
},
|
||||
successMessage: 'You\'ve been added as a free agent!',
|
||||
});
|
||||
};
|
||||
|
||||
export default useEnrollFreeAgent;
|
||||
20
src/features/tournaments/hooks/use-unenroll-free-agent.ts
Normal file
20
src/features/tournaments/hooks/use-unenroll-free-agent.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { unenrollFreeAgent } from "@/features/tournaments/server";
|
||||
import { tournamentKeys } from "../queries";
|
||||
|
||||
const useUnenrollFreeAgent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useServerMutation({
|
||||
mutationFn: (data: { tournamentId: string, playerId: string }) => {
|
||||
return unenrollFreeAgent({ data });
|
||||
},
|
||||
onSuccess: (data, { tournamentId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
||||
},
|
||||
successMessage: 'You\'ve been removed as a free agent.',
|
||||
});
|
||||
};
|
||||
|
||||
export default useUnenrollFreeAgent;
|
||||
@@ -1,11 +1,12 @@
|
||||
import { getCurrentTournament, getTournament, getUnenrolledTeams, listTournaments } from "./server";
|
||||
import { getCurrentTournament, getFreeAgents, getTournament, getUnenrolledTeams, listTournaments } from "./server";
|
||||
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
|
||||
export const tournamentKeys = {
|
||||
list: ['tournaments', 'list'] as const,
|
||||
details: (id: string) => ['tournaments', 'details', id] as const,
|
||||
current: ['tournaments', 'current'] as const,
|
||||
unenrolled: (id: string) => ['tournaments', 'unenrolled', id] as const
|
||||
unenrolled: (id: string) => ['tournaments', 'unenrolled', id] as const,
|
||||
free_agents: (id: string) => ['tournaments', 'free_agents', id] as const
|
||||
};
|
||||
|
||||
export const tournamentQueries = {
|
||||
@@ -24,6 +25,10 @@ export const tournamentQueries = {
|
||||
unenrolled: (id: string) => ({
|
||||
queryKey: tournamentKeys.unenrolled(id),
|
||||
queryFn: () => getUnenrolledTeams({ data: id })
|
||||
}),
|
||||
free_agents: (id: string) => ({
|
||||
queryKey: tournamentKeys.free_agents(id),
|
||||
queryFn: () => getFreeAgents({ data: id })
|
||||
})
|
||||
};
|
||||
|
||||
@@ -38,3 +43,6 @@ export const useCurrentTournament = () =>
|
||||
|
||||
export const useUnenrolledTeams = (tournamentId: string) =>
|
||||
useServerSuspenseQuery(tournamentQueries.unenrolled(tournamentId));
|
||||
|
||||
export const useFreeAgents = (tournamentId: string) =>
|
||||
useServerSuspenseQuery(tournamentQueries.free_agents(tournamentId));
|
||||
|
||||
@@ -84,3 +84,38 @@ export const getUnenrolledTeams = createServerFn()
|
||||
.handler(async ({ data: tournamentId }) =>
|
||||
toServerResult(() => pbAdmin.getUnenrolledTeams(tournamentId))
|
||||
);
|
||||
|
||||
export const getFreeAgents = createServerFn()
|
||||
.validator(z.string())
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.handler(async ({ data: tournamentId }) =>
|
||||
toServerResult(() => pbAdmin.getFreeAgents(tournamentId))
|
||||
);
|
||||
|
||||
export const enrollFreeAgent = createServerFn()
|
||||
.validator(z.object({ phone: z.string(), tournamentId: z.string() }))
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ context, data }) =>
|
||||
toServerResult(async () => {
|
||||
const userAuthId = context.userAuthId;
|
||||
const player = await pbAdmin.getPlayerByAuthId(userAuthId);
|
||||
if (!player) throw new Error("Player not found");
|
||||
|
||||
await pbAdmin.enrollFreeAgent(player.id, data.phone, data.tournamentId);
|
||||
logger.info('Player enrolled as free agent', { playerId: player.id, phone: data.phone });
|
||||
})
|
||||
);
|
||||
|
||||
export const unenrollFreeAgent = createServerFn()
|
||||
.validator(z.object({ tournamentId: z.string() }))
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ context, data }) =>
|
||||
toServerResult(async () => {
|
||||
const userAuthId = context.userAuthId;
|
||||
const player = await pbAdmin.getPlayerByAuthId(userAuthId);
|
||||
if (!player) throw new Error("Player not found");
|
||||
|
||||
await pbAdmin.unenrollFreeAgent(player.id, data.tournamentId);
|
||||
logger.info('Player unenrolled as free agent', { playerId: player.id });
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2,12 +2,9 @@ import { useEffect, useRef } from "react";
|
||||
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 { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import { reactionKeys, reactionQueries } from "@/features/reactions/queries";
|
||||
|
||||
let newIdeasAvailable = false;
|
||||
let newIdeasCallbacks: (() => void)[] = [];
|
||||
|
||||
const logger = new Logger('ServerEvents');
|
||||
|
||||
type SSEEvent = {
|
||||
@@ -19,45 +16,19 @@ type EventHandler = (event: SSEEvent, queryClient: ReturnType<typeof useQueryCli
|
||||
|
||||
const eventHandlers: Record<string, EventHandler> = {
|
||||
"connected": () => {
|
||||
logger.info("ServerEvents | New Connection");
|
||||
logger.info("New Connection");
|
||||
},
|
||||
|
||||
"ping": () => {},
|
||||
|
||||
"test": (event, queryClient) => {
|
||||
|
||||
},
|
||||
|
||||
"match": (event, queryClient) => {
|
||||
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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export function getNewIdeasAvailable(): boolean {
|
||||
return newIdeasAvailable;
|
||||
}
|
||||
|
||||
export function clearNewIdeasAvailable(): void {
|
||||
newIdeasAvailable = false;
|
||||
}
|
||||
|
||||
export function subscribeToNewIdeas(callback: () => void): () => void {
|
||||
newIdeasCallbacks.push(callback);
|
||||
return () => {
|
||||
const index = newIdeasCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
newIdeasCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useServerEvents() {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
|
||||
@@ -105,13 +105,11 @@ export function createPlayersService(pb: PocketBase) {
|
||||
|
||||
async getUnenrolledPlayers(tournamentId: string): Promise<Player[]> {
|
||||
try {
|
||||
// Get the tournament with its enrolled teams
|
||||
const tournament = await pb.collection("tournaments").getOne(tournamentId, {
|
||||
fields: "teams",
|
||||
expand: "teams,teams.players"
|
||||
});
|
||||
|
||||
// Extract player IDs from all enrolled teams
|
||||
const enrolledPlayerIds: string[] = [];
|
||||
if (tournament.expand?.teams) {
|
||||
const teams = Array.isArray(tournament.expand.teams) ? tournament.expand.teams : [tournament.expand.teams];
|
||||
@@ -125,7 +123,6 @@ export function createPlayersService(pb: PocketBase) {
|
||||
});
|
||||
}
|
||||
|
||||
// If no players are enrolled, return all players
|
||||
if (enrolledPlayerIds.length === 0) {
|
||||
const allPlayers = await pb.collection("players").getFullList<Player>({
|
||||
fields: "id,first_name,last_name,email",
|
||||
@@ -133,7 +130,6 @@ export function createPlayersService(pb: PocketBase) {
|
||||
return allPlayers.map(transformPlayer);
|
||||
}
|
||||
|
||||
// Build filter to exclude enrolled players
|
||||
const filter = enrolledPlayerIds
|
||||
.map((playerId: string) => `id != "${playerId}"`)
|
||||
.join(" && ");
|
||||
@@ -146,7 +142,6 @@ export function createPlayersService(pb: PocketBase) {
|
||||
return availablePlayers.map(transformPlayer);
|
||||
} catch (error) {
|
||||
console.error("Error getting unenrolled players:", error);
|
||||
// Fallback to all players if there's an error
|
||||
const allPlayers = await pb.collection("players").getFullList<Player>({
|
||||
fields: "id,first_name,last_name,email",
|
||||
});
|
||||
|
||||
@@ -7,8 +7,10 @@ import type {
|
||||
} from "@/features/tournaments/types";
|
||||
import type { Team } from "@/features/teams/types";
|
||||
import PocketBase from "pocketbase";
|
||||
import { transformTournament, transformTournamentInfo } from "@/lib/pocketbase/util/transform-types";
|
||||
import { transformFreeAgent, transformTournament, transformTournamentInfo } from "@/lib/pocketbase/util/transform-types";
|
||||
import { transformTeam } from "@/lib/pocketbase/util/transform-types";
|
||||
import { getFreeAgents } from "@/features/tournaments/server";
|
||||
import { PlayerInfo } from "@/features/players/types";
|
||||
|
||||
export function createTournamentsService(pb: PocketBase) {
|
||||
return {
|
||||
@@ -133,5 +135,33 @@ export function createTournamentsService(pb: PocketBase) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async enrollFreeAgent(playerId: string, phone: string, tournamentId: string): Promise<void> {
|
||||
await pb.collection("free_agents").create({
|
||||
tournament: tournamentId,
|
||||
player: playerId,
|
||||
phone: phone
|
||||
});
|
||||
},
|
||||
|
||||
async unenrollFreeAgent(playerId: string, tournamentId: string): Promise<void> {
|
||||
const result = await pb.collection("free_agents").getFirstListItem(
|
||||
`player = "${playerId}" && tournament = "${tournamentId}"`
|
||||
);
|
||||
await pb.collection("free_agents").delete(result.id);
|
||||
},
|
||||
async getFreeAgents(tournamentId: string): Promise<{ id: string, phone: string, player: PlayerInfo | undefined }[]> {
|
||||
try {
|
||||
const free_agents = await pb
|
||||
.collection("free_agents")
|
||||
.getFullList({ filter: `tournament = "${tournamentId}"`,
|
||||
expand: 'player'
|
||||
});
|
||||
|
||||
return free_agents.map(transformFreeAgent);
|
||||
} catch (error) {
|
||||
logger.error("PocketBase | Error getting unenrolled teams", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +85,22 @@ export function transformPlayer(record: any): Player {
|
||||
};
|
||||
}
|
||||
|
||||
export function transformFreeAgent(record: any) {
|
||||
const player = record.expand?.player ? transformPlayerInfo(record.expand.player) : undefined;
|
||||
const tournaments =
|
||||
record.expand?.tournaments
|
||||
?.sort((a: any, b: any) =>
|
||||
new Date(a.created!) < new Date(b.created!) ? -1 : 0
|
||||
)
|
||||
?.map(transformTournamentInfo) ?? [];
|
||||
|
||||
return {
|
||||
id: record.id as string,
|
||||
phone: record.phone as string,
|
||||
player
|
||||
};
|
||||
}
|
||||
|
||||
export function transformTeam(record: any): Team {
|
||||
const players =
|
||||
record.expand?.players
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { getSessionForSSR } from "supertokens-node/custom";
|
||||
import { ensureSuperTokensBackend } from "../server";
|
||||
import { logger } from "../";
|
||||
import SuperTokens from "supertokens-node";
|
||||
|
||||
export async function getSessionForStart(request: Request, options?: { sessionRequired?: boolean }) {
|
||||
ensureSuperTokensBackend();
|
||||
|
||||
try {
|
||||
const session = await getSessionForSSR(request);
|
||||
|
||||
if (session.hasToken) {
|
||||
if (session.accessTokenPayload?.sub === undefined || session.accessTokenPayload?.sessionHandle === undefined) {
|
||||
const userId = session.accessTokenPayload?.sub;
|
||||
if (userId === undefined || session.accessTokenPayload?.sessionHandle === undefined) {
|
||||
return {
|
||||
hasToken: true,
|
||||
needsRefresh: true,
|
||||
@@ -17,11 +18,15 @@ export async function getSessionForStart(request: Request, options?: { sessionRe
|
||||
}
|
||||
}
|
||||
|
||||
const user = await SuperTokens.getUser(userId);
|
||||
|
||||
|
||||
return {
|
||||
hasToken: true,
|
||||
accessTokenPayload: session.accessTokenPayload,
|
||||
userId: session.accessTokenPayload?.sub,
|
||||
userId,
|
||||
sessionHandle: session.accessTokenPayload?.sessionHandle,
|
||||
phone: user?.phoneNumbers[0]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ export const verifySuperTokensSession = async (
|
||||
userAuthId,
|
||||
roles,
|
||||
metadata,
|
||||
phone: session.phone,
|
||||
session: {
|
||||
accessTokenPayload: session.accessTokenPayload,
|
||||
sessionHandle: session.sessionHandle,
|
||||
@@ -68,7 +69,7 @@ export const verifySuperTokensSession = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const getSessionContext = async (request: Request, options?: { isServerFunction?: boolean }): Promise<any> => {
|
||||
export const getSessionContext = async (request: Request, options?: { isServerFunction?: boolean }) => {
|
||||
const session = await verifySuperTokensSession(request);
|
||||
|
||||
if (session.context.session?.tryRefresh) {
|
||||
@@ -93,6 +94,7 @@ export const getSessionContext = async (request: Request, options?: { isServerFu
|
||||
userAuthId: session.context.userAuthId,
|
||||
roles: session.context.roles,
|
||||
metadata: session.context.metadata,
|
||||
phone: session.context.phone
|
||||
};
|
||||
|
||||
return context;
|
||||
|
||||
Reference in New Issue
Block a user