diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 3ef653ab..0e53d894 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -67,6 +67,7 @@ import type * as _model_matchResults_queries_getMatchResults from "../_model/mat import type * as _model_matchResults_queries_getMatchResultsByTournament from "../_model/matchResults/queries/getMatchResultsByTournament.js"; import type * as _model_matchResults_queries_getMatchResultsByTournamentPairing from "../_model/matchResults/queries/getMatchResultsByTournamentPairing.js"; import type * as _model_matchResults_queries_getMatchResultsByTournamentRound from "../_model/matchResults/queries/getMatchResultsByTournamentRound.js"; +import type * as _model_matchResults_queries_getMatchResultsByUser from "../_model/matchResults/queries/getMatchResultsByUser.js"; import type * as _model_tournamentCompetitors__helpers_deepenTournamentCompetitor from "../_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor.js"; import type * as _model_tournamentCompetitors__helpers_sortTournamentCompetitorsByName from "../_model/tournamentCompetitors/_helpers/sortTournamentCompetitorsByName.js"; import type * as _model_tournamentCompetitors_fields from "../_model/tournamentCompetitors/fields.js"; @@ -258,6 +259,7 @@ declare const fullApi: ApiFromModules<{ "_model/matchResults/queries/getMatchResultsByTournament": typeof _model_matchResults_queries_getMatchResultsByTournament; "_model/matchResults/queries/getMatchResultsByTournamentPairing": typeof _model_matchResults_queries_getMatchResultsByTournamentPairing; "_model/matchResults/queries/getMatchResultsByTournamentRound": typeof _model_matchResults_queries_getMatchResultsByTournamentRound; + "_model/matchResults/queries/getMatchResultsByUser": typeof _model_matchResults_queries_getMatchResultsByUser; "_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor": typeof _model_tournamentCompetitors__helpers_deepenTournamentCompetitor; "_model/tournamentCompetitors/_helpers/sortTournamentCompetitorsByName": typeof _model_tournamentCompetitors__helpers_sortTournamentCompetitorsByName; "_model/tournamentCompetitors/fields": typeof _model_tournamentCompetitors_fields; diff --git a/convex/_model/matchResults/index.ts b/convex/_model/matchResults/index.ts index 4e9ec3f0..bf7039b4 100644 --- a/convex/_model/matchResults/index.ts +++ b/convex/_model/matchResults/index.ts @@ -58,3 +58,7 @@ export { getMatchResultsByTournamentRound, getMatchResultsByTournamentRoundArgs, } from './queries/getMatchResultsByTournamentRound'; +export { + getMatchResultsByUser, + getMatchResultsByUserArgs, +} from './queries/getMatchResultsByUser'; diff --git a/convex/_model/matchResults/queries/getMatchResultsByUser.ts b/convex/_model/matchResults/queries/getMatchResultsByUser.ts new file mode 100644 index 00000000..f01c052a --- /dev/null +++ b/convex/_model/matchResults/queries/getMatchResultsByUser.ts @@ -0,0 +1,29 @@ +import { paginationOptsValidator, PaginationResult } from 'convex/server'; +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { deepenMatchResult, DeepMatchResult } from '../_helpers/deepenMatchResult'; + +export const getMatchResultsByUserArgs = v.object({ + userId: v.id('users'), + paginationOpts: paginationOptsValidator, +}); + +export const getMatchResultsByUser = async ( + ctx: QueryCtx, + args: Infer, +): Promise> => { + const results = await ctx.db.query('matchResults') + .filter((q) => q.or( + q.eq(q.field('player0UserId'), args.userId), + q.eq(q.field('player1UserId'), args.userId), + )) + .order('desc') + .paginate(args.paginationOpts); + return { + ...results, + page: await Promise.all(results.page.map( + async (item) => await deepenMatchResult(ctx, item), + )), + }; +}; diff --git a/convex/matchResults.ts b/convex/matchResults.ts index 8d22aed1..6252cc2a 100644 --- a/convex/matchResults.ts +++ b/convex/matchResults.ts @@ -16,6 +16,11 @@ export const getMatchResultsByTournament = query({ handler: model.getMatchResultsByTournament, }); +export const getMatchResultsByUser = query({ + args: model.getMatchResultsByUserArgs, + handler: model.getMatchResultsByUser, +}); + export const getMatchResultsByTournamentPairing = query({ args: model.getMatchResultsByTournamentPairingArgs, handler: model.getMatchResultsByTournamentPairing, diff --git a/src/components/AccountMenu/AccountMenu.module.scss b/src/components/AccountMenu/AccountMenu.module.scss index dbe7af09..945da301 100644 --- a/src/components/AccountMenu/AccountMenu.module.scss +++ b/src/components/AccountMenu/AccountMenu.module.scss @@ -15,18 +15,41 @@ } .Content { - @include flex.column($gap: 0.25rem); + @include variants.card($elevated: true); @include animate.duration-quick; + min-width: 12rem; margin: 0.25rem 0; - padding: 0.25rem; @include animate.style-pop; // Must list last because it contains nested declarations } .UserDisplayName { @include text.ui($muted: true); + @include text.single-line; + + height: 2.5rem; + padding: 0.75rem; +} + +.Items { + @include flex.column($gap: 0); + + padding: 0.25rem; +} + +.Item { + @include variants.ghost; + @include corners.normal; + @include flex.row($gap: 0.5rem); + @include text.ui; + + height: 2.5rem; + padding: 0 0.5rem; - padding: 0.5rem 1rem; + svg { + width: 1rem; + height: 1rem; + } } diff --git a/src/components/AccountMenu/AccountMenu.tsx b/src/components/AccountMenu/AccountMenu.tsx index 3364c90b..a0fdd7eb 100644 --- a/src/components/AccountMenu/AccountMenu.tsx +++ b/src/components/AccountMenu/AccountMenu.tsx @@ -1,24 +1,35 @@ -import { useNavigate } from 'react-router-dom'; +import { generatePath, useNavigate } from 'react-router-dom'; import * as Popover from '@radix-ui/react-popover'; -import { Cog, LogOut } from 'lucide-react'; +import { + Cog, + LogOut, + User, +} from 'lucide-react'; import { useAuth } from '~/components/AuthProvider'; import { Avatar } from '~/components/generic/Avatar'; -import { Button } from '~/components/generic/Button'; import { Separator } from '~/components/generic/Separator'; import { useSignOut } from '~/services/auth/useSignOut'; +import { PATHS } from '~/settings'; import { getUserDisplayNameString } from '~/utils/common/getUserDisplayNameString'; import styles from './AccountMenu.module.scss'; export const AccountMenu = (): JSX.Element => { + const navigate = useNavigate(); const user = useAuth(); const { signOut } = useSignOut(); const displayName = user ? getUserDisplayNameString(user) : 'Unknown User'; - const navigate = useNavigate(); const items = [ + { + icon: , + label: 'Profile', + onClick: (): void => { + navigate(generatePath(PATHS.userProfile, { id: user!._id })); + }, + }, { icon: , label: 'Settings', @@ -38,17 +49,19 @@ export const AccountMenu = (): JSX.Element => { - +
{displayName} - +
- {items.map((item, i) => ( - - - - ))} +
+ {items.map((item, i) => ( + +
+ {item.icon}{item.label} +
+
+ ))} +
); diff --git a/src/components/IdentityBadge/IdentityBadge.module.scss b/src/components/IdentityBadge/IdentityBadge.module.scss index 4880289b..78b490b1 100644 --- a/src/components/IdentityBadge/IdentityBadge.module.scss +++ b/src/components/IdentityBadge/IdentityBadge.module.scss @@ -1,11 +1,23 @@ @use "/src/style/flex"; +@use "/src/style/corners"; @use "/src/style/text"; +@use "/src/style/variants"; .IdentityBadge { --avatar-size: 2.5rem; --avatar-spacing: 0.75rem; + margin: -0.5rem; + padding: 0.5rem; + + // background-color: blue; + @include flex.row($gap: var(--avatar-spacing)); + @include corners.normal; + + &[data-type="user"] { + @include variants.ghost; + } &[data-flipped="true"] { justify-content: flex-end; diff --git a/src/components/IdentityBadge/IdentityBadge.tsx b/src/components/IdentityBadge/IdentityBadge.tsx index 9b180822..177efcb5 100644 --- a/src/components/IdentityBadge/IdentityBadge.tsx +++ b/src/components/IdentityBadge/IdentityBadge.tsx @@ -1,7 +1,9 @@ import { cloneElement } from 'react'; +import { generatePath, useNavigate } from 'react-router-dom'; import clsx from 'clsx'; import { TournamentCompetitor, User } from '~/api'; +import { PATHS } from '~/settings'; import { ElementSize } from '~/types/componentLib'; import { useIdentityElements } from './IdentityBadge.hooks'; import { IdentityBadgePlaceholder } from './IdentityBadge.types'; @@ -34,6 +36,8 @@ export const IdentityBadge = ({ size = 'normal', user, }: IdentityBadgeProps): JSX.Element | null => { + const navigate = useNavigate(); + const [displayAvatar, displayName] = useIdentityElements({ user, competitor, @@ -48,8 +52,14 @@ export const IdentityBadge = ({ }), // TODO: Add claim button ]; + const type = user ? 'user' : 'competitor'; return ( -
+
user ? navigate(generatePath(PATHS.userProfile, { id: user._id })) : null} + > {flipped ? elements.reverse() : elements} {/* TODO: Add factions */}
diff --git a/src/components/generic/Card/CardHeader.module.scss b/src/components/generic/Card/CardHeader.module.scss new file mode 100644 index 00000000..a48239a3 --- /dev/null +++ b/src/components/generic/Card/CardHeader.module.scss @@ -0,0 +1,27 @@ +@use "/src/style/flex"; +@use "/src/style/variables"; +@use "/src/style/text"; +@use "/src/style/borders"; + +.CardHeader { + @include flex.row; + @include borders.normal($side: bottom); + + box-sizing: unset; + min-height: 2.5rem; + padding: + calc(1rem - var(--border-width)) + calc(1rem - var(--border-width)) + 1rem + var(--container-padding-x); + + h2 { + @include text.single-line; + } + + &_Actions { + @include flex.row; + + margin-left: auto; + } +} diff --git a/src/components/generic/Card/CardHeader.tsx b/src/components/generic/Card/CardHeader.tsx new file mode 100644 index 00000000..be8007a6 --- /dev/null +++ b/src/components/generic/Card/CardHeader.tsx @@ -0,0 +1,29 @@ +import { ReactNode } from 'react'; +import clsx from 'clsx'; + +import styles from './CardHeader.module.scss'; + +export interface CardHeaderProps { + className?: string; + title: ReactNode | string; + children?: ReactNode; +} + +export const CardHeader = ({ + className, + title, + children, +}: CardHeaderProps): JSX.Element => ( +
+ {typeof title === 'string' ? ( +

+ {title} +

+ ) : title} + {children && ( +
+ {children} +
+ )} +
+); diff --git a/src/components/generic/Card/index.ts b/src/components/generic/Card/index.ts index 96ef78eb..be497c1c 100644 --- a/src/components/generic/Card/index.ts +++ b/src/components/generic/Card/index.ts @@ -1,2 +1,8 @@ -export type { CardProps } from './Card'; -export { Card } from './Card'; +export { + Card, + type CardProps, +} from './Card'; +export { + CardHeader, + type CardHeaderProps, +} from './CardHeader'; diff --git a/src/pages/UserProfilePage/UserProfilePage.module.scss b/src/pages/UserProfilePage/UserProfilePage.module.scss new file mode 100644 index 00000000..3dcf0d1f --- /dev/null +++ b/src/pages/UserProfilePage/UserProfilePage.module.scss @@ -0,0 +1,121 @@ +@use "/src/style/flex"; +@use "/src/style/text"; +@use "/src/style/variables"; +@use "/src/style/corners"; +@use "/src/style/shadows"; +@use "/src/style/borders"; + +.UserProfilePage { + overflow: hidden; + flex-grow: 1; + gap: 1rem; + + &[data-layout="extra-wide"] { + display: grid; + grid-template-areas: "profile tabs tabs"; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr; + } + + &[data-layout="wide"], + &[data-layout="narrow"] { + @include flex.column; + } + + &_ProfileCard { + @include flex.column; + + &_Avatar { + width: 8rem; + height: 8rem; + } + + &_Identity { + padding: var(--container-padding-x); + + @include flex.row; + } + + &_Awards { + @include borders.normal($side: top); + + padding: 1rem var(--container-padding-x); + } + + &_Badges { + @include borders.normal($side: right); + + padding: var(--container-padding-x); + + // background-color: red; + } + + &_Teams { + padding: var(--container-padding-x); + + // background-color: red; + } + + &_Achievements { + @include borders.normal($side: right); + + padding: var(--container-padding-x); + + // background-color: red; + } + + &_Avatar { + max-width: 8rem; + } + + &_DisplayName { + @include text.large; + } + } + + &[data-layout="narrow"] { + .UserProfilePage_Profile { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr 1fr; + + &_Badges { + @include borders.normal($side: bottom); + } + + &_Achievements { + @include borders.normal($side: bottom); + } + } + } + + &_Tournaments { + display: grid; + grid-template-columns: repeat(auto-fill, 10rem); + gap: 1rem; + padding: 1rem var(--container-padding-x); + } + + &_MatchResults { + grid-area: matchResults; + + &_List { + @include flex.column($gap: 0.5rem); + + padding: 0.5rem; + + &_ViewAllButton { + @include flex.centered; + + flex-grow: 1; + padding: 1rem; + } + } + } + + &_Tabs { + @include flex.column; + + grid-area: tabs; + min-height: 0; + } +} diff --git a/src/pages/UserProfilePage/UserProfilePage.tsx b/src/pages/UserProfilePage/UserProfilePage.tsx new file mode 100644 index 00000000..8a2b6e28 --- /dev/null +++ b/src/pages/UserProfilePage/UserProfilePage.tsx @@ -0,0 +1,96 @@ +import { useCallback } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { useWindowWidth } from '@react-hook/window-size/throttled'; +import { Swords } from 'lucide-react'; + +import { UserId } from '~/api'; +import { Avatar } from '~/components/generic/Avatar'; +import { Card } from '~/components/generic/Card'; +import { + Tabs, + TabsContent, + TabsList, +} from '~/components/generic/Tabs'; +import { PageWrapper } from '~/components/PageWrapper'; +import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize'; +import { UserMatchResultsCard } from '~/pages/UserProfilePage/components/UserMatchResultsCard'; +import { useGetUser } from '~/services/users'; +import { MIN_WIDTH_DESKTOP, MIN_WIDTH_TABLET } from '~/settings'; +import { getUserDisplayNameString } from '~/utils/common/getUserDisplayNameString'; + +import styles from './UserProfilePage.module.scss'; + +export const UserProfilePage = (): JSX.Element => { + const [deviceSize] = useDeviceSize(); + const params = useParams(); + const userId = params.id! as UserId; // Must exist or else how did we get to this route? + + const windowWidth = useWindowWidth(); + + const getLayout = () => { + if (windowWidth >= MIN_WIDTH_DESKTOP) { + return 'extra-wide'; + } + if (windowWidth >= MIN_WIDTH_TABLET) { + return 'wide'; + } + return 'narrow'; + }; + + const layout = getLayout(); + + const { data: user } = useGetUser({ id: userId }); + + const [searchParams, setSearchParams] = useSearchParams(); + const queryTab = searchParams.get('tab'); + + const handleTabChange = useCallback((tab: string) => { + setSearchParams({ tab }, { replace: true }); + }, [setSearchParams]); + + const isDesktop = deviceSize >= DeviceSize.Wide; + const tabs = [ + { value: 'matchResults', label: 'Match Results', icon: }, + // { value: 'tournaments', label: 'Tournaments', icon: }, + ]; + const activeTab = !queryTab || tabs.findIndex((tab) => tab.value === queryTab) === -1 ? 'matchResults' : queryTab; + const showTabLabels = deviceSize >= DeviceSize.Default; + + // const awards = []; + + return ( + +
+ +
+ +
+

{getUserDisplayNameString(user)}

+

{user?.username}

+
+
+ {/* {(awards ?? []).length > 0 && ( +
+

{getUserDisplayNameString(user)}

+

{user?.username}

+
+ )} */} +
+ + {tabs.length > 1 && ( + + )} + + + + {/* + + + + stats card goes here + */} + +
+
+ ); +}; diff --git a/src/pages/UserProfilePage/components/UserMatchResultsCard/UserMatchResultsCard.module.scss b/src/pages/UserProfilePage/components/UserMatchResultsCard/UserMatchResultsCard.module.scss new file mode 100644 index 00000000..5a32f47d --- /dev/null +++ b/src/pages/UserProfilePage/components/UserMatchResultsCard/UserMatchResultsCard.module.scss @@ -0,0 +1,21 @@ +@use "/src/style/flex"; +@use "/src/style/text"; +@use "/src/style/variables"; +@use "/src/style/corners"; +@use "/src/style/shadows"; +@use "/src/style/borders"; + +.UserMatchResultsCard { + &_List { + @include flex.column($gap: 0.5rem); + + padding: 0.5rem; + + &_ViewAllButton { + @include flex.centered; + + flex-grow: 1; + padding: 1rem; + } + } +} diff --git a/src/pages/UserProfilePage/components/UserMatchResultsCard/UserMatchResultsCard.tsx b/src/pages/UserProfilePage/components/UserMatchResultsCard/UserMatchResultsCard.tsx new file mode 100644 index 00000000..29c62544 --- /dev/null +++ b/src/pages/UserProfilePage/components/UserMatchResultsCard/UserMatchResultsCard.tsx @@ -0,0 +1,59 @@ +import clsx from 'clsx'; +import { Plus } from 'lucide-react'; + +import { UserId } from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { Button } from '~/components/generic/Button'; +import { Card, CardHeader } from '~/components/generic/Card'; +import { ScrollArea } from '~/components/generic/ScrollArea'; +import { MatchResultCard } from '~/components/MatchResultCard'; +import { MatchResultCreateDialog, useMatchResultCreateDialog } from '~/components/MatchResultCreateDialog'; +import { useGetMatchResultsByUser } from '~/services/matchResults'; + +import styles from './UserMatchResultsCard.module.scss'; + +export interface UserMatchResultsCardProps { + className?: string; + userId: UserId; +} + +export const UserMatchResultsCard = ({ + className, + userId, +}: UserMatchResultsCardProps): JSX.Element => { + const user = useAuth(); + const { open } = useMatchResultCreateDialog(); + const { data: matchResults, loadMore: loadMoreMatchResults, status } = useGetMatchResultsByUser({ + userId, + }); + return ( + + + {user?._id === userId && ( + + )} + + +
+ {(matchResults ?? []).map((matchResult) => ( + + ))} + {(status === 'CanLoadMore' || status === 'LoadingMore') && ( +
+ +
+ )} +
+
+ +
+ ); +}; diff --git a/src/pages/UserProfilePage/components/UserMatchResultsCard/index.ts b/src/pages/UserProfilePage/components/UserMatchResultsCard/index.ts new file mode 100644 index 00000000..0eb9f1ce --- /dev/null +++ b/src/pages/UserProfilePage/components/UserMatchResultsCard/index.ts @@ -0,0 +1,4 @@ +export { + UserMatchResultsCard, + type UserMatchResultsCardProps, +} from './UserMatchResultsCard'; diff --git a/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.module.scss b/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.module.scss new file mode 100644 index 00000000..87df5db8 --- /dev/null +++ b/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.module.scss @@ -0,0 +1,30 @@ +@use "/src/style/flex"; +@use "/src/style/text"; +@use "/src/style/variables"; +@use "/src/style/corners"; +@use "/src/style/shadows"; +@use "/src/style/borders"; +@use "/src/style/variants"; + +.UserTournamentItem { + @include corners.normal; + @include flex.column($xAlign: center); + @include variants.ghost; + + margin: -1rem; + padding: 1rem; + + &_LogoWrapper { + @include corners.round; + @include flex.centered; + + width: 8rem; + height: 8rem; + background-color: red; + } + + &_Logo { + width: 5.5rem; + height: 5.5rem; + } +} diff --git a/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.tsx b/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.tsx new file mode 100644 index 00000000..72b55a4b --- /dev/null +++ b/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.tsx @@ -0,0 +1,36 @@ +import { Tournament, UserId } from '~/api'; +import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; +import { useGetTournamentRankings } from '~/services/tournaments'; +import { getTournamentCompetitorDisplayName } from '~/utils/common/getTournamentCompetitorDisplayName'; + +import styles from './UserTournamentItem.module.scss'; + +export interface UserTournamentItemProps { + userId: UserId; + tournament: Tournament; +} + +export const UserTournamentItem = ({ + userId, + tournament, +}: UserTournamentItemProps): JSX.Element => { + const { data: rankings } = useGetTournamentRankings({ + tournamentId: tournament._id, + round: tournament.roundCount - 1, + }); + const { data: competitors } = useGetTournamentCompetitorsByTournament({ + tournamentId: tournament._id, + }); + const competitor = (competitors ?? []).find((c) => c.players.find((p) => p.user._id === userId)); + return ( +
+
+ +
+

{tournament.title}

+ {tournament.useTeams && ( +

{getTournamentCompetitorDisplayName(competitor)}

+ )} +
+ ); +}; diff --git a/src/pages/UserProfilePage/components/UserTournamentItem/index.ts b/src/pages/UserProfilePage/components/UserTournamentItem/index.ts new file mode 100644 index 00000000..ec62a087 --- /dev/null +++ b/src/pages/UserProfilePage/components/UserTournamentItem/index.ts @@ -0,0 +1,4 @@ +export { + UserTournamentItem, + type UserTournamentItemProps, +} from './UserTournamentItem'; diff --git a/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.module.scss b/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.module.scss new file mode 100644 index 00000000..7ec5a28d --- /dev/null +++ b/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.module.scss @@ -0,0 +1,13 @@ +@use "/src/style/flex"; +@use "/src/style/text"; +@use "/src/style/variables"; +@use "/src/style/corners"; +@use "/src/style/shadows"; +@use "/src/style/borders"; + +.UserTournamentsCard { + display: grid; + grid-template-columns: repeat(auto-fill, 10rem); + gap: 1rem; + padding: 1rem var(--container-padding-x); +} diff --git a/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.tsx b/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.tsx new file mode 100644 index 00000000..224dae75 --- /dev/null +++ b/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.tsx @@ -0,0 +1,49 @@ +import clsx from 'clsx'; +import { Plus } from 'lucide-react'; + +import { UserId } from '~/api'; +import { Button } from '~/components/generic/Button'; +import { Card, CardHeader } from '~/components/generic/Card'; +import { ScrollArea } from '~/components/generic/ScrollArea'; +import { MatchResultCard } from '~/components/MatchResultCard'; +import { useGetMatchResultsByUser } from '~/services/matchResults'; + +import styles from './UserTournamentsCard.module.scss'; + +export interface UserTournamentsCardProps { + className?: string; + userId: UserId; +} + +export const UserTournamentsCard = ({ + className, + userId, +}: UserTournamentsCardProps): JSX.Element => { + + const { data: matchResults, loadMore: loadMoreMatchResults, status } = useGetMatchResultsByUser({ + userId, + }); + return ( + + + +
+ {(matchResults ?? []).map((matchResult) => ( + + ))} + {(status === 'CanLoadMore' || status === 'LoadingMore') && ( +
+ +
+ )} +
+
+
+ ); +}; diff --git a/src/pages/UserProfilePage/components/UserTournamentsCard/index.ts b/src/pages/UserProfilePage/components/UserTournamentsCard/index.ts new file mode 100644 index 00000000..dc528737 --- /dev/null +++ b/src/pages/UserProfilePage/components/UserTournamentsCard/index.ts @@ -0,0 +1,4 @@ +export { + UserTournamentsCard, + type UserTournamentsCardProps, +} from './UserTournamentsCard'; diff --git a/src/pages/UserProfilePage/index.ts b/src/pages/UserProfilePage/index.ts new file mode 100644 index 00000000..4c624ac8 --- /dev/null +++ b/src/pages/UserProfilePage/index.ts @@ -0,0 +1 @@ +export { UserProfilePage } from './UserProfilePage'; diff --git a/src/routes.tsx b/src/routes.tsx index 9d681eee..2454cfd2 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -31,6 +31,7 @@ import { TournamentEditPage } from '~/pages/TournamentEditPage/TournamentEditPag import { TournamentPairingDetailPage } from '~/pages/TournamentPairingDetailPage'; import { TournamentPairingsPage } from '~/pages/TournamentPairingsPage'; import { TournamentsPage } from '~/pages/TournamentsPage'; +import { UserProfilePage } from '~/pages/UserProfilePage'; import { PATHS } from '~/settings'; export interface AppRoute { @@ -148,6 +149,11 @@ export const routes = [ visibility: [], element: , }, + { + path: PATHS.userProfile, + visibility: [], + element: , + }, { path: '/settings', title: 'Settings', diff --git a/src/services/matchResults.ts b/src/services/matchResults.ts index 80dad147..78baa416 100644 --- a/src/services/matchResults.ts +++ b/src/services/matchResults.ts @@ -13,6 +13,7 @@ export const useGetMatchResults = createPaginatedQueryHook(api.matchResults.getM export const useGetMatchResultsByTournament = createQueryHook(api.matchResults.getMatchResultsByTournament); export const useGetMatchResultsByTournamentPairing = createQueryHook(api.matchResults.getMatchResultsByTournamentPairing); export const useGetMatchResultsByTournamentRound = createQueryHook(api.matchResults.getMatchResultsByTournamentRound); +export const useGetMatchResultsByUser = createPaginatedQueryHook(api.matchResults.getMatchResultsByUser); // Basic (C_UD) Mutations export const useCreateMatchResult = createMutationHook(api.matchResults.createMatchResult); diff --git a/src/services/utils/createPaginatedQueryHook.ts b/src/services/utils/createPaginatedQueryHook.ts index b4ad9802..6aad8410 100644 --- a/src/services/utils/createPaginatedQueryHook.ts +++ b/src/services/utils/createPaginatedQueryHook.ts @@ -19,9 +19,10 @@ export const createPaginatedQueryHook = (queryFn: T) => { data: undefined, loading: false, loadMore: (_n: number) => undefined, + status: null, }; } - const { results: data, isLoading, loadMore } = usePaginatedQuery(queryFn, args, { initialNumItems: 10 }); + const { results: data, isLoading, loadMore, status } = usePaginatedQuery(queryFn, args, { initialNumItems: 10 }); const stored = useRef(data); if (data !== undefined) { stored.current = data; @@ -30,6 +31,7 @@ export const createPaginatedQueryHook = (queryFn: T) => { data: stored.current, loading: isLoading || stored.current === undefined, loadMore, + status, }; }; }; diff --git a/src/settings.ts b/src/settings.ts index 728b76e6..f987f412 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -22,4 +22,5 @@ export const PATHS = { tournamentPairings: '/tournaments/:id/pairings', tournaments: '/tournaments', tournamentPairingDetails: '/pairings/:id', + userProfile: '/users/:id', } as const;