free agents

This commit is contained in:
yohlo
2025-09-20 20:50:44 -05:00
parent 5e20b94a1f
commit 1027b49258
37 changed files with 817 additions and 128 deletions

View File

@@ -51,6 +51,7 @@ services:
- postgres - postgres
environment: environment:
POSTGRESQL_CONNECTION_URI: postgresql://supertokens:password@postgres:5432/supertokens POSTGRESQL_CONNECTION_URI: postgresql://supertokens:password@postgres:5432/supertokens
ACCESS_TOKEN_VALIDITY: 360000
ports: ports:
- "3567:3567" - "3567:3567"
env_file: env_file:

View 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);
})

View 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)
})

View 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)
})

View 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);
})

View 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)
})

View 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)
})

View File

@@ -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 { import {
HeadContent, HeadContent,
Navigate, Navigate,
@@ -18,9 +14,12 @@ import Providers from "@/features/core/components/providers";
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core"; import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
import { HeaderConfig } from "@/features/core/types/header-config"; import { HeaderConfig } from "@/features/core/types/header-config";
import { playerQueries } from "@/features/players/queries"; import { playerQueries } from "@/features/players/queries";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure"; import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import FullScreenLoader from "@/components/full-screen-loader"; 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<{ export const Route = createRootRouteWithContext<{
queryClient: QueryClient; queryClient: QueryClient;
@@ -61,6 +60,10 @@ export const Route = createRootRouteWithContext<{
}, },
{ rel: "manifest", href: "/site.webmanifest" }, { rel: "manifest", href: "/site.webmanifest" },
{ rel: "icon", href: "/favicon.ico" }, { rel: "icon", href: "/favicon.ico" },
{ rel: 'stylesheet', href: mantineCssUrl },
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
{ rel: 'stylesheet', href: mantineDatesCssUrl },
{ rel: 'stylesheet', href: mantineTiptapCssUrl }
], ],
}), }),
errorComponent: (props) => { errorComponent: (props) => {
@@ -131,7 +134,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
> >
<div className="app">{children}</div> <div className="app">{children}</div>
<Scripts /> <Scripts />
<ReactQueryDevtools />
</body> </body>
</html> </html>
); );

View File

