diff --git a/pb_migrations/1760127117_updated_players.js b/pb_migrations/1760127117_updated_players.js
new file mode 100644
index 0000000..f748717
--- /dev/null
+++ b/pb_migrations/1760127117_updated_players.js
@@ -0,0 +1,26 @@
+///
+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)
+})
diff --git a/src/app/routes/_authed/admin/activities.tsx b/src/app/routes/_authed/admin/activities.tsx
index e9141d1..e1ee2ba 100644
--- a/src/app/routes/_authed/admin/activities.tsx
+++ b/src/app/routes/_authed/admin/activities.tsx
@@ -1,12 +1,16 @@
import { createFileRoute } from "@tanstack/react-router";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
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")({
component: Stats,
beforeLoad: ({ context }) => {
const queryClient = context.queryClient;
prefetchServerQuery(queryClient, activityQueries.search());
+ prefetchServerQuery(queryClient, playerQueries.activity());
},
loader: () => ({
withPadding: false,
@@ -15,10 +19,27 @@ export const Route = createFileRoute("/_authed/admin/activities")({
title: "Activities",
withBackButton: true,
},
- refresh: [activityQueries.search().queryKey],
+ refresh: [activityQueries.search().queryKey, playerQueries.activity().queryKey],
}),
});
function Stats() {
- return ;
+ const [activeTab, setActiveTab] = useState("server-functions");
+
+ return (
+
+
+ Server Functions
+ Player Activity
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/src/components/button.tsx b/src/components/button.tsx
index 68b6dab..9224a52 100644
--- a/src/components/button.tsx
+++ b/src/components/button.tsx
@@ -10,5 +10,4 @@ const Button = forwardRef((props, ref) => {
return ;
});
-Button.displayName = "Button";
export default Button;
diff --git a/src/features/activities/components/activities-table.tsx b/src/features/activities/components/activities-table.tsx
index 2b07a88..dbbb381 100644
--- a/src/features/activities/components/activities-table.tsx
+++ b/src/features/activities/components/activities-table.tsx
@@ -92,8 +92,6 @@ const ActivityListItem = memo(({ activity, onClick }: ActivityListItemProps) =>
);
});
-ActivityListItem.displayName = "ActivityListItem";
-
interface ActivityDetailsSheetProps {
activity: Activity | null;
isOpen: boolean;
@@ -205,8 +203,6 @@ const ActivityDetailsSheet = memo(({ activity, isOpen, onClose }: ActivityDetail
);
});
-ActivityDetailsSheet.displayName = "ActivityDetailsSheet";
-
const ActivitiesResults = ({ searchParams, page, setPage, onActivityClick }: any) => {
const { data: result } = useActivities(searchParams);
return (
diff --git a/src/features/players/components/player-stats-table.tsx b/src/features/players/components/player-stats-table.tsx
index f16d95b..8195f36 100644
--- a/src/features/players/components/player-stats-table.tsx
+++ b/src/features/players/components/player-stats-table.tsx
@@ -141,8 +141,6 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr }: PlayerListItemProps)
);
});
-PlayerListItem.displayName = 'PlayerListItem';
-
const PlayerStatsTable = () => {
const { data: playerStats } = useAllPlayerStats();
const navigate = useNavigate();
diff --git a/src/features/players/components/players-activity-table.tsx b/src/features/players/components/players-activity-table.tsx
new file mode 100644
index 0000000..ba64f8f
--- /dev/null
+++ b/src/features/players/components/players-activity-table.tsx
@@ -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 (
+
+
+
+
+
+ {playerName}
+
+ {isActive && (
+
+ )}
+
+
+
+ {getTimeSince(player.last_activity)}
+
+ {player.last_activity && (
+
+ {formatDate(player.last_activity)}
+
+ )}
+
+
+
+
+ );
+});
+
+export const PlayersActivityTable = () => {
+ const { data: players } = usePlayersActivity();
+
+ return (
+
+
+
+
+ {players.length} players
+
+
+
+
+ {players.map((player: Player, index: number) => (
+
+
+ {index < players.length - 1 && }
+
+ ))}
+
+
+ {players.length === 0 && (
+
+ No player activity found
+
+ )}
+
+
+ );
+};
diff --git a/src/features/players/index.ts b/src/features/players/index.ts
index d847586..b551ff7 100644
--- a/src/features/players/index.ts
+++ b/src/features/players/index.ts
@@ -1,3 +1,5 @@
import { Logger } from "@/lib/logger";
-export const logger = new Logger('Players');
\ No newline at end of file
+export const logger = new Logger('Players');
+export * from "./queries";
+export { PlayersActivityTable } from "./components/players-activity-table";
\ No newline at end of file
diff --git a/src/features/players/queries.ts b/src/features/players/queries.ts
index ee4170f..3a871b4 100644
--- a/src/features/players/queries.ts
+++ b/src/features/players/queries.ts
@@ -1,5 +1,5 @@
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 = {
auth: ['auth'],
@@ -10,6 +10,7 @@ export const playerKeys = {
stats: (id: string) => ['players', 'stats', id],
allStats: ['players', 'stats', 'all'],
matches: (id: string) => ['players', 'matches', id],
+ activity: ['players', 'activity'],
};
export const playerQueries = {
@@ -45,6 +46,10 @@ export const playerQueries = {
queryKey: playerKeys.matches(id),
queryFn: async () => await getPlayerMatches({ data: id })
}),
+ activity: () => ({
+ queryKey: playerKeys.activity,
+ queryFn: async () => await getPlayersActivity()
+ }),
};
export const useMe = () => {
@@ -89,4 +94,7 @@ export const usePlayerMatches = (id: string) =>
useServerSuspenseQuery(playerQueries.matches(id));
export const useUnenrolledPlayers = (tournamentId: string) =>
- useServerSuspenseQuery(playerQueries.unenrolled(tournamentId));
\ No newline at end of file
+ useServerSuspenseQuery(playerQueries.unenrolled(tournamentId));
+
+export const usePlayersActivity = () =>
+ useServerSuspenseQuery(playerQueries.activity());
\ No newline at end of file
diff --git a/src/features/players/server.ts b/src/features/players/server.ts
index d316b0d..399dba3 100644
--- a/src/features/players/server.ts
+++ b/src/features/players/server.ts
@@ -161,3 +161,9 @@ export const getUnenrolledPlayers = createServerFn()
.handler(async ({ data: tournamentId }) =>
toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId))
);
+
+export const getPlayersActivity = createServerFn()
+ .middleware([superTokensFunctionMiddleware])
+ .handler(async () =>
+ toServerResult(async () => await pbAdmin.getPlayersActivity())
+ );
diff --git a/src/features/players/types.ts b/src/features/players/types.ts
index f850715..bf7bba0 100644
--- a/src/features/players/types.ts
+++ b/src/features/players/types.ts
@@ -14,6 +14,7 @@ export interface Player {
last_name?: string;
created?: string;
updated?: string;
+ last_activity?: string;
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"),
});
-export const playerUpdateSchema = playerInputSchema.partial();
+export const playerUpdateSchema = playerInputSchema.extend({
+ last_activity: z.string().optional(),
+}).partial();
export type PlayerInput = z.infer;
export type PlayerUpdateInput = z.infer;
diff --git a/src/features/tournaments/components/upcoming-tournament/enroll-team/team-selection-view.tsx b/src/features/tournaments/components/upcoming-tournament/enroll-team/team-selection-view.tsx
index 95a3bb7..d4e3956 100644
--- a/src/features/tournaments/components/upcoming-tournament/enroll-team/team-selection-view.tsx
+++ b/src/features/tournaments/components/upcoming-tournament/enroll-team/team-selection-view.tsx
@@ -79,6 +79,4 @@ const TeamSelectionView: React.FC = React.memo(({
);
});
-TeamSelectionView.displayName = 'TeamSelectionView';
-
export default TeamSelectionView;
\ No newline at end of file
diff --git a/src/lib/pocketbase/services/players.ts b/src/lib/pocketbase/services/players.ts
index cdd2783..89a720d 100644
--- a/src/lib/pocketbase/services/players.ts
+++ b/src/lib/pocketbase/services/players.ts
@@ -166,5 +166,13 @@ export function createPlayersService(pb: PocketBase) {
return allPlayers.map(transformPlayer);
}
},
+
+ async getPlayersActivity(): Promise {
+ const result = await pb.collection("players").getFullList({
+ sort: "-last_activity",
+ fields: "id,first_name,last_name,auth_id,last_activity",
+ });
+ return result.map(transformPlayer);
+ },
};
}
diff --git a/src/lib/pocketbase/util/transform-types.ts b/src/lib/pocketbase/util/transform-types.ts
index b80a4e0..03c583d 100644
--- a/src/lib/pocketbase/util/transform-types.ts
+++ b/src/lib/pocketbase/util/transform-types.ts
@@ -128,6 +128,7 @@ export function transformPlayer(record: any): Player {
auth_id: record.auth_id,
created: record.created,
updated: record.updated,
+ last_activity: record.last_activity,
teams,
};
}
diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts
index 8d7efc1..faeba78 100644
--- a/src/utils/supertokens.ts
+++ b/src/utils/supertokens.ts
@@ -11,6 +11,7 @@ import { getSessionForStart } from "@/lib/supertokens/recipes/start-session";
import { Logger } from "@/lib/logger";
import z from "zod";
import { serverFnLoggingMiddleware } from "./activities";
+import { pbAdmin } from "@/lib/pocketbase/client";
const logger = new Logger("Middleware");
const verifySuperTokensSession = async (
@@ -75,6 +76,17 @@ export const getSessionContext = createServerOnlyFn(async (request: Request, opt
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;
});