last activity for players
This commit is contained in:
26
pb_migrations/1760127117_updated_players.js
Normal file
26
pb_migrations/1760127117_updated_players.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(5, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date3558165700",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "last_activity",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("date3558165700")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||||
import { ActivitiesTable, activityQueries } from "@/features/activities";
|
import { ActivitiesTable, activityQueries } from "@/features/activities";
|
||||||
|
import { PlayersActivityTable, playerQueries } from "@/features/players";
|
||||||
|
import { Tabs } from "@mantine/core";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/activities")({
|
export const Route = createFileRoute("/_authed/admin/activities")({
|
||||||
component: Stats,
|
component: Stats,
|
||||||
beforeLoad: ({ context }) => {
|
beforeLoad: ({ context }) => {
|
||||||
const queryClient = context.queryClient;
|
const queryClient = context.queryClient;
|
||||||
prefetchServerQuery(queryClient, activityQueries.search());
|
prefetchServerQuery(queryClient, activityQueries.search());
|
||||||
|
prefetchServerQuery(queryClient, playerQueries.activity());
|
||||||
},
|
},
|
||||||
loader: () => ({
|
loader: () => ({
|
||||||
withPadding: false,
|
withPadding: false,
|
||||||
@@ -15,10 +19,27 @@ export const Route = createFileRoute("/_authed/admin/activities")({
|
|||||||
title: "Activities",
|
title: "Activities",
|
||||||
withBackButton: true,
|
withBackButton: true,
|
||||||
},
|
},
|
||||||
refresh: [activityQueries.search().queryKey],
|
refresh: [activityQueries.search().queryKey, playerQueries.activity().queryKey],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
function Stats() {
|
function Stats() {
|
||||||
return <ActivitiesTable />;
|
const [activeTab, setActiveTab] = useState<string | null>("server-functions");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
|
<Tabs.List mb='md'>
|
||||||
|
<Tabs.Tab value="server-functions">Server Functions</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="player-activity">Player Activity</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="server-functions">
|
||||||
|
<ActivitiesTable />
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="player-activity">
|
||||||
|
<PlayersActivityTable />
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
|||||||
return <MantineButton fullWidth ref={ref} {...props} />;
|
return <MantineButton fullWidth ref={ref} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
Button.displayName = "Button";
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
|||||||
@@ -92,8 +92,6 @@ const ActivityListItem = memo(({ activity, onClick }: ActivityListItemProps) =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ActivityListItem.displayName = "ActivityListItem";
|
|
||||||
|
|
||||||
interface ActivityDetailsSheetProps {
|
interface ActivityDetailsSheetProps {
|
||||||
activity: Activity | null;
|
activity: Activity | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -205,8 +203,6 @@ const ActivityDetailsSheet = memo(({ activity, isOpen, onClose }: ActivityDetail
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ActivityDetailsSheet.displayName = "ActivityDetailsSheet";
|
|
||||||
|
|
||||||
const ActivitiesResults = ({ searchParams, page, setPage, onActivityClick }: any) => {
|
const ActivitiesResults = ({ searchParams, page, setPage, onActivityClick }: any) => {
|
||||||
const { data: result } = useActivities(searchParams);
|
const { data: result } = useActivities(searchParams);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -141,8 +141,6 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr }: PlayerListItemProps)
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
PlayerListItem.displayName = 'PlayerListItem';
|
|
||||||
|
|
||||||
const PlayerStatsTable = () => {
|
const PlayerStatsTable = () => {
|
||||||
const { data: playerStats } = useAllPlayerStats();
|
const { data: playerStats } = useAllPlayerStats();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
118
src/features/players/components/players-activity-table.tsx
Normal file
118
src/features/players/components/players-activity-table.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
import {
|
||||||
|
Text,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
UnstyledButton,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Player } from "../types";
|
||||||
|
import { usePlayersActivity } from "../queries";
|
||||||
|
|
||||||
|
interface PlayerActivityItemProps {
|
||||||
|
player: Player;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlayerActivityItem = memo(({ player }: PlayerActivityItemProps) => {
|
||||||
|
const playerName = player.first_name && player.last_name
|
||||||
|
? `${player.first_name} ${player.last_name}`
|
||||||
|
: player.first_name || player.last_name || "Unknown Player";
|
||||||
|
|
||||||
|
const formatDate = (dateStr?: string) => {
|
||||||
|
if (!dateStr) return "Never";
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeSince = (dateStr?: string) => {
|
||||||
|
if (!dateStr) return "Never active";
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
|
||||||
|
if (diffMins < 1) return "Just now";
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
if (diffDays < 30) return `${diffDays}d ago`;
|
||||||
|
return formatDate(dateStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isActive = player.last_activity &&
|
||||||
|
(new Date().getTime() - new Date(player.last_activity).getTime()) < 5 * 60 * 1000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w="100%"
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
borderRadius: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
|
<Stack gap={4} flex={1}>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{playerName}
|
||||||
|
</Text>
|
||||||
|
{isActive && (
|
||||||
|
<Box
|
||||||
|
w={8}
|
||||||
|
h={8}
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "var(--mantine-color-green-6)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Group gap="md">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{getTimeSince(player.last_activity)}
|
||||||
|
</Text>
|
||||||
|
{player.last_activity && (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{formatDate(player.last_activity)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PlayersActivityTable = () => {
|
||||||
|
const { data: players } = usePlayersActivity();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="100%" px={0}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group px="md" justify="space-between" align="center">
|
||||||
|
<Text size="10px" lh={0} c="dimmed">
|
||||||
|
{players.length} players
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap={0}>
|
||||||
|
{players.map((player: Player, index: number) => (
|
||||||
|
<Box key={player.id}>
|
||||||
|
<PlayerActivityItem player={player} />
|
||||||
|
{index < players.length - 1 && <Divider />}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{players.length === 0 && (
|
||||||
|
<Text ta="center" c="dimmed" py="xl">
|
||||||
|
No player activity found
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import { Logger } from "@/lib/logger";
|
import { Logger } from "@/lib/logger";
|
||||||
|
|
||||||
export const logger = new Logger('Players');
|
export const logger = new Logger('Players');
|
||||||
|
export * from "./queries";
|
||||||
|
export { PlayersActivityTable } from "./components/players-activity-table";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||||
import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats, getAllPlayerStats, getPlayerMatches, getUnenrolledPlayers } from "./server";
|
import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats, getAllPlayerStats, getPlayerMatches, getUnenrolledPlayers, getPlayersActivity } from "./server";
|
||||||
|
|
||||||
export const playerKeys = {
|
export const playerKeys = {
|
||||||
auth: ['auth'],
|
auth: ['auth'],
|
||||||
@@ -10,6 +10,7 @@ export const playerKeys = {
|
|||||||
stats: (id: string) => ['players', 'stats', id],
|
stats: (id: string) => ['players', 'stats', id],
|
||||||
allStats: ['players', 'stats', 'all'],
|
allStats: ['players', 'stats', 'all'],
|
||||||
matches: (id: string) => ['players', 'matches', id],
|
matches: (id: string) => ['players', 'matches', id],
|
||||||
|
activity: ['players', 'activity'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const playerQueries = {
|
export const playerQueries = {
|
||||||
@@ -45,6 +46,10 @@ export const playerQueries = {
|
|||||||
queryKey: playerKeys.matches(id),
|
queryKey: playerKeys.matches(id),
|
||||||
queryFn: async () => await getPlayerMatches({ data: id })
|
queryFn: async () => await getPlayerMatches({ data: id })
|
||||||
}),
|
}),
|
||||||
|
activity: () => ({
|
||||||
|
queryKey: playerKeys.activity,
|
||||||
|
queryFn: async () => await getPlayersActivity()
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMe = () => {
|
export const useMe = () => {
|
||||||
@@ -89,4 +94,7 @@ export const usePlayerMatches = (id: string) =>
|
|||||||
useServerSuspenseQuery(playerQueries.matches(id));
|
useServerSuspenseQuery(playerQueries.matches(id));
|
||||||
|
|
||||||
export const useUnenrolledPlayers = (tournamentId: string) =>
|
export const useUnenrolledPlayers = (tournamentId: string) =>
|
||||||
useServerSuspenseQuery(playerQueries.unenrolled(tournamentId));
|
useServerSuspenseQuery(playerQueries.unenrolled(tournamentId));
|
||||||
|
|
||||||
|
export const usePlayersActivity = () =>
|
||||||
|
useServerSuspenseQuery(playerQueries.activity());
|
||||||
@@ -161,3 +161,9 @@ export const getUnenrolledPlayers = createServerFn()
|
|||||||
.handler(async ({ data: tournamentId }) =>
|
.handler(async ({ data: tournamentId }) =>
|
||||||
toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId))
|
toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getPlayersActivity = createServerFn()
|
||||||
|
.middleware([superTokensFunctionMiddleware])
|
||||||
|
.handler(async () =>
|
||||||
|
toServerResult<Player[]>(async () => await pbAdmin.getPlayersActivity())
|
||||||
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface Player {
|
|||||||
last_name?: string;
|
last_name?: string;
|
||||||
created?: string;
|
created?: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
|
last_activity?: string;
|
||||||
teams?: TeamInfo[];
|
teams?: TeamInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +24,9 @@ export const playerInputSchema = z.object({
|
|||||||
last_name: z.string().min(2).max(20).regex(/^[a-zA-Z0-9\s]+$/, "Last name must be 2-20 characters long and contain only letters and spaces"),
|
last_name: z.string().min(2).max(20).regex(/^[a-zA-Z0-9\s]+$/, "Last name must be 2-20 characters long and contain only letters and spaces"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const playerUpdateSchema = playerInputSchema.partial();
|
export const playerUpdateSchema = playerInputSchema.extend({
|
||||||
|
last_activity: z.string().optional(),
|
||||||
|
}).partial();
|
||||||
|
|
||||||
export type PlayerInput = z.infer<typeof playerInputSchema>;
|
export type PlayerInput = z.infer<typeof playerInputSchema>;
|
||||||
export type PlayerUpdateInput = z.infer<typeof playerUpdateSchema>;
|
export type PlayerUpdateInput = z.infer<typeof playerUpdateSchema>;
|
||||||
|
|||||||
@@ -79,6 +79,4 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
TeamSelectionView.displayName = 'TeamSelectionView';
|
|
||||||
|
|
||||||
export default TeamSelectionView;
|
export default TeamSelectionView;
|
||||||
@@ -166,5 +166,13 @@ export function createPlayersService(pb: PocketBase) {
|
|||||||
return allPlayers.map(transformPlayer);
|
return allPlayers.map(transformPlayer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getPlayersActivity(): Promise<Player[]> {
|
||||||
|
const result = await pb.collection("players").getFullList<Player>({
|
||||||
|
sort: "-last_activity",
|
||||||
|
fields: "id,first_name,last_name,auth_id,last_activity",
|
||||||
|
});
|
||||||
|
return result.map(transformPlayer);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export function transformPlayer(record: any): Player {
|
|||||||
auth_id: record.auth_id,
|
auth_id: record.auth_id,
|
||||||
created: record.created,
|
created: record.created,
|
||||||
updated: record.updated,
|
updated: record.updated,
|
||||||
|
last_activity: record.last_activity,
|
||||||
teams,
|
teams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
|
|||||||
import { Logger } from "@/lib/logger";
|
import { Logger } from "@/lib/logger";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { serverFnLoggingMiddleware } from "./activities";
|
import { serverFnLoggingMiddleware } from "./activities";
|
||||||
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
const logger = new Logger("Middleware");
|
const logger = new Logger("Middleware");
|
||||||
|
|
||||||
const verifySuperTokensSession = async (
|
const verifySuperTokensSession = async (
|
||||||
@@ -75,6 +76,17 @@ export const getSessionContext = createServerOnlyFn(async (request: Request, opt
|
|||||||
phone: session.context.phone
|
phone: session.context.phone
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const player = await pbAdmin.getPlayerByAuthId(session.context.userAuthId);
|
||||||
|
if (player) {
|
||||||
|
await pbAdmin.updatePlayer(player.id, {
|
||||||
|
last_activity: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to update player last_activity", error);
|
||||||
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user