@@ -3,9 +3,13 @@ import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/
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"; 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/")({ export const Route = createFileRoute("/_authed/")({
component: Home, component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
<Home />
</Suspense>,
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const queryClient = context.queryClient; const queryClient = context.queryClient;
const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current()) const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current())
@@ -18,6 +22,7 @@ export const Route = createFileRoute("/_authed/")({
title: context.tournament.name || "FLXN" title: context.tournament.name || "FLXN"
} }
}), }),
pendingComponent: () => <UpcomingTournamentSkeleton />
}); });
function Home() { function Home() {

View File

@@ -7,7 +7,7 @@ export const Route = createFileRoute("/_authed/stats")({
component: Stats, component: Stats,
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const queryClient = context.queryClient; const queryClient = context.queryClient;
await ensureServerQueryData(queryClient, playerQueries.allStats()); ensureServerQueryData(queryClient, playerQueries.allStats());
}, },
loader: () => ({ loader: () => ({
withPadding: false, withPadding: false,

View File

@@ -1,8 +1,7 @@
import TeamProfile from "@/features/teams/components/team-profile"; import TeamProfile from "@/features/teams/components/team-profile";
import { teamKeys, teamQueries } from "@/features/teams/queries"; import { teamKeys, teamQueries } from "@/features/teams/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch"; 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"; import { z } from "zod";
const searchSchema = z.object({ const searchSchema = z.object({

View File

@@ -14,6 +14,7 @@ export function RichTextEditor({
const editor = useEditor({ const editor = useEditor({
extensions: [StarterKit], extensions: [StarterKit],
content: value, content: value,
immediatelyRender: false,
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
onChange(editor.getHTML()); onChange(editor.getHTML());
}, },

View File

@@ -63,7 +63,7 @@ const Drawer: React.FC<DrawerProps> = ({
<VaulDrawer.Root open={opened} onOpenChange={onChange}> <VaulDrawer.Root open={opened} onOpenChange={onChange}>
<VaulDrawer.Portal> <VaulDrawer.Portal>
<VaulDrawer.Overlay className={styles.drawerOverlay} /> <VaulDrawer.Overlay className={styles.drawerOverlay} />
<VaulDrawer.Content className={styles.drawerContent}> <VaulDrawer.Content className={styles.drawerContent} aria-describedby="drawer">
<Container flex={1} p="md"> <Container flex={1} p="md">
<Box <Box
mb="sm" mb="sm"

View File

@@ -14,12 +14,14 @@ interface AuthData {
user: Player | undefined; user: Player | undefined;
metadata: { accentColor: MantineColor; colorScheme: MantineColorScheme }; metadata: { accentColor: MantineColor; colorScheme: MantineColorScheme };
roles: string[]; roles: string[];
phone: string;
} }
export const defaultAuthData: AuthData = { export const defaultAuthData: AuthData = {
user: undefined, user: undefined,
metadata: { accentColor: "blue", colorScheme: "auto" }, metadata: { accentColor: "blue", colorScheme: "auto" },
roles: [], roles: [],
phone: ""
}; };
export interface AuthContextType extends AuthData { export interface AuthContextType extends AuthData {
@@ -59,6 +61,7 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
user: data?.user || defaultAuthData.user, user: data?.user || defaultAuthData.user,
metadata: data?.metadata || defaultAuthData.metadata, metadata: data?.metadata || defaultAuthData.metadata,
roles: data?.roles || defaultAuthData.roles, roles: data?.roles || defaultAuthData.roles,
phone: data?.phone || "",
set, set,
}), }),
[data, defaultAuthData] [data, defaultAuthData]

View File

@@ -107,7 +107,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
</Text> </Text>
<Stack gap={1}> <Stack gap={1}>
{match.home?.players.map((p) => ( {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} {p.first_name} {p.last_name}
</Text> </Text>
))} ))}
@@ -163,7 +163,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
</Text> </Text>
<Stack gap={1}> <Stack gap={1}>
{match.away?.players.map((p) => ( {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} {p.first_name} {p.last_name}
</Text> </Text>
))} ))}

View File

@@ -21,11 +21,18 @@ export const fetchMe = createServerFn()
return { return {
user: result || undefined, user: result || undefined,
roles: context.roles, roles: context.roles,
metadata: context.metadata metadata: context.metadata,
phone: context.phone
}; };
} catch (error: any) { } catch (error: any) {
logger.info('fetchMe: Session error', error.message); logger.info("FetchMe: Session error", error)
return { user: undefined, roles: [], metadata: {} }; 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 };
} }
}) })
); );
@@ -146,4 +153,4 @@ export const getUnenrolledPlayers = createServerFn()
.middleware([superTokensFunctionMiddleware]) .middleware([superTokensFunctionMiddleware])
.handler(async ({ data: tournamentId }) => .handler(async ({ data: tournamentId }) =>
toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId)) toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId))
); );

View File

@@ -58,15 +58,12 @@ const EmojiBar = ({
return reaction.players.map(p => p.id).includes(user?.id || ""); return reaction.players.map(p => p.id).includes(user?.id || "");
}, [user?.id]); }, [user?.id]);
// Get emojis the current user has reacted to
const userReactions = reactions?.filter(r => hasReacted(r)).map(r => r.emoji) || []; const userReactions = reactions?.filter(r => hasReacted(r)).map(r => r.emoji) || [];
if (!reactions) return; if (!reactions) return;
// Sort reactions by count (descending)
const sortedReactions = [...reactions].sort((a, b) => b.count - a.count); 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 visibleReactions = sortedReactions.slice(0, 3);
const groupedReactions = sortedReactions.slice(3); const groupedReactions = sortedReactions.slice(3);

View File

@@ -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 { getMatchReactions, toggleMatchReaction } from "@/features/matches/server";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
@@ -14,7 +14,7 @@ export const reactionQueries = {
}; };
export const useMatchReactions = (matchId: string) => export const useMatchReactions = (matchId: string) =>
useServerQuery(reactionQueries.match(matchId)); useServerSuspenseQuery(reactionQueries.match(matchId));
export const useToggleMatchReaction = (matchId: string) => { export const useToggleMatchReaction = (matchId: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -27,4 +27,4 @@ export const useToggleMatchReaction = (matchId: string) => {
}); });
}, },
}); });
}; };

View File

