Skip to content

Commit

Permalink
Create recent runs table. Cleanup / improve other tables.
Browse files Browse the repository at this point in the history
  • Loading branch information
leighmacdonald committed Jan 3, 2025
1 parent 00e2199 commit 20871b8
Show file tree
Hide file tree
Showing 39 changed files with 1,199 additions and 314 deletions.
48 changes: 45 additions & 3 deletions frontend/src/api/speedruns.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Duration, TimeStamped, transformCreatedOnDate } from '../util/time.ts';
import { apiCall } from './common.ts';
import { UserProfile } from './profile.ts';

export type MapDetail = {
map_id: number;
Expand All @@ -9,20 +10,26 @@ export type MapDetail = {
export type SpeedrunPointCaptures = {
speedrun_id: number;
round_id: number;
players: SpeedrunPointCaptures[];
players: SpeedrunParticipant[];
duration: Duration;
point_name: string;
};
export type SpeedrunParticipant = {
person: UserProfile;
round_id: number;
steam_id: string;
kills: number;
destructions: number;
duration: Duration;
persona_name: string;
avatar_hash: string;
};

export type SpeedrunResult = {
speedrun_id: number;
server_id: number;
rank: number;
initial_rank: number;
map_detail: MapDetail;
point_captures: SpeedrunPointCaptures[];
players: SpeedrunParticipant[];
Expand All @@ -33,6 +40,20 @@ export type SpeedrunResult = {
category: string;
};

export type SpeedrunMapOverview = {
speedrun_id: number;
server_id: number;
rank: number;
initial_rank: number;
map_detail: MapDetail;
duration: Duration;
player_count: number;
bot_count: number;
created_on: Date;
category: string;
total_players: number;
};

export const getSpeedrunsTopOverall = async (count: number = 5) => {
const results = await apiCall<Record<string, SpeedrunResult[]>>(`/api/speedruns/overall/top?count=${count}`, 'GET');
for (const key of Object.keys(results)) {
Expand All @@ -41,6 +62,27 @@ export const getSpeedrunsTopOverall = async (count: number = 5) => {
return results;
};

export const getSpeedrunsForMap = async (map_name: string) => {
return await apiCall<SpeedrunResult[]>(`/api/speeduns/map?map_name=${map_name}`, 'GET');
export const getSpeedrunsTopMap = async (map_name: string) => {
return (await apiCall<SpeedrunMapOverview[]>(`/api/speedruns/map?map_name=${map_name}`, 'GET')).map(
transformCreatedOnDate
);
};

export const getSpeedrunsRecent = async (count: number = 5) => {
return (await apiCall<SpeedrunMapOverview[]>(`/api/speedruns/overall/recent?count=${count}`, 'GET')).map(
transformCreatedOnDate
);
};

export const getSpeedrun = async (speedrun_id: number) => {
const r = transformCreatedOnDate(await apiCall<SpeedrunResult>(`/api/speedruns/byid/${speedrun_id}`, 'GET'));
r.players = r.players.sort((a, b) => {
return a.duration > b.duration ? 1 : -1;
});
r.point_captures.map((p) => {
p.players = p.players.sort((a, b) => {
return a.duration > b.duration ? 1 : -1;
});
});
return r;
};
7 changes: 3 additions & 4 deletions frontend/src/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, ReactNode, useCallback, useEffect } from 'react';
import { ReactNode, useCallback, useEffect } from 'react';
import { apiGetCurrentProfile, defaultAvatarHash, PermissionLevel, UserProfile } from './api';
import { AuthContext } from './component/AuthContext.tsx';
import { logoutFn } from './util/auth/logoutFn.ts';
import { readAccessToken } from './util/auth/readAccessToken.ts';
import { logErr } from './util/errors.ts';
Expand All @@ -9,8 +10,6 @@ export const accessTokenKey = 'token';
export const profileKey = 'profile';
export const logoutKey = 'logout';

export const AuthContext = createContext<AuthContext | null>(null);

export function AuthProvider({
children,
profile,
Expand Down Expand Up @@ -93,7 +92,7 @@ export function AuthProvider({
);
}

export type AuthContext = {
export type AuthContextProps = {
profile: UserProfile;
login: (profile: UserProfile) => void;
logout: () => Promise<void>;
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/component/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from 'react';
import { AuthContextProps } from '../auth.tsx';

export const AuthContext = createContext<AuthContextProps | null>(null);
54 changes: 21 additions & 33 deletions frontend/src/component/NewsView.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import { useState } from 'react';
import { Pagination } from '@mui/material';
import Pagination from '@mui/material/Pagination';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import { apiGetNewsLatest, NewsEntry } from '../api/news';
import { useUserFlashCtx } from '../hooks/useUserFlashCtx.ts';
import { logErr } from '../util/errors';
import { useQuery } from '@tanstack/react-query';
import { apiGetNewsLatest } from '../api/news';
import { renderDate } from '../util/time.ts';
import { MarkDownRenderer } from './MarkdownRenderer';
import { SplitHeading } from './SplitHeading';
Expand All @@ -15,39 +13,29 @@ export interface NewsViewProps {
}

export const NewsView = ({ itemsPerPage }: NewsViewProps) => {
const { sendFlash } = useUserFlashCtx();
const [articles, setArticles] = useState<NewsEntry[]>([]);
const [page, setPage] = useState<number>(0);

useEffect(() => {
const abortController = new AbortController();
const fetchNews = async () => {
try {
const response = await apiGetNewsLatest(abortController);
setArticles(response);
} catch (error) {
logErr(error);
}
};

fetchNews().catch(logErr);

return () => abortController.abort();
}, [itemsPerPage, sendFlash]);
const { data: articles, isLoading } = useQuery({
queryKey: ['articles'],
queryFn: async () => {
return await apiGetNewsLatest();
}
});

return (
<Stack spacing={3}>
{(articles || [])?.slice(page * itemsPerPage, page * itemsPerPage + itemsPerPage).map((article) => {
if (!article.created_on || !article.updated_on) {
return null;
}
return (
<Paper elevation={1} key={`news_` + article.news_id}>
<SplitHeading left={article.title} right={renderDate(article.created_on)} />
<MarkDownRenderer body_md={article.body_md} />
</Paper>
);
})}
{!isLoading &&
(articles || [])?.slice(page * itemsPerPage, page * itemsPerPage + itemsPerPage).map((article) => {
if (!article.created_on || !article.updated_on) {
return null;
}
return (
<Paper elevation={1} key={`news_` + article.news_id}>
<SplitHeading left={article.title} right={renderDate(article.created_on)} />
<MarkDownRenderer body_md={article.body_md} />
</Paper>
);
})}
<Pagination
count={articles ? Math.ceil(articles.length / itemsPerPage) : 0}
defaultValue={1}
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/component/NotificationsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { JSX, useState } from 'react';
import { UserNotification } from '../api';
import { NotificationsCtx } from '../contexts/NotificationsCtx.tsx';

export const NotificationsProvider = ({ children }: { children: JSX.Element }) => {
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [notifications, setNotifications] = useState<UserNotification[]>([]);

return (
<NotificationsCtx.Provider
value={{
setNotifications,
notifications,
selectedIds,
setSelectedIds
}}
>
{children}
</NotificationsCtx.Provider>
);
};
53 changes: 19 additions & 34 deletions frontend/src/component/PersonCell.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { MouseEventHandler } from 'react';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/material/styles';
import { useNavigate } from '@tanstack/react-router';
import SteamID from 'steamid';
import { useUserFlashCtx } from '../hooks/useUserFlashCtx.ts';
import { avatarHashToURL } from '../util/text.tsx';
import { ButtonLink } from './ButtonLink.tsx';

export interface PersonCellProps {
steam_id: string;
Expand All @@ -21,32 +20,15 @@ export interface PersonCellProps {
}

export const PersonCell = ({ steam_id, avatar_hash, personaname, onClick, showCopy = false }: PersonCellProps) => {
const navigate = useNavigate();
const theme = useTheme();
const { sendFlash } = useUserFlashCtx();

return (
<Stack
minWidth={200}
direction={'row'}
alignItems={'center'}
onClick={
onClick != undefined
? onClick
: async () => {
await navigate({ to: `/profile/${steam_id}` });
}
}
sx={{
'&:hover': {
cursor: 'pointer',
backgroundColor: theme.palette.background.default
}
}}
>
<Stack minWidth={200} direction={'row'} alignItems={'center'}>
{showCopy && (
<Tooltip title={'Copy Steamid'}>
<IconButton
color={'warning'}
onClick={async (event) => {
event.preventDefault();
event.stopPropagation();
Expand All @@ -59,26 +41,29 @@ export const PersonCell = ({ steam_id, avatar_hash, personaname, onClick, showCo
</IconButton>
</Tooltip>
)}
<Tooltip title={personaname}>
<>
<ButtonLink
to={'/profile/$steamId'}
params={{ steamId: steam_id }}
onClick={onClick ?? undefined}
sx={{
'&:hover': {
cursor: 'pointer',
backgroundColor: theme.palette.background.default
}
}}
startIcon={
<Avatar
alt={personaname}
src={avatarHashToURL(avatar_hash, 'small')}
variant={'square'}
sx={{ height: '32px', width: '32px' }}
/>
</>
</Tooltip>

<Box
height={'100%'}
alignContent={'center'}
alignItems={'center'}
display={'inline-block'}
marginLeft={personaname == '' ? 0 : 2}
}
>
<Typography variant={'body1'}>{personaname}</Typography>
</Box>
<Typography fontWeight={'bold'} color={theme.palette.text.primary} variant={'body1'}>
{personaname != '' ? personaname : steam_id}
</Typography>
</ButtonLink>
</Stack>
);
};
5 changes: 4 additions & 1 deletion frontend/src/component/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ import { useNavigate } from '@tanstack/react-router';
import { MenuItemData, NestedDropdown } from 'mui-nested-menu';
import { apiGetNotifications, PermissionLevel, UserNotification } from '../api';
import { useAppInfoCtx } from '../contexts/AppInfoCtx.ts';
import { NotificationsProvider } from '../contexts/NotificationsCtx';
import { useAuth } from '../hooks/useAuth.ts';
import { useColourModeCtx } from '../hooks/useColourModeCtx.ts';
import steamLogo from '../icons/steam_login_sm.png';
import { tf2Fonts } from '../theme';
import { generateOIDCLink } from '../util/auth/generateOIDCLink.ts';
import { DesktopNotifications } from './DesktopNotifications.tsx';
import { NotificationsProvider } from './NotificationsProvider.tsx';
import RouterLink from './RouterLink.tsx';
import { VCenterBox } from './VCenterBox.tsx';

Expand All @@ -75,6 +75,9 @@ export const TopBar = () => {
const { data: notifications, isLoading } = useQuery({
queryKey: ['notifications'],
queryFn: async () => {
if (profile.steam_id == '') {
return [];
}
return (await apiGetNotifications()) ?? [];
},
refetchInterval: 60 * 1000,
Expand Down
20 changes: 1 addition & 19 deletions frontend/src/contexts/NotificationsCtx.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, Dispatch, SetStateAction, useState, JSX } from 'react';
import { createContext, Dispatch, SetStateAction } from 'react';
import { UserNotification } from '../api';
import { noop } from '../util/lists.ts';

Expand All @@ -15,21 +15,3 @@ export const NotificationsCtx = createContext<NotificationState>({
selectedIds: [],
setSelectedIds: () => noop
});

export const NotificationsProvider = ({ children }: { children: JSX.Element }) => {
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [notifications, setNotifications] = useState<UserNotification[]>([]);

return (
<NotificationsCtx.Provider
value={{
setNotifications,
notifications,
selectedIds,
setSelectedIds
}}
>
{children}
</NotificationsCtx.Provider>
);
};
5 changes: 3 additions & 2 deletions frontend/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useContext } from 'react';
import { AuthContext } from '../auth.tsx';
import { AuthContextProps } from '../auth.tsx';
import { AuthContext } from '../component/AuthContext.tsx';

export const useAuth = (): AuthContext => {
export const useAuth = (): AuthContextProps => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
Expand Down
Loading

0 comments on commit 20871b8

Please sign in to comment.