From b52c79772f62aba225c70e658a518757ac5bae1e Mon Sep 17 00:00:00 2001 From: yohlo Date: Thu, 2 Oct 2025 21:58:20 -0500 Subject: [PATCH] activities --- src/app/routeTree.gen.ts | 21 ++ src/app/routes/_authed/admin/activities.tsx | 24 ++ .../components/activities-table.tsx | 222 ++++++++++++++++++ src/features/activities/index.ts | 3 + src/features/activities/queries.ts | 17 ++ src/features/activities/server.ts | 29 +++ src/features/activities/types.ts | 1 + src/features/admin/components/admin-page.tsx | 6 + src/lib/pocketbase/services/activities.ts | 65 ++++- src/lib/pocketbase/util/transform-types.ts | 16 ++ 10 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 src/app/routes/_authed/admin/activities.tsx create mode 100644 src/features/activities/components/activities-table.tsx create mode 100644 src/features/activities/index.ts create mode 100644 src/features/activities/queries.ts create mode 100644 src/features/activities/server.ts create mode 100644 src/features/activities/types.ts diff --git a/src/app/routeTree.gen.ts b/src/app/routeTree.gen.ts index bd4aa4c..b8bfcc6 100644 --- a/src/app/routeTree.gen.ts +++ b/src/app/routeTree.gen.ts @@ -34,6 +34,7 @@ import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$t import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId' import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview' import { Route as AuthedAdminBadgesRouteImport } from './routes/_authed/admin/badges' +import { Route as AuthedAdminActivitiesRouteImport } from './routes/_authed/admin/activities' import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index' import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket' import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index' @@ -167,6 +168,11 @@ const AuthedAdminBadgesRoute = AuthedAdminBadgesRouteImport.update({ path: '/badges', getParentRoute: () => AuthedAdminRoute, } as any) +const AuthedAdminActivitiesRoute = AuthedAdminActivitiesRouteImport.update({ + id: '/activities', + path: '/activities', + getParentRoute: () => AuthedAdminRoute, +} as any) const AuthedAdminTournamentsIndexRoute = AuthedAdminTournamentsIndexRouteImport.update({ id: '/tournaments/', @@ -212,6 +218,7 @@ export interface FileRoutesByFullPath { '/settings': typeof AuthedSettingsRoute '/stats': typeof AuthedStatsRoute '/': typeof AuthedIndexRoute + '/admin/activities': typeof AuthedAdminActivitiesRoute '/admin/badges': typeof AuthedAdminBadgesRoute '/admin/preview': typeof AuthedAdminPreviewRoute '/profile/$playerId': typeof AuthedProfilePlayerIdRoute @@ -243,6 +250,7 @@ export interface FileRoutesByTo { '/settings': typeof AuthedSettingsRoute '/stats': typeof AuthedStatsRoute '/': typeof AuthedIndexRoute + '/admin/activities': typeof AuthedAdminActivitiesRoute '/admin/badges': typeof AuthedAdminBadgesRoute '/admin/preview': typeof AuthedAdminPreviewRoute '/profile/$playerId': typeof AuthedProfilePlayerIdRoute @@ -277,6 +285,7 @@ export interface FileRoutesById { '/_authed/settings': typeof AuthedSettingsRoute '/_authed/stats': typeof AuthedStatsRoute '/_authed/': typeof AuthedIndexRoute + '/_authed/admin/activities': typeof AuthedAdminActivitiesRoute '/_authed/admin/badges': typeof AuthedAdminBadgesRoute '/_authed/admin/preview': typeof AuthedAdminPreviewRoute '/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute @@ -311,6 +320,7 @@ export interface FileRouteTypes { | '/settings' | '/stats' | '/' + | '/admin/activities' | '/admin/badges' | '/admin/preview' | '/profile/$playerId' @@ -342,6 +352,7 @@ export interface FileRouteTypes { | '/settings' | '/stats' | '/' + | '/admin/activities' | '/admin/badges' | '/admin/preview' | '/profile/$playerId' @@ -375,6 +386,7 @@ export interface FileRouteTypes { | '/_authed/settings' | '/_authed/stats' | '/_authed/' + | '/_authed/admin/activities' | '/_authed/admin/badges' | '/_authed/admin/preview' | '/_authed/profile/$playerId' @@ -595,6 +607,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedAdminBadgesRouteImport parentRoute: typeof AuthedAdminRoute } + '/_authed/admin/activities': { + id: '/_authed/admin/activities' + path: '/activities' + fullPath: '/admin/activities' + preLoaderRoute: typeof AuthedAdminActivitiesRouteImport + parentRoute: typeof AuthedAdminRoute + } '/_authed/admin/tournaments/': { id: '/_authed/admin/tournaments/' path: '/tournaments' @@ -641,6 +660,7 @@ declare module '@tanstack/react-router' { } interface AuthedAdminRouteChildren { + AuthedAdminActivitiesRoute: typeof AuthedAdminActivitiesRoute AuthedAdminBadgesRoute: typeof AuthedAdminBadgesRoute AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute @@ -651,6 +671,7 @@ interface AuthedAdminRouteChildren { } const AuthedAdminRouteChildren: AuthedAdminRouteChildren = { + AuthedAdminActivitiesRoute: AuthedAdminActivitiesRoute, AuthedAdminBadgesRoute: AuthedAdminBadgesRoute, AuthedAdminPreviewRoute: AuthedAdminPreviewRoute, AuthedAdminIndexRoute: AuthedAdminIndexRoute, diff --git a/src/app/routes/_authed/admin/activities.tsx b/src/app/routes/_authed/admin/activities.tsx new file mode 100644 index 0000000..e9141d1 --- /dev/null +++ b/src/app/routes/_authed/admin/activities.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch"; +import { ActivitiesTable, activityQueries } from "@/features/activities"; + +export const Route = createFileRoute("/_authed/admin/activities")({ + component: Stats, + beforeLoad: ({ context }) => { + const queryClient = context.queryClient; + prefetchServerQuery(queryClient, activityQueries.search()); + }, + loader: () => ({ + withPadding: false, + fullWidth: true, + header: { + title: "Activities", + withBackButton: true, + }, + refresh: [activityQueries.search().queryKey], + }), +}); + +function Stats() { + return ; +} diff --git a/src/features/activities/components/activities-table.tsx b/src/features/activities/components/activities-table.tsx new file mode 100644 index 0000000..8cb8529 --- /dev/null +++ b/src/features/activities/components/activities-table.tsx @@ -0,0 +1,222 @@ +import { useState, useMemo, memo } from "react"; +import { + Text, + TextInput, + Stack, + Group, + Box, + Container, + Divider, + UnstyledButton, + Badge, + Select, + Pagination, +} from "@mantine/core"; +import { + MagnifyingGlassIcon, + CaretUpIcon, + CaretDownIcon, + CheckIcon, + XIcon, +} from "@phosphor-icons/react"; +import { Activity, ActivitySearchParams } from "../types"; +import { useActivities } from "../queries"; + +interface ActivityListItemProps { + activity: Activity; +} + +const ActivityListItem = memo(({ activity }: ActivityListItemProps) => { + const playerName = typeof activity.player === "object" && activity.player + ? `${activity.player.first_name} ${activity.player.last_name}` + : "System"; + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleString(); + }; + + return ( + + + + + + {activity.name} + + {activity.success ? ( + + ) : ( + + )} + + + + {playerName} + + + {activity.duration}ms + + + {formatDate(activity.created)} + + + {activity.error && ( + + {activity.error} + + )} + + + + ); +}); + +const ActivitiesResults = ({ searchParams, page, setPage }: any) => { + const { data: result } = useActivities(searchParams); + return ( + <> + + {result.items.map((activity: Activity, index: number) => ( + + + {index < result.items.length - 1 && } + + ))} + + + {result.items.length === 0 && ( + + No activities found + + )} + + {result.totalPages > 1 && ( + + + + )} + + ) +}; + +export const ActivitiesTable = () => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(100); + const [search, setSearch] = useState(""); + const [successFilter, setSuccessFilter] = useState(null); + const [sortBy, setSortBy] = useState("-created"); + + const searchParams: ActivitySearchParams = useMemo( + () => ({ + page, + perPage, + name: search || undefined, + success: successFilter === "success" ? true : successFilter === "failure" ? false : undefined, + sortBy, + }), + [page, perPage, search, successFilter, sortBy] + ); + + const { data: result } = useActivities(searchParams); + + const handleSort = (field: string) => { + setSortBy((prev) => { + if (prev === field) return `-${field}`; + if (prev === `-${field}`) return field; + return `-${field}`; + }); + }; + + const getSortIcon = (field: string) => { + if (sortBy === field) return ; + if (sortBy === `-${field}`) return ; + return null; + }; + + return ( + + + + { + setSearch(e.currentTarget.value); + setPage(1); + }} + leftSection={} + size="md" + /> + + +