@@ -69,6 +69,7 @@ const TeamList = ({ teams, loading = false }: TeamListProps) => {
{teams?.map((team) => ( {teams?.map((team) => (
<div key={team.id}> <div key={team.id}>
<ListItem <ListItem
key={`team-list-${team.id}`}
p="xs" p="xs"
icon={ icon={
<Avatar <Avatar

View File

@@ -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;

View File

@@ -2,11 +2,23 @@ import Button from "@/components/button";
import Sheet from "@/components/sheet/sheet"; import Sheet from "@/components/sheet/sheet";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { useSheet } from "@/hooks/use-sheet"; 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 { 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 ( return (
<> <>
<Button variant="subtle" size="sm" onClick={open}> <Button variant="subtle" size="sm" onClick={open}>
@@ -14,13 +26,19 @@ const EnrollFreeAgent = () => {
</Button> </Button>
<Sheet title="Free Agent Enrollment" opened={isOpen} onChange={toggle}> <Sheet title="Free Agent Enrollment" opened={isOpen} onChange={toggle}>
<Text size="md" mb="md"> <Stack gap="xs">
Enrolling as a free agent will enter you in a pool of players wanting to play but don't have a teammate yet. <Text size="md">
</Text> Enrolling as a free agent will enter you in a pool of players wanting to play but don't have a teammate yet.
<Text size="sm" mb="md" c='dimmed'> </Text>
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'>
</Text> 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.
<Button onClick={console.log}>Confirm</Button> </Text>
<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> </Sheet>
</> </>
); );

View File

@@ -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;

View File

@@ -14,10 +14,6 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
() => new Date(tournament.start_time), () => new Date(tournament.start_time),
[tournament.start_time] [tournament.start_time]
); );
const teamCount = useMemo(
() => tournament.teams?.length || 0,
[tournament.teams]
);
return ( return (
<Stack align="center" gap={0}> <Stack align="center" gap={0}>

View File

@@ -15,8 +15,9 @@ import TeamCard from "@/features/teams/components/team-card";
import UpdateTeam from "./update-team"; import UpdateTeam from "./update-team";
import UnenrollTeam from "./unenroll-team"; import UnenrollTeam from "./unenroll-team";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { tournamentKeys } from "../../queries"; import { tournamentKeys, useFreeAgents } from "../../queries";
import RulesListButton from "./rules-list-button"; import RulesListButton from "./rules-list-button";
import EnrolledFreeAgent from "./enrolled-free-agent";
const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
tournament, tournament,
@@ -40,57 +41,79 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const handleSubmit = () => { 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 ( return (
<Stack gap="lg"> <Stack gap="lg">
<Header tournament={tournament} /> <Header tournament={tournament} />
{tournament.desc && <Text size="sm">{tournament.desc}</Text>}
<Card withBorder radius="lg" p="lg"> <Stack px="md">
<Stack gap="xs"> {tournament.desc && <Text size="sm">{tournament.desc}</Text>}
<Group mb="sm" gap="xs" align="center">
<UsersIcon size={16} /> <Card withBorder radius="lg" p="lg">
<Text size="sm" fw={500}> <Stack gap="xs">
Enrollment <Group mb="sm" gap="xs" align="center">
</Text> <UsersIcon size={16} />
{isEnrollmentOpen && ( <Text size="sm" fw={500}>
<Box ml="auto"> Enrollment
<Countdown </Text>
date={enrollmentDeadline} {isEnrollmentOpen && (
label="Time left" <Box ml="auto">
color="yellow" <Countdown
/> date={enrollmentDeadline}
</Box> label="Time left"
color="yellow"
/>
</Box>
)}
</Group>
{!isUserEnrolled && !isEnrollmentOpen && (
<Text fw={600} c="dimmed" size="sm">
Enrollment has been closed for this tournament.
</Text>
)} )}
</Group>
{!isUserEnrolled &&!isEnrollmentOpen && ( {!isUserEnrolled && isEnrollmentOpen && !isFreeAgent && (
<Text fw={600} c="dimmed" size="sm"> <>
Enrollment has been closed for this tournament. <EnrollTeam
</Text> tournamentId={tournament.id}
)} onSubmit={handleSubmit}
/>
<Divider my={0} label="or" />
<EnrollFreeAgent tournamentId={tournament.id} />
</>
)}
{!isUserEnrolled &&isEnrollmentOpen && ( {isUserEnrolled && (
<> <>
<EnrollTeam tournamentId={tournament.id} onSubmit={handleSubmit} /> <Suspense fallback={<TeamCardSkeleton />}>
<Divider my={0} label="or" /> <TeamCard teamId={userTeam.id} />
<EnrollFreeAgent /> </Suspense>
</> <UpdateTeam tournamentId={tournament.id} teamId={userTeam.id} />
)} {isEnrollmentOpen && (
<UnenrollTeam
tournamentId={tournament.id}
teamId={userTeam.id}
onSubmit={handleSubmit}
/>
)}
</>
)}
{ {
isUserEnrolled && <> isFreeAgent && isEnrollmentOpen && (
<Suspense fallback={<TeamCardSkeleton />}> <EnrolledFreeAgent tournamentId={tournament.id} />
<TeamCard teamId={userTeam.id} /> )
</Suspense> }
<UpdateTeam tournamentId={tournament.id} teamId={userTeam.id} />
{ isEnrollmentOpen && <UnenrollTeam tournamentId={tournament.id} teamId={userTeam.id} onSubmit={handleSubmit} />} </Stack>
</> </Card>
} </Stack>
</Stack>
</Card>
<Box> <Box>
<Divider /> <Divider />
@@ -102,10 +125,10 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
/> />
)} )}
<ListLink <ListLink
label={`View Bracket`} label={`View Bracket`}
to={`/tournaments/${tournament.id}/bracket`} to={`/tournaments/${tournament.id}/bracket`}
Icon={TreeStructureIcon} Icon={TreeStructureIcon}
/> />
<RulesListButton tournamentId={tournament.id} /> <RulesListButton tournamentId={tournament.id} />
<TeamListButton teams={tournament.teams || []} /> <TeamListButton teams={tournament.teams || []} />
</Box> </Box>

View File

@@ -20,6 +20,7 @@ const RulesListButton: React.FC<RulesListButtonProps> = ({ tournamentId }) => {
extensions: [StarterKit], extensions: [StarterKit],
content: tournament?.rules || '', content: tournament?.rules || '',
editable: false, editable: false,
immediatelyRender: false,
}); });
return ( return (

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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"; import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
export const tournamentKeys = { export const tournamentKeys = {
list: ['tournaments', 'list'] as const, list: ['tournaments', 'list'] as const,
details: (id: string) => ['tournaments', 'details', id] as const, details: (id: string) => ['tournaments', 'details', id] as const,
current: ['tournaments', 'current'] 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 = { export const tournamentQueries = {
@@ -24,6 +25,10 @@ export const tournamentQueries = {
unenrolled: (id: string) => ({ unenrolled: (id: string) => ({
queryKey: tournamentKeys.unenrolled(id), queryKey: tournamentKeys.unenrolled(id),
queryFn: () => getUnenrolledTeams({ data: 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) => export const useUnenrolledTeams = (tournamentId: string) =>
useServerSuspenseQuery(tournamentQueries.unenrolled(tournamentId)); useServerSuspenseQuery(tournamentQueries.unenrolled(tournamentId));
export const useFreeAgents = (tournamentId: string) =>
useServerSuspenseQuery(tournamentQueries.free_agents(tournamentId));

View File

@@ -83,4 +83,39 @@ export const getUnenrolledTeams = createServerFn()
.middleware([superTokensAdminFunctionMiddleware]) .middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data: tournamentId }) => .handler(async ({ data: tournamentId }) =>
toServerResult(() => pbAdmin.getUnenrolledTeams(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 });
})
);

View File

@@ -2,12 +2,9 @@ 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 { tournamentQueries } from "@/features/tournaments/queries";
import { reactionKeys, reactionQueries } from "@/features/reactions/queries"; import { reactionKeys, reactionQueries } from "@/features/reactions/queries";
let newIdeasAvailable = false;
let newIdeasCallbacks: (() => void)[] = [];
const logger = new Logger('ServerEvents'); const logger = new Logger('ServerEvents');
type SSEEvent = { type SSEEvent = {
@@ -19,45 +16,19 @@ type EventHandler = (event: SSEEvent, queryClient: ReturnType<typeof useQueryCli
const eventHandlers: Record<string, EventHandler> = { const eventHandlers: Record<string, EventHandler> = {
"connected": () => { "connected": () => {
logger.info("ServerEvents | New Connection"); logger.info("New Connection");
}, },
"ping": () => {}, "ping": () => {},
"test": (event, queryClient) => {
},
"match": (event, queryClient) => { "match": (event, queryClient) => {
queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId)) queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId))
queryClient.invalidateQueries(tournamentQueries.current()) queryClient.invalidateQueries(tournamentQueries.current())
}, },
"reaction": (event, queryClient) => { "reaction": (event, queryClient) => {
queryClient.invalidateQueries(reactionQueries.match(event.matchId)); queryClient.invalidateQueries(reactionQueries.match(event.matchId));
queryClient.setQueryData(reactionKeys.match(event.matchId), () => event.reactions); 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() { export function useServerEvents() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { user } = useAuth(); const { user } = useAuth();

View File

@@ -105,13 +105,11 @@ export function createPlayersService(pb: PocketBase) {
async getUnenrolledPlayers(tournamentId: string): Promise<Player[]> { async getUnenrolledPlayers(tournamentId: string): Promise<Player[]> {
try { try {
// Get the tournament with its enrolled teams
const tournament = await pb.collection("tournaments").getOne(tournamentId, { const tournament = await pb.collection("tournaments").getOne(tournamentId, {
fields: "teams", fields: "teams",
expand: "teams,teams.players" expand: "teams,teams.players"
}); });
// Extract player IDs from all enrolled teams
const enrolledPlayerIds: string[] = []; const enrolledPlayerIds: string[] = [];
if (tournament.expand?.teams) { if (tournament.expand?.teams) {
const teams = Array.isArray(tournament.expand.teams) ? tournament.expand.teams : [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) { if (enrolledPlayerIds.length === 0) {
const allPlayers = await pb.collection("players").getFullList<Player>({ const allPlayers = await pb.collection("players").getFullList<Player>({
fields: "id,first_name,last_name,email", fields: "id,first_name,last_name,email",
@@ -133,7 +130,6 @@ export function createPlayersService(pb: PocketBase) {
return allPlayers.map(transformPlayer); return allPlayers.map(transformPlayer);
} }
// Build filter to exclude enrolled players
const filter = enrolledPlayerIds const filter = enrolledPlayerIds
.map((playerId: string) => `id != "${playerId}"`) .map((playerId: string) => `id != "${playerId}"`)
.join(" && "); .join(" && ");
@@ -146,7 +142,6 @@ export function createPlayersService(pb: PocketBase) {
return availablePlayers.map(transformPlayer); return availablePlayers.map(transformPlayer);
} catch (error) { } catch (error) {
console.error("Error getting unenrolled players:", 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>({ const allPlayers = await pb.collection("players").getFullList<Player>({
fields: "id,first_name,last_name,email", fields: "id,first_name,last_name,email",
}); });

View File

@@ -7,8 +7,10 @@ import type {
} from "@/features/tournaments/types"; } from "@/features/tournaments/types";
import type { Team } from "@/features/teams/types"; import type { Team } from "@/features/teams/types";
import PocketBase from "pocketbase"; 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 { transformTeam } from "@/lib/pocketbase/util/transform-types";
import { getFreeAgents } from "@/features/tournaments/server";
import { PlayerInfo } from "@/features/players/types";
export function createTournamentsService(pb: PocketBase) { export function createTournamentsService(pb: PocketBase) {
return { return {
@@ -133,5 +135,33 @@ export function createTournamentsService(pb: PocketBase) {
throw error; 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;
}
},
}; };
} }

View File

@@ -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 { export function transformTeam(record: any): Team {
const players = const players =
record.expand?.players record.expand?.players

View File

@@ -1,27 +1,32 @@
import { getSessionForSSR } from "supertokens-node/custom"; import { getSessionForSSR } from "supertokens-node/custom";
import { ensureSuperTokensBackend } from "../server"; import { ensureSuperTokensBackend } from "../server";
import { logger } from "../"; import { logger } from "../";
import SuperTokens from "supertokens-node";
export async function getSessionForStart(request: Request, options?: { sessionRequired?: boolean }) { export async function getSessionForStart(request: Request, options?: { sessionRequired?: boolean }) {
ensureSuperTokensBackend(); ensureSuperTokensBackend();
try { try {
const session = await getSessionForSSR(request); const session = await getSessionForSSR(request);
if (session.hasToken) { 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 { return {
hasToken: true, hasToken: true,
needsRefresh: true, needsRefresh: true,
error: 'TRY_REFRESH_TOKEN' error: 'TRY_REFRESH_TOKEN'
} }
} }
const user = await SuperTokens.getUser(userId);
return { return {
hasToken: true, hasToken: true,
accessTokenPayload: session.accessTokenPayload, accessTokenPayload: session.accessTokenPayload,
userId: session.accessTokenPayload?.sub, userId,
sessionHandle: session.accessTokenPayload?.sessionHandle, sessionHandle: session.accessTokenPayload?.sessionHandle,
phone: user?.phoneNumbers[0]
}; };
} }

View File

@@ -60,6 +60,7 @@ export const verifySuperTokensSession = async (
userAuthId, userAuthId,
roles, roles,
metadata, metadata,
phone: session.phone,
session: { session: {
accessTokenPayload: session.accessTokenPayload, accessTokenPayload: session.accessTokenPayload,
sessionHandle: session.sessionHandle, 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); const session = await verifySuperTokensSession(request);
if (session.context.session?.tryRefresh) { if (session.context.session?.tryRefresh) {
@@ -93,6 +94,7 @@ export const getSessionContext = async (request: Request, options?: { isServerFu
userAuthId: session.context.userAuthId, userAuthId: session.context.userAuthId,
roles: session.context.roles, roles: session.context.roles,
metadata: session.context.metadata, metadata: session.context.metadata,
phone: session.context.phone
}; };
return context; return context;