From 08ada6f86ab9aaf5d630d4ef0d4603c4c0031eed Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Sun, 20 Jul 2025 21:37:01 +0200 Subject: [PATCH] feat: #57 Implement basic dashboard --- convex/_generated/api.d.ts | 2 + .../getActiveTournamentPairingsByUser.ts | 7 +- convex/_model/tournaments/index.ts | 5 + .../tournaments/queries/getTournaments.ts | 20 +++- .../queries/getTournamentsByUser.ts | 52 +++++++++ .../utils/createTestTournamentMatchResults.ts | 17 ++- convex/tournaments.ts | 12 +- .../CheckInMatchDialog/CheckInMatchDialog.tsx | 4 + .../EmptyState/EmptyState.module.scss} | 2 +- .../EmptyState/EmptyState.tsx} | 14 +-- src/components/EmptyState/index.ts | 1 + .../FowV4MatchResultForm.tsx | 6 +- .../PageWrapper/PageWrapper.module.scss | 4 +- .../TournamentTimer.module.scss | 9 +- .../generic/Table/Table.module.scss | 2 +- src/components/generic/Table/Table.types.ts | 2 +- .../DashboardPage/DashboardPage.module.scss | 50 +++++++++ src/pages/DashboardPage/DashboardPage.tsx | 64 +++++++++-- .../ActiveTournament.module.scss | 70 ++++++++++++ .../ActiveTournament/ActiveTournament.tsx | 104 ++++++++++++++++++ .../ActiveTournament.utils.tsx | 82 ++++++++++++++ .../components/ActiveTournament/index.ts | 2 + .../components/Header/Header.module.scss | 27 +++++ .../components/Header/Header.tsx | 29 +++++ .../DashboardPage/components/Header/index.ts | 4 + .../MatchResultsCard.module.scss | 69 ++++++++++++ .../MatchResultsCard/MatchResultsCard.tsx | 76 +++++++++++++ .../components/MatchResultsCard/index.ts | 4 + .../StatsCard/StatsCard.module.scss | 8 ++ .../components/StatsCard/StatsCard.tsx | 20 ++++ .../components/StatsCard/index.ts | 4 + .../TournamentsCard.module.scss | 52 +++++++++ .../TournamentsCard/TournamentsCard.tsx | 74 +++++++++++++ .../TournamentsCard/TournamentsCard.utils.ts | 23 ++++ .../components/TournamentsCard/index.ts | 2 + .../TournamentsList.module.scss | 39 +++++++ .../TournamentsList/TournamentsList.tsx | 59 ++++++++++ .../components/TournamentsList/index.ts | 2 + .../TournamentDetailBanner.module.scss | 8 ++ .../TournamentMatchResultsCard.tsx | 4 +- .../TournamentPairingsCard.module.scss | 6 +- .../TournamentPairingsCard.tsx | 6 +- .../TournamentRankingsCard.tsx | 4 +- .../TournamentRosterCard.tsx | 4 +- .../TournamentTabEmptyState/index.ts | 1 - src/services/tournaments.ts | 2 + src/settings.ts | 4 +- src/style/_text.scss | 11 ++ 48 files changed, 1016 insertions(+), 57 deletions(-) create mode 100644 convex/_model/tournaments/queries/getTournamentsByUser.ts rename src/{pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.module.scss => components/EmptyState/EmptyState.module.scss} (93%) rename src/{pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.tsx => components/EmptyState/EmptyState.tsx} (61%) create mode 100644 src/components/EmptyState/index.ts create mode 100644 src/pages/DashboardPage/DashboardPage.module.scss create mode 100644 src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss create mode 100644 src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx create mode 100644 src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx create mode 100644 src/pages/DashboardPage/components/ActiveTournament/index.ts create mode 100644 src/pages/DashboardPage/components/Header/Header.module.scss create mode 100644 src/pages/DashboardPage/components/Header/Header.tsx create mode 100644 src/pages/DashboardPage/components/Header/index.ts create mode 100644 src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.module.scss create mode 100644 src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx create mode 100644 src/pages/DashboardPage/components/MatchResultsCard/index.ts create mode 100644 src/pages/DashboardPage/components/StatsCard/StatsCard.module.scss create mode 100644 src/pages/DashboardPage/components/StatsCard/StatsCard.tsx create mode 100644 src/pages/DashboardPage/components/StatsCard/index.ts create mode 100644 src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.module.scss create mode 100644 src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.tsx create mode 100644 src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.utils.ts create mode 100644 src/pages/DashboardPage/components/TournamentsCard/index.ts create mode 100644 src/pages/DashboardPage/components/TournamentsList/TournamentsList.module.scss create mode 100644 src/pages/DashboardPage/components/TournamentsList/TournamentsList.tsx create mode 100644 src/pages/DashboardPage/components/TournamentsList/index.ts delete mode 100644 src/pages/TournamentDetailPage/components/TournamentTabEmptyState/index.ts diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 0e141492..26751c9a 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -130,6 +130,7 @@ import type * as _model_tournaments_queries_getTournamentOpenRound from "../_mod import type * as _model_tournaments_queries_getTournamentRankings from "../_model/tournaments/queries/getTournamentRankings.js"; import type * as _model_tournaments_queries_getTournaments from "../_model/tournaments/queries/getTournaments.js"; import type * as _model_tournaments_queries_getTournamentsByStatus from "../_model/tournaments/queries/getTournamentsByStatus.js"; +import type * as _model_tournaments_queries_getTournamentsByUser from "../_model/tournaments/queries/getTournamentsByUser.js"; import type * as _model_users__helpers_checkUserAuth from "../_model/users/_helpers/checkUserAuth.js"; import type * as _model_users__helpers_checkUserTournamentForcedName from "../_model/users/_helpers/checkUserTournamentForcedName.js"; import type * as _model_users__helpers_checkUserTournamentRelationship from "../_model/users/_helpers/checkUserTournamentRelationship.js"; @@ -318,6 +319,7 @@ declare const fullApi: ApiFromModules<{ "_model/tournaments/queries/getTournamentRankings": typeof _model_tournaments_queries_getTournamentRankings; "_model/tournaments/queries/getTournaments": typeof _model_tournaments_queries_getTournaments; "_model/tournaments/queries/getTournamentsByStatus": typeof _model_tournaments_queries_getTournamentsByStatus; + "_model/tournaments/queries/getTournamentsByUser": typeof _model_tournaments_queries_getTournamentsByUser; "_model/users/_helpers/checkUserAuth": typeof _model_users__helpers_checkUserAuth; "_model/users/_helpers/checkUserTournamentForcedName": typeof _model_users__helpers_checkUserTournamentForcedName; "_model/users/_helpers/checkUserTournamentRelationship": typeof _model_users__helpers_checkUserTournamentRelationship; diff --git a/convex/_model/tournamentPairings/queries/getActiveTournamentPairingsByUser.ts b/convex/_model/tournamentPairings/queries/getActiveTournamentPairingsByUser.ts index 0110d4d2..6c28fa87 100644 --- a/convex/_model/tournamentPairings/queries/getActiveTournamentPairingsByUser.ts +++ b/convex/_model/tournamentPairings/queries/getActiveTournamentPairingsByUser.ts @@ -6,6 +6,7 @@ import { deepenTournamentPairing, TournamentPairingDeep } from '../_helpers/deep export const getActiveTournamentPairingsByUserArgs = v.object({ userId: v.id('users'), + round: v.optional(v.number()), }); /** @@ -37,8 +38,10 @@ export const getActiveTournamentPairingsByUser = async ( return (await Promise.all(tournamentPairings.map(async (tournamentPairing) => { const tournament = activeTournaments.find((activeTournament) => activeTournament._id === tournamentPairing.tournamentId); - // If pairing belongs to an inactive tournament OR an active tournament but not the current round, exclude it: - if (!tournament || tournament.currentRound !== tournamentPairing.round) { + const round = args.round ?? tournament?.currentRound; + + // If pairing belongs to an inactive tournament OR an active tournament but not the correct round, exclude it: + if (!tournament || round !== tournamentPairing.round) { return null; } diff --git a/convex/_model/tournaments/index.ts b/convex/_model/tournaments/index.ts index 38281fe7..a4ba9dbc 100644 --- a/convex/_model/tournaments/index.ts +++ b/convex/_model/tournaments/index.ts @@ -83,8 +83,13 @@ export { } from './queries/getTournamentRankings'; export { getTournaments, + getTournamentsArgs, } from './queries/getTournaments'; export { getTournamentsByStatus, getTournamentsByStatusArgs, } from './queries/getTournamentsByStatus'; +export { + getTournamentsByUser, + getTournamentsByUserArgs, +} from './queries/getTournamentsByUser'; diff --git a/convex/_model/tournaments/queries/getTournaments.ts b/convex/_model/tournaments/queries/getTournaments.ts index 23a9db26..5dd5c4a0 100644 --- a/convex/_model/tournaments/queries/getTournaments.ts +++ b/convex/_model/tournaments/queries/getTournaments.ts @@ -1,20 +1,38 @@ +import { Infer, v } from 'convex/values'; + import { QueryCtx } from '../../../_generated/server'; import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined'; import { checkTournamentVisibility } from '../_helpers/checkTournamentVisibility'; import { deepenTournament, TournamentDeep } from '../_helpers/deepenTournament'; +export const getTournamentsArgs = v.object({ + startsAfter: v.optional(v.union(v.string(), v.number())), +}); + /** * Gets an array of ALL deep Tournaments. * * @param ctx - Convex query context + * @param args - Convex query args + * @param args.startsAfter - Filter for tournaments starting after this date * @returns An array of deep Tournaments */ export const getTournaments = async ( ctx: QueryCtx, + args: Infer, ): Promise => { + const tournaments = await ctx.db.query('tournaments').collect(); const deepTournaments = await Promise.all( - tournaments.map(async (tournament) => { + tournaments.filter((tournament) => { + if (args.startsAfter) { + const startsAfter = typeof args.startsAfter === 'string' ? Date.parse(args.startsAfter) : args.startsAfter; + if (Date.parse(tournament.startsAt) < startsAfter) { + return false; + } + } + return true; + }).map(async (tournament) => { if (await checkTournamentVisibility(ctx, tournament)) { return await deepenTournament(ctx, tournament); } diff --git a/convex/_model/tournaments/queries/getTournamentsByUser.ts b/convex/_model/tournaments/queries/getTournamentsByUser.ts new file mode 100644 index 00000000..b736d851 --- /dev/null +++ b/convex/_model/tournaments/queries/getTournamentsByUser.ts @@ -0,0 +1,52 @@ +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { tournamentStatus } from '../../../common/tournamentStatus'; +import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined'; +import { checkTournamentVisibility } from '../_helpers/checkTournamentVisibility'; +import { deepenTournament, TournamentDeep } from '../_helpers/deepenTournament'; + +export const getTournamentsByUserArgs = v.object({ + userId: v.id('users'), + status: v.optional(tournamentStatus), +}); + +/** + * Gets an array of deep tournaments which were attended by a given user. + * + * @param ctx - Convex query context + * @param args - Convex query args + * @param args.status - The user ID to filter by + * @param args.status - Tournament status to filter by + * @returns An array of deep tournaments + */ +export const getTournamentsByUser = async ( + ctx: QueryCtx, + args: Infer, +): Promise => { + const tournaments = await ctx.db.query('tournaments').collect(); + const deepTournaments = await Promise.all( + tournaments.map(async (tournament) => { + if (await checkTournamentVisibility(ctx, tournament)) { + return await deepenTournament(ctx, tournament); + } + return null; + }), + ); + return deepTournaments.filter((tournament): tournament is TournamentDeep => { + if (!notNullOrUndefined(tournament)) { + return false; + } + const userIds = [ + ...tournament.organizerUserIds, + ...tournament.activePlayerUserIds, + ]; + if (!userIds.includes(args.userId)) { + return false; + } + if (args.status && tournament.status !== args.status) { + return false; + } + return true; + }); +}; diff --git a/convex/_model/utils/createTestTournamentMatchResults.ts b/convex/_model/utils/createTestTournamentMatchResults.ts index c1c5b3fb..eea90988 100644 --- a/convex/_model/utils/createTestTournamentMatchResults.ts +++ b/convex/_model/utils/createTestTournamentMatchResults.ts @@ -76,12 +76,17 @@ export const createTestTournamentMatchResults = async ( playerData.player1Placeholder = 'Bye'; } - const matchResultId = await ctx.db.insert('matchResults', createMockFowV4MatchResultData({ - ...playerData, - tournamentPairingId: pairing._id, - gameSystemConfig: tournament.gameSystemConfig, - gameSystemId: tournament.gameSystemId, - })); + // TODO: Replace with actual call to the create mutation + const matchResultId = await ctx.db.insert('matchResults', { + ...createMockFowV4MatchResultData({ + ...playerData, + tournamentPairingId: pairing._id, + + gameSystemConfig: tournament.gameSystemConfig, + gameSystemId: tournament.gameSystemId, + }), + tournamentId: tournament._id, + }); if (matchResultId) { matchResultIds.push(matchResultId); diff --git a/convex/tournaments.ts b/convex/tournaments.ts index 2de224de..9c50a783 100644 --- a/convex/tournaments.ts +++ b/convex/tournaments.ts @@ -7,10 +7,20 @@ export const getTournament = query({ }); export const getTournaments = query({ - args: {}, + args: model.getTournamentsArgs, handler: model.getTournaments, }); +export const getTournamentsByStatus = query({ + args: model.getTournamentsByStatusArgs, + handler: model.getTournamentsByStatus, +}); + +export const getTournamentsByUser = query({ + args: model.getTournamentsByUserArgs, + handler: model.getTournamentsByUser, +}); + export const getTournamentOpenRound = query({ args: model.getTournamentOpenRoundArgs, handler: model.getTournamentOpenRound, diff --git a/src/components/CheckInMatchDialog/CheckInMatchDialog.tsx b/src/components/CheckInMatchDialog/CheckInMatchDialog.tsx index dd00a364..54d3dc8b 100644 --- a/src/components/CheckInMatchDialog/CheckInMatchDialog.tsx +++ b/src/components/CheckInMatchDialog/CheckInMatchDialog.tsx @@ -1,5 +1,6 @@ import { ReactNode, useState } from 'react'; +import { TournamentPairingId } from '~/api'; import { FowV4MatchResultForm } from '~/components/FowV4MatchResultForm'; import { Dialog } from '~/components/generic/Dialog'; import { ScrollArea } from '~/components/generic/ScrollArea'; @@ -9,11 +10,13 @@ import styles from './CheckInMatchDialog.module.scss'; export interface CheckInMatchDialogProps { children?: ReactNode; trigger?: ReactNode; + tournamentPairingId?: TournamentPairingId; } export const CheckInMatchDialog = ({ children, trigger, + tournamentPairingId, }: CheckInMatchDialogProps): JSX.Element => { const [open, setOpen] = useState(false); return ( @@ -31,6 +34,7 @@ export const CheckInMatchDialog = ({ setOpen(false)} /> diff --git a/src/pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.module.scss b/src/components/EmptyState/EmptyState.module.scss similarity index 93% rename from src/pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.module.scss rename to src/components/EmptyState/EmptyState.module.scss index 4930e95f..6e436ea6 100644 --- a/src/pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.module.scss +++ b/src/components/EmptyState/EmptyState.module.scss @@ -2,7 +2,7 @@ @use "/src/style/flex"; @use "/src/style/text"; -.TournamentTabEmptyState { +.EmptyState { @include flex.stretchy; @include flex.column($xAlign: center, $yAlign: center); @include text.ui($muted: true); diff --git a/src/pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.tsx b/src/components/EmptyState/EmptyState.tsx similarity index 61% rename from src/pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.tsx rename to src/components/EmptyState/EmptyState.tsx index 8deb3460..3a22e291 100644 --- a/src/pages/TournamentDetailPage/components/TournamentTabEmptyState/TournamentTabEmptyState.tsx +++ b/src/components/EmptyState/EmptyState.tsx @@ -6,31 +6,31 @@ import { import clsx from 'clsx'; import { Satellite } from 'lucide-react'; -import styles from './TournamentTabEmptyState.module.scss'; +import styles from './EmptyState.module.scss'; -export interface TournamentTabEmptyStateProps { +export interface EmptyStateProps { className?: string; children?: ReactNode; icon?: ReactElement; message?: string; } -export const TournamentTabEmptyState = ({ +export const EmptyState = ({ className, message, icon, children, -}: TournamentTabEmptyStateProps): JSX.Element => { +}: EmptyStateProps): JSX.Element => { const iconProps = { - className: styles.TournamentTabEmptyState_Icon, + className: styles.EmptyState_Icon, size: 96, absoluteStrokeWidth: true, strokeWidth: 4, }; return ( -
+
{icon ? cloneElement(icon, iconProps) : } -
+
{message ?? 'Nothing to show yet.'}
{children && ( diff --git a/src/components/EmptyState/index.ts b/src/components/EmptyState/index.ts new file mode 100644 index 00000000..89798dbd --- /dev/null +++ b/src/components/EmptyState/index.ts @@ -0,0 +1 @@ +export { EmptyState } from './EmptyState'; diff --git a/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx b/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx index 8bffffbc..4c872870 100644 --- a/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx +++ b/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx @@ -41,6 +41,7 @@ export interface FowV4MatchResultFormProps { id: string; className?: string; matchResultId?: MatchResultId; + tournamentPairingId?: TournamentPairingId; onSuccess?: () => void; } @@ -48,6 +49,7 @@ export const FowV4MatchResultForm = ({ id, className, matchResultId, + tournamentPairingId: forcedTournamentPairingId, onSuccess, }: FowV4MatchResultFormProps): JSX.Element => { const user = useAuth(); @@ -60,7 +62,7 @@ export const FowV4MatchResultForm = ({ const [ tournamentPairingId, setTournamentPairingId, - ] = useAsyncState('single', matchResult?.tournamentPairingId); + ] = useAsyncState('single', forcedTournamentPairingId ?? matchResult?.tournamentPairingId); const { open: openConfirmMatchResultDialog, @@ -157,7 +159,7 @@ export const FowV4MatchResultForm = ({ options={resultForOptions} value={tournamentPairingId} onChange={handleChangeResultFor} - disabled={!!matchResult} + disabled={!!forcedTournamentPairingId || !!matchResult} />
diff --git a/src/components/PageWrapper/PageWrapper.module.scss b/src/components/PageWrapper/PageWrapper.module.scss index ed4d6e62..bb67a763 100644 --- a/src/components/PageWrapper/PageWrapper.module.scss +++ b/src/components/PageWrapper/PageWrapper.module.scss @@ -58,13 +58,13 @@ $header-height: 2.5rem; height: inherit; margin: 0 auto; - padding: 1rem var(--modal-outer-gutter); + padding: 1rem var(--modal-outer-gutter) var(--modal-outer-gutter); } &_Header { @include flex.row; - height: $header-height; + min-height: $header-height; } &_Body { diff --git a/src/components/TournamentTimer/TournamentTimer.module.scss b/src/components/TournamentTimer/TournamentTimer.module.scss index 3e7909c5..bb43536c 100644 --- a/src/components/TournamentTimer/TournamentTimer.module.scss +++ b/src/components/TournamentTimer/TournamentTimer.module.scss @@ -20,13 +20,8 @@ --digit-width: 1.5rem; @include flex.column($gap: 0); - @include borders.normal; - @include shadows.elevated; - @include corners.normal; - min-width: 17.5rem; // Hardcoded for simplicity height: min-content; - min-height: 8rem; // Hardcoded for simplicity background-color: var(--card-bg); &_TimeSection { @@ -39,7 +34,7 @@ grid-template-rows: 1rem var(--digit-height) 1rem; gap: 1rem; - padding: calc(1rem - var(--border-width)); + padding: var(--container-padding-x); color: var(--text-color-default); } @@ -130,7 +125,7 @@ @include flex.row($gap: 0.25rem, $xAlign: center); @include borders.normal($side: top); - padding: 0.5rem; + padding: 0.5rem var(--container-padding-x); } &_LoadingIcon { diff --git a/src/components/generic/Table/Table.module.scss b/src/components/generic/Table/Table.module.scss index 40a5f110..9c614f7e 100644 --- a/src/components/generic/Table/Table.module.scss +++ b/src/components/generic/Table/Table.module.scss @@ -57,7 +57,7 @@ overflow: hidden; display: flex; align-items: center; - min-width: 2.5rem; + min-width: 1.5rem; &[data-align="left"] { justify-content: start; diff --git a/src/components/generic/Table/Table.types.ts b/src/components/generic/Table/Table.types.ts index 617c5423..3b32fbf2 100644 --- a/src/components/generic/Table/Table.types.ts +++ b/src/components/generic/Table/Table.types.ts @@ -9,7 +9,7 @@ export type ColumnDef = { label?: string; renderCell?: (row: T, index: number) => ReactNode; renderHeader?: () => ReactNode; - width?: number; + width?: number | 'auto'; }; export type Row = [T, number]; diff --git a/src/pages/DashboardPage/DashboardPage.module.scss b/src/pages/DashboardPage/DashboardPage.module.scss new file mode 100644 index 00000000..0174beee --- /dev/null +++ b/src/pages/DashboardPage/DashboardPage.module.scss @@ -0,0 +1,50 @@ +@use "/src/style/flex"; +@use "/src/style/text"; +@use "/src/style/variables"; +@use "/src/style/corners"; +@use "/src/style/shadows"; +@use "/src/style/borders"; + +.DashboardPage { + flex-grow: 1; + gap: 1rem; + min-height: 0; + + &[data-orientation="horizontal"] { + display: grid; + grid-template-areas: "tournaments matchResults stats"; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr; + } + + &[data-orientation="vertical"] { + display: flex; + flex-direction: column; + } + + &_Tournaments { + grid-area: tournaments; + flex-grow: 1; + } + + &_MatchResults { + grid-area: matchResults; + flex-grow: 1; + } + + &_Stats { + grid-area: stats; + flex-grow: 1; + } + + &_Tabs { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + flex-grow: 1; + gap: 0.5rem; + } + + &_TabTrigger { + @include flex.centered; + } +} diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx index f6e8e55e..207ec00b 100644 --- a/src/pages/DashboardPage/DashboardPage.tsx +++ b/src/pages/DashboardPage/DashboardPage.tsx @@ -1,11 +1,57 @@ +import { ReactElement, useState } from 'react'; +import { useWindowWidth } from '@react-hook/window-size/throttled'; +import { + LineChart, + Swords, + Trophy, +} from 'lucide-react'; + +import { Button } from '~/components/generic/Button'; import { PageWrapper } from '~/components/PageWrapper'; +import { MIN_WIDTH_DESKTOP } from '~/settings'; +import { MatchResultsCard } from './components/MatchResultsCard'; +import { StatsCard } from './components/StatsCard'; +import { TournamentsCard } from './components/TournamentsCard'; + +import styles from './DashboardPage.module.scss'; + +type TabKey = 'tournaments' | 'matchResults' | 'stats'; -export const DashboardPage = (): JSX.Element => ( - -

Hello world!

-
- That's programmer speak for "I'm alive!" -
- In the near future, this landing page will be filled with useful information like your recent activity, upcoming matches, and more. -
-); +export const DashboardPage = (): JSX.Element => { + const windowWidth = useWindowWidth(); + const isDesktop = windowWidth >= MIN_WIDTH_DESKTOP; + const [view, setView] = useState('tournaments'); + const tabs: Record = { + tournaments: , + matchResults: , + stats: , + }; + return ( + + {Object.entries(tabs).map(([key, icon]) => ( +
+ +
+ ))} +
+ ) : undefined}> +
+ {(isDesktop || view === 'tournaments') && ( + + )} + {(isDesktop || view === 'matchResults') && ( + + )} + {(isDesktop || view === 'stats') && ( + + )} +
+ + ); +}; diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss new file mode 100644 index 00000000..7f6cf802 --- /dev/null +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss @@ -0,0 +1,70 @@ +@use "/src/style/flex"; +@use "/src/style/variants"; +@use "/src/style/variables"; +@use "/src/style/text"; +@use "/src/style/borders"; + +.ActiveTournament { + @include flex.column($gap: 0); + @include variants.card; + + overflow: hidden; + min-height: 0; + + &_Loading { + @include text.ui; + @include flex.row($gap: 0.5rem, $xAlign: center); + + flex-grow: 1; + color: var(--text-color-muted); + + svg { + width: 1rem; + height: 1rem; + } + } + + &_OpponentSection { + display: grid; + grid-template-areas: + "opponentLabel tableLabel ." + "opponent table checkInButton"; + grid-template-columns: 1fr auto auto; + grid-template-rows: auto auto; + row-gap: 0.5rem; + column-gap: 1rem; + + padding: 1rem var(--container-padding-x); + + &_Table { + @include text.large; + + grid-area: table; + place-self: center; + } + + &_Opponent { + grid-area: opponent; + } + + &_OpponentLabel { + grid-area: opponentLabel; + } + + &_TableLabel { + grid-area: tableLabel; + } + + &_CheckInButton { + grid-area: checkInButton; + } + } + + &_Rankings { + flex-grow: 1; + + &_Row { + padding: 0.5rem var(--container-padding-x); + } + } +} diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx new file mode 100644 index 00000000..1c600420 --- /dev/null +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx @@ -0,0 +1,104 @@ +import { generatePath, useNavigate } from 'react-router-dom'; +import clsx from 'clsx'; +import { ChevronRight, Plus } from 'lucide-react'; + +import { + Tournament, + TournamentCompetitorRanked, + TournamentPairing, +} from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { CheckInMatchDialog } from '~/components/CheckInMatchDialog'; +import { Button } from '~/components/generic/Button'; +import { Separator } from '~/components/generic/Separator'; +import { IdentityBadge } from '~/components/IdentityBadge'; +import { TournamentActionsProvider } from '~/components/TournamentActionsProvider'; +import { TournamentContextMenu } from '~/components/TournamentContextMenu'; +import { TournamentProvider } from '~/components/TournamentProvider'; +import { TournamentTimer } from '~/components/TournamentTimer'; +import { PATHS } from '~/settings'; +import { Header } from '../Header'; +import { + getOpponent, + renderRankings, + renderTitle, +} from './ActiveTournament.utils'; + +import styles from './ActiveTournament.module.scss'; + +export interface ActiveTournamentProps { + className?: string; + tournament: Tournament; + pairing?: TournamentPairing; + rankedCompetitors?: TournamentCompetitorRanked[]; +} + +export const ActiveTournament = ({ + className, + tournament, + pairing, + rankedCompetitors = [], +}: ActiveTournamentProps): JSX.Element => { + const navigate = useNavigate(); + const user = useAuth(); + + const opponent = getOpponent(user?._id, pairing); + + const handleViewMore = (): void => { + navigate(generatePath(PATHS.tournamentDetails, { id: tournament._id })); + }; + + const isOrganizer = user && tournament && tournament.organizerUserIds.includes(user._id); + const showTimer = tournament && tournament.currentRound !== undefined; + const showOpponent = tournament && pairing && opponent; + const showRankings = tournament && rankedCompetitors; + + return ( +
+ +
+ {isOrganizer && ( + + + + )} + +
+ {showTimer && ( + <> + + + + )} + {showOpponent && ( + <> +
+

+ {tournament.currentRound !== undefined ? 'Current Opponent' : 'Next Opponent'} +

+ +

+ Table +

+ + {(pairing?.table ?? 0) + 1} + + + + +
+ + + )} + {showRankings && renderRankings(tournament, rankedCompetitors)} +
+
+ ); +}; diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx new file mode 100644 index 00000000..bc6b2469 --- /dev/null +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx @@ -0,0 +1,82 @@ +import { ReactElement } from 'react'; + +import { + Tournament, + TournamentCompetitor, + TournamentCompetitorRanked, + TournamentPairing, + UserId, +} from '~/api'; +import { Pulsar } from '~/components/generic/Pulsar'; +import { Table } from '~/components/generic/Table'; +import { IdentityBadge } from '~/components/IdentityBadge'; + +import styles from './ActiveTournament.module.scss'; + +export const getOpponent = (userId?: UserId, pairing?: TournamentPairing): TournamentCompetitor | null => { + if (!userId) { + console.warn('Cannot find opponent without a user ID!'); + return null; + } + if (!pairing) { + console.warn('Cannot find opponent without a pairing!'); + return null; + } + + const competitor0UserIds = pairing.tournamentCompetitor0.players.map((player) => player.user._id); + if (competitor0UserIds.includes(userId)) { + return pairing.tournamentCompetitor1; + } + + const competitor1UserIds = (pairing.tournamentCompetitor1?.players ?? []).map((player) => player.user._id); + if (competitor1UserIds.includes(userId)) { + return pairing.tournamentCompetitor0; + } + + return null; +}; + +export const renderTitle = (title: string): ReactElement => ( + <> + +

+ Live: + {title} +

+ +); + +export const renderRankings = ( + tournament: Tournament, + competitors?: TournamentCompetitorRanked[], +): ReactElement => ( + ( +
+ {tournament.lastRound !== undefined ? r.rank + 1 : '-'} +
+ ), + }, + { + key: 'identity', + label: tournament?.useTeams ? 'Team' : 'Player', + renderCell: (r) => ( + + ), + }, + ]} + /> +); diff --git a/src/pages/DashboardPage/components/ActiveTournament/index.ts b/src/pages/DashboardPage/components/ActiveTournament/index.ts new file mode 100644 index 00000000..dfa0623f --- /dev/null +++ b/src/pages/DashboardPage/components/ActiveTournament/index.ts @@ -0,0 +1,2 @@ +export type { ActiveTournamentProps } from './ActiveTournament'; +export { ActiveTournament } from './ActiveTournament'; diff --git a/src/pages/DashboardPage/components/Header/Header.module.scss b/src/pages/DashboardPage/components/Header/Header.module.scss new file mode 100644 index 00000000..5eac2a84 --- /dev/null +++ b/src/pages/DashboardPage/components/Header/Header.module.scss @@ -0,0 +1,27 @@ +@use "/src/style/flex"; +@use "/src/style/variables"; +@use "/src/style/text"; +@use "/src/style/borders"; + +.Header { + @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/pages/DashboardPage/components/Header/Header.tsx b/src/pages/DashboardPage/components/Header/Header.tsx new file mode 100644 index 00000000..2e3e9696 --- /dev/null +++ b/src/pages/DashboardPage/components/Header/Header.tsx @@ -0,0 +1,29 @@ +import { ReactNode } from 'react'; +import clsx from 'clsx'; + +import styles from './Header.module.scss'; + +export interface HeaderProps { + className?: string; + title: ReactNode | string; + children?: ReactNode; +} + +export const Header = ({ + className, + title, + children, +}: HeaderProps): JSX.Element => ( +
+ {typeof title === 'string' ? ( +

+ {title} +

+ ) : title} + {children && ( +
+ {children} +
+ )} +
+); diff --git a/src/pages/DashboardPage/components/Header/index.ts b/src/pages/DashboardPage/components/Header/index.ts new file mode 100644 index 00000000..67b73e8c --- /dev/null +++ b/src/pages/DashboardPage/components/Header/index.ts @@ -0,0 +1,4 @@ +export { + Header, + type HeaderProps, +} from './Header'; diff --git a/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.module.scss b/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.module.scss new file mode 100644 index 00000000..e35939fb --- /dev/null +++ b/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.module.scss @@ -0,0 +1,69 @@ +@use "/src/style/flex"; +@use "/src/style/variants"; +@use "/src/style/variables"; +@use "/src/style/text"; +@use "/src/style/borders"; +@use "/src/style/corners"; + +.MatchResultsCard { + @include flex.column($gap: 0); + @include variants.card; + + overflow: hidden; + min-height: 0; + + &_Loading { + @include text.ui; + @include flex.row($gap: 0.5rem, $xAlign: center); + + flex-grow: 1; + color: var(--text-color-muted); + + svg { + width: 1rem; + height: 1rem; + } + } + + &_Header { + @include flex.row; + + 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; + + min-height: 2.5rem; + margin-left: auto; + } + } + + &_ScrollArea { + flex-grow: 1; + } + + &_List { + @include flex.column($gap: 0.5rem); + + flex-grow: 1; + height: 100%; + padding: 0.5rem; + + &_ViewAllButton { + @include flex.centered; + + flex-grow: 1; + padding: 1rem; + } + } + + &_EmptyState { + @include flex.centered; + + flex-grow: 1; + } +} diff --git a/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx b/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx new file mode 100644 index 00000000..20086ed2 --- /dev/null +++ b/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx @@ -0,0 +1,76 @@ +import { useNavigate } from 'react-router-dom'; +import clsx from 'clsx'; +import { ChevronRight, Plus } from 'lucide-react'; + +import { Button } from '~/components/generic/Button'; +import { ScrollArea } from '~/components/generic/ScrollArea'; +import { Separator } from '~/components/generic/Separator'; +import { Spinner } from '~/components/generic/Spinner'; +import { MatchResultCard } from '~/components/MatchResultCard'; +import { useGetMatchResults } from '~/services/matchResults'; +import { PATHS } from '~/settings'; + +import styles from './MatchResultsCard.module.scss'; + +export interface MatchResultsCardProps { + className?: string; +} + +export const MatchResultsCard = ({ + className, +}: MatchResultsCardProps): JSX.Element => { + const navigate = useNavigate(); + const { data: matchResults, loading } = useGetMatchResults({}); + + const handleViewMore = (): void => { + navigate(PATHS.matchResults); + }; + + const handleCreate = (): void => { + navigate(PATHS.tournamentCreate); + }; + + return ( +
+ {loading ? ( +
+ Loading... +
+ ) : ( + <> +
+

+ Recent Matches +

+
+ +
+
+ + {(matchResults ?? []).length ? ( + +
+ {(matchResults ?? []).slice(0, 5).map((matchResult) => ( + + ))} + {/* {(tournaments ?? []).length > 5 && ( */} +
+ +
+ {/* )} */} +
+
+ ) : ( +
+ +
+ )} + + )} +
+ ); +}; diff --git a/src/pages/DashboardPage/components/MatchResultsCard/index.ts b/src/pages/DashboardPage/components/MatchResultsCard/index.ts new file mode 100644 index 00000000..367e3fae --- /dev/null +++ b/src/pages/DashboardPage/components/MatchResultsCard/index.ts @@ -0,0 +1,4 @@ +export { + MatchResultsCard, + type MatchResultsCardProps, +} from './MatchResultsCard'; diff --git a/src/pages/DashboardPage/components/StatsCard/StatsCard.module.scss b/src/pages/DashboardPage/components/StatsCard/StatsCard.module.scss new file mode 100644 index 00000000..bf29245c --- /dev/null +++ b/src/pages/DashboardPage/components/StatsCard/StatsCard.module.scss @@ -0,0 +1,8 @@ +@use "/src/style/flex"; +@use "/src/style/variants"; +@use "/src/style/variables"; + +.StatsCard { + @include flex.column; + @include variants.card; +} diff --git a/src/pages/DashboardPage/components/StatsCard/StatsCard.tsx b/src/pages/DashboardPage/components/StatsCard/StatsCard.tsx new file mode 100644 index 00000000..ff25bdc7 --- /dev/null +++ b/src/pages/DashboardPage/components/StatsCard/StatsCard.tsx @@ -0,0 +1,20 @@ +import clsx from 'clsx'; +import { Construction } from 'lucide-react'; + +import { EmptyState } from '~/components/EmptyState'; +import { Header } from '../Header'; + +import styles from './StatsCard.module.scss'; + +export interface StatsCardProps { + className?: string; +} + +export const StatsCard = ({ + className, +}: StatsCardProps): JSX.Element => ( +
+
+ } message="Under Construction" /> +
+); diff --git a/src/pages/DashboardPage/components/StatsCard/index.ts b/src/pages/DashboardPage/components/StatsCard/index.ts new file mode 100644 index 00000000..a7b558ac --- /dev/null +++ b/src/pages/DashboardPage/components/StatsCard/index.ts @@ -0,0 +1,4 @@ +export { + StatsCard, + type StatsCardProps, +} from './StatsCard'; diff --git a/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.module.scss b/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.module.scss new file mode 100644 index 00000000..46a276ec --- /dev/null +++ b/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.module.scss @@ -0,0 +1,52 @@ +@use "/src/style/flex"; +@use "/src/style/variants"; +@use "/src/style/variables"; +@use "/src/style/text"; +@use "/src/style/borders"; +@use "/src/style/corners"; + +.TournamentsCard { + @include flex.column($gap: 0); + @include variants.card; + + overflow: hidden; + min-height: 0; + + &_Loading { + @include text.ui; + @include flex.row($gap: 0.5rem, $xAlign: center); + + flex-grow: 1; + color: var(--text-color-muted); + + svg { + width: 1rem; + height: 1rem; + } + } + + &_ScrollArea { + flex-grow: 1; + } + + &_List { + @include flex.column($gap: 0.5rem); + + flex-grow: 1; + height: 100%; + padding: 0.5rem; + + &_ViewAllButton { + @include flex.centered; + + flex-grow: 1; + padding: 1rem; + } + } + + &_EmptyState { + @include flex.centered; + + flex-grow: 1; + } +} diff --git a/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.tsx b/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.tsx new file mode 100644 index 00000000..655cc02f --- /dev/null +++ b/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.tsx @@ -0,0 +1,74 @@ +import clsx from 'clsx'; + +import { useAuth } from '~/components/AuthProvider'; +import { Spinner } from '~/components/generic/Spinner'; +import { ActiveTournament } from '~/pages/DashboardPage/components/ActiveTournament'; +import { TournamentsList } from '~/pages/DashboardPage/components/TournamentsList'; +import { useGetActiveTournamentPairingsByUser } from '~/services/tournamentPairings'; +import { useGetTournamentRankings, useGetTournaments } from '~/services/tournaments'; + +import styles from './TournamentsCard.module.scss'; + +export interface TournamentsCardProps { + className?: string; +} + +export const TournamentsCard = ({ + className, +}: TournamentsCardProps): JSX.Element => { + const user = useAuth(); + + const { + data: tournaments, + loading: tournamentsLoading, + } = useGetTournaments({}); + + const upcomingTournaments = (tournaments ?? []).filter((tournament) => ( + tournament.status === 'published' + )); + + const liveTournament = (tournaments ?? []).filter((tournament) => ( + tournament.status === 'active' && user && [ + ...tournament.playerUserIds, + ...tournament.organizerUserIds, + ].includes(user._id) + )).at(-1); + + const { + data: rankings, + loading: rankingsLoading, + } = useGetTournamentRankings(liveTournament ? { + tournamentId: liveTournament._id, + round: liveTournament.currentRound ?? liveTournament.nextRound ?? 0, + } : 'skip'); + const { + data: pairings, + loading: pairingsLoading, + } = useGetActiveTournamentPairingsByUser(user && liveTournament ? { + userId: user._id, + round: liveTournament.currentRound ?? liveTournament.nextRound, + } : 'skip'); + const pairing = pairings?.at(-1); + + const showLoading = tournamentsLoading || pairingsLoading || rankingsLoading; + + return ( +
+ {showLoading ? ( +
+ Loading... +
+ ) : ( + liveTournament ? ( + + ) : ( + + ) + )} +
+ ); +}; diff --git a/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.utils.ts b/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.utils.ts new file mode 100644 index 00000000..689cd4c2 --- /dev/null +++ b/src/pages/DashboardPage/components/TournamentsCard/TournamentsCard.utils.ts @@ -0,0 +1,23 @@ +import { + TournamentCompetitor, + TournamentPairing, + UserId, +} from '~/api'; + +export const getOpponent = (userId?: UserId, pairing?: TournamentPairing): TournamentCompetitor | null => { + + if (!userId || !pairing) { + return null; + } + + const competitor0UserIds = pairing.tournamentCompetitor0.players.map((player) => player.user._id); + if (competitor0UserIds.includes(userId)) { + return pairing.tournamentCompetitor1; + } + const competitor1UserIds = pairing.tournamentCompetitor0.players.map((player) => player.user._id); + if (competitor1UserIds.includes(userId)) { + return pairing.tournamentCompetitor0; + } + + return null; +}; diff --git a/src/pages/DashboardPage/components/TournamentsCard/index.ts b/src/pages/DashboardPage/components/TournamentsCard/index.ts new file mode 100644 index 00000000..e5fc563d --- /dev/null +++ b/src/pages/DashboardPage/components/TournamentsCard/index.ts @@ -0,0 +1,2 @@ +export type { TournamentsCardProps } from './TournamentsCard'; +export { TournamentsCard } from './TournamentsCard'; diff --git a/src/pages/DashboardPage/components/TournamentsList/TournamentsList.module.scss b/src/pages/DashboardPage/components/TournamentsList/TournamentsList.module.scss new file mode 100644 index 00000000..40db1296 --- /dev/null +++ b/src/pages/DashboardPage/components/TournamentsList/TournamentsList.module.scss @@ -0,0 +1,39 @@ +@use "/src/style/flex"; +@use "/src/style/variants"; +@use "/src/style/variables"; +@use "/src/style/text"; +@use "/src/style/borders"; +@use "/src/style/corners"; + +.TournamentsList { + @include flex.column($gap: 0); + @include variants.card; + + overflow: hidden; + min-height: 0; + + &_ScrollArea { + flex-grow: 1; + } + + &_List { + @include flex.column($gap: 0.5rem); + + flex-grow: 1; + height: 100%; + padding: 0.5rem; + + &_ViewAllButton { + @include flex.centered; + + flex-grow: 1; + padding: 1rem; + } + } + + &_EmptyState { + @include flex.centered; + + flex-grow: 1; + } +} diff --git a/src/pages/DashboardPage/components/TournamentsList/TournamentsList.tsx b/src/pages/DashboardPage/components/TournamentsList/TournamentsList.tsx new file mode 100644 index 00000000..482b4aa0 --- /dev/null +++ b/src/pages/DashboardPage/components/TournamentsList/TournamentsList.tsx @@ -0,0 +1,59 @@ +import { useNavigate } from 'react-router-dom'; +import clsx from 'clsx'; +import { ChevronRight, Plus } from 'lucide-react'; + +import { Tournament } from '~/api'; +import { Button } from '~/components/generic/Button'; +import { ScrollArea } from '~/components/generic/ScrollArea'; +import { TournamentCard } from '~/components/TournamentCard'; +import { PATHS } from '~/settings'; +import { Header } from '../Header'; + +import styles from './TournamentsList.module.scss'; + +export interface TournamentsListProps { + className?: string; + tournaments?: Tournament[]; + limit?: number; +} + +export const TournamentsList = ({ + className, + tournaments = [], + limit = 5, +}: TournamentsListProps): JSX.Element => { + const navigate = useNavigate(); + const handleViewMore = (): void => { + navigate(PATHS.tournaments); + }; + const handleCreate = (): void => { + navigate(PATHS.tournamentCreate); + }; + return ( + <> +
+ +
+ {tournaments.length ? ( + +
+ {tournaments.slice(0, limit).map((tournament) => ( + + ))} + {tournaments.length > limit && ( +
+ +
+ )} +
+
+ ) : ( +
+ +
+ )} + + ); +}; diff --git a/src/pages/DashboardPage/components/TournamentsList/index.ts b/src/pages/DashboardPage/components/TournamentsList/index.ts new file mode 100644 index 00000000..b3412d88 --- /dev/null +++ b/src/pages/DashboardPage/components/TournamentsList/index.ts @@ -0,0 +1,2 @@ +export type { TournamentsListProps } from './TournamentsList'; +export { TournamentsList } from './TournamentsList'; diff --git a/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.module.scss b/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.module.scss index be5e90eb..e55eed87 100644 --- a/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.module.scss +++ b/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.module.scss @@ -1,5 +1,8 @@ @use "/src/style/flex"; @use "/src/style/text"; +@use "/src/style/borders"; +@use "/src/style/shadows"; +@use "/src/style/corners"; .TournamentDetailBanner { display: flex; @@ -33,6 +36,11 @@ &_TimerSection { @include flex.centered; + @include borders.normal; + @include shadows.elevated; + @include corners.normal; + + overflow: hidden; grid-area: timer; align-items: stretch; } diff --git a/src/pages/TournamentDetailPage/components/TournamentMatchResultsCard/TournamentMatchResultsCard.tsx b/src/pages/TournamentDetailPage/components/TournamentMatchResultsCard/TournamentMatchResultsCard.tsx index e3d0a74c..f5af3823 100644 --- a/src/pages/TournamentDetailPage/components/TournamentMatchResultsCard/TournamentMatchResultsCard.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentMatchResultsCard/TournamentMatchResultsCard.tsx @@ -2,12 +2,12 @@ import { ReactElement, useState } from 'react'; import clsx from 'clsx'; import { Swords } from 'lucide-react'; +import { EmptyState } from '~/components/EmptyState'; import { InputSelect } from '~/components/generic/InputSelect'; import { MatchResultCard } from '~/components/MatchResultCard'; import { useTournament } from '~/components/TournamentProvider'; import { useGetMatchResultsByTournamentRound } from '~/services/matchResults'; import { TournamentDetailCard } from '../TournamentDetailCard'; -import { TournamentTabEmptyState } from '../TournamentTabEmptyState'; import styles from './TournamentMatchResultsCard.module.scss'; @@ -60,7 +60,7 @@ export const TournamentMatchResultsCard = ({ ) : ( showEmptyState ? ( - } /> + } /> ) : (
{(matchResults || []).map((matchResult) => ( diff --git a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss index d3367b4c..0fbcc6ae 100644 --- a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss +++ b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss @@ -22,11 +22,7 @@ } &_Table { - @include text.ui; - - font-size: 1.5rem; - font-weight: 300; - line-height: 1.75rem; + @include text.large; } &_Pairing { diff --git a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx index a9d2df5b..22eb0c02 100644 --- a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx @@ -2,6 +2,7 @@ import { ReactElement, useState } from 'react'; import clsx from 'clsx'; import { Zap } from 'lucide-react'; +import { EmptyState } from '~/components/EmptyState'; import { Button } from '~/components/generic/Button'; import { InputSelect } from '~/components/generic/InputSelect'; import { Table } from '~/components/generic/Table'; @@ -9,7 +10,6 @@ import { useTournamentActions } from '~/components/TournamentActionsProvider/Tou import { useTournament } from '~/components/TournamentProvider'; import { useGetTournamentPairings } from '~/services/tournamentPairings'; import { TournamentDetailCard } from '../TournamentDetailCard'; -import { TournamentTabEmptyState } from '../TournamentTabEmptyState'; import { getTournamentPairingTableConfig } from './TournamentPairingsCard.utils'; import styles from './TournamentPairingsCard.module.scss'; @@ -67,13 +67,13 @@ export const TournamentPairingsCard = ({
) : ( showEmptyState ? ( - }> + }> {actions?.configureRound && ( )} - +
) : (
) diff --git a/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.tsx b/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.tsx index eec88196..260d904b 100644 --- a/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.tsx @@ -2,6 +2,7 @@ import { ReactElement, useState } from 'react'; import clsx from 'clsx'; import { Trophy } from 'lucide-react'; +import { EmptyState } from '~/components/EmptyState'; import { InputSelect } from '~/components/generic/InputSelect'; import { Table } from '~/components/generic/Table'; import { useTournamentCompetitors } from '~/components/TournamentCompetitorsProvider'; @@ -9,7 +10,6 @@ import { useTournament } from '~/components/TournamentProvider'; import { getTournamentRankingTableConfig, RankingRow } from '~/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.utils'; import { useGetTournamentRankings } from '~/services/tournaments'; import { TournamentDetailCard } from '../TournamentDetailCard'; -import { TournamentTabEmptyState } from '../TournamentTabEmptyState'; import styles from './TournamentRankingsCard.module.scss'; @@ -97,7 +97,7 @@ export const TournamentRankingsCard = ({ ) : ( showEmptyState ? ( - } /> + } /> ) : (
) diff --git a/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx index 6189d93b..087fbd3b 100644 --- a/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx @@ -9,6 +9,7 @@ import { } from 'lucide-react'; import { useAuth } from '~/components/AuthProvider'; +import { EmptyState } from '~/components/EmptyState'; import { Button } from '~/components/generic/Button'; import { toast } from '~/components/ToastProvider'; import { TournamentCompetitorCreateDialog, useTournamentCompetitorCreateDialog } from '~/components/TournamentCompetitorCreateDialog'; @@ -21,7 +22,6 @@ import { useCreateTournamentCompetitor, useGetTournamentCompetitorsByTournament import { usePublishTournament } from '~/services/tournaments'; import { PATHS } from '~/settings'; import { TournamentDetailCard } from '../TournamentDetailCard'; -import { TournamentTabEmptyState } from '../TournamentTabEmptyState'; import styles from './TournamentRosterCard.module.scss'; @@ -129,7 +129,7 @@ export const TournamentRosterCard = ({ ) : ( showEmptyState ? ( - + ) : ( ) diff --git a/src/pages/TournamentDetailPage/components/TournamentTabEmptyState/index.ts b/src/pages/TournamentDetailPage/components/TournamentTabEmptyState/index.ts deleted file mode 100644 index 98ea2543..00000000 --- a/src/pages/TournamentDetailPage/components/TournamentTabEmptyState/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TournamentTabEmptyState } from './TournamentTabEmptyState'; diff --git a/src/services/tournaments.ts b/src/services/tournaments.ts index c6d34789..a7c18a48 100644 --- a/src/services/tournaments.ts +++ b/src/services/tournaments.ts @@ -9,6 +9,8 @@ export const useGetTournaments = createQueryHook(api.tournaments.getTournaments) export const useGetTournamentOpenRound = createQueryHook(api.tournaments.getTournamentOpenRound); export type TournamentOpenRound = typeof api.tournaments.getTournamentOpenRound._returnType; // TODO: Move to back-end export const useGetTournamentRankings = createQueryHook(api.tournaments.getTournamentRankings); +export const useGetTournamentsByStatus = createQueryHook(api.tournaments.getTournamentsByStatus); +export const useGetTournamentsByUser = createQueryHook(api.tournaments.getTournamentsByUser); // Basic (C_UD) Mutations export const useCreateTournament = createMutationHook(api.tournaments.createTournament); diff --git a/src/settings.ts b/src/settings.ts index f2dd5d39..700a206d 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -14,8 +14,8 @@ export const PATHS = { authSignIn: '/auth/sign-in', authSignUp: '/auth/sign-up', dashboard: '/dashboard', - matchResultDetails: '/match-results/:id', - matchResults: '/match-results', + matchResultDetails: '/matches/:id', + matchResults: '/matches', tournamentCreate: '/tournaments/create', tournamentDetails: '/tournaments/:id', tournamentEdit: '/tournaments/:id/edit', diff --git a/src/style/_text.scss b/src/style/_text.scss index acd6858b..c635e6e6 100644 --- a/src/style/_text.scss +++ b/src/style/_text.scss @@ -38,6 +38,17 @@ } } +@mixin large($bold: false, $muted: false) { + font-size: 1.5rem; + font-weight: 300; + line-height: 2rem; + color: var(--text-color-default); + + @if $muted == true { + color: var(--text-color-muted); + } +} + // - `text.uiBold` 0.875rem x 1.25rem, 700 // - `text.uiMuted` 0.875rem x 1.25rem, 700 // - `text.normal` 0.875rem, muted color