diff --git a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx index 0c49b73aa..f8e491006 100644 --- a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx +++ b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx @@ -43,7 +43,7 @@ export default function SpecificSessionPage({ reqUser, game }: UGPT) { ); } - return ; + return ; } function SessionPage({ data, game }: { data: SessionReturns } & UGPT) { @@ -132,128 +132,137 @@ function SessionPage({ data, game }: { data: SessionReturns } & UGPT) { `/sessions/${session.sessionID}/adjacent`, ); + const sessionIndex = sessionData.index; + + const centerContent = ( +
+ + Session #{sessionIndex} + + { + const newSession: SessionReturns = { + ...sessionData, + session: { ...sessionData.session, name }, + }; + setSessionData(newSession); + updateSession(newSession); + }} + placeholderText={session.name || "Untitled Session"} + /> + + { + const newSession: SessionReturns = { + ...sessionData, + session: { ...sessionData.session, desc }, + }; + setSessionData(newSession); + updateSession(newSession); + }} + placeholderText={session.desc || "No Description..."} + /> + + {session.highlight && ( + + Highlight! + + )} + + {user.id === loggedInUser?.id && ( +
+ {highlight ? ( + + ) : ( + + )} +
+ )} +
+ ); + return ( - - - -
- { - const newSession: SessionReturns = { - ...sessionData, - session: { - ...sessionData.session, - name, - }, - }; + {/* Desktop: tape layout */} +
+ + {centerContent} + +
- setSessionData(newSession); - - updateSession(newSession); - }} - placeholderText={session.name || "Untitled Session"} + {/* Mobile: stacked layout */} +
+ - - { - const newSession: SessionReturns = { - ...sessionData, - session: { - ...sessionData.session, - desc, - }, - }; - - setSessionData(newSession); - - updateSession(newSession); - }} - placeholderText={session.desc || "No Description..."} + {centerContent} +
- {session.highlight && ( - - Highlight! - - )} - - {user.id === loggedInUser?.id && ( - <> - {highlight ? ( - - ) : ( - - )} - - - )} + `/u/${username}/games/${s.game}/sessions/${s.sessionID}`; + const prev = adjacent?.prev ?? null; + const next = adjacent?.next ?? null; + return ( -
-
+
+ {!bottom && prev ? ( + + « +
+
+ « #{sessionIndex - 1} · {formatSessionTimeDiff(currentSession, prev)} +
+
+ {prev.name} +
+
+
+ ) : !bottom ? ( +
+ « +
No earlier session
+
+ ) : null} + + {bottom && next ? ( + +
+
+ #{sessionIndex + 1} · {formatSessionTimeDiff(currentSession, next)} » +
+
+ {next.name} +
+
+ » +
+ ) : bottom ? ( +
+
No later session
+ » +
+ ) : null} +
+ ); +} + +function SessionNavRow({ + adjacent, + children, + currentSession, + sessionIndex, + username, +}: { + adjacent: SessionAdjacentReturns | null; + children: React.ReactNode; + currentSession: SessionDocument; + sessionIndex: number; + username: string; +}) { + const sessionUrl = (s: SessionDocument) => + `/u/${username}/games/${s.game}/sessions/${s.sessionID}`; + + const borderColor = "var(--bs-secondary)"; + const sideStyle = { + border: `1px solid ${borderColor}`, + background: "rgba(255,255,255,0.02)", + }; + + return ( +
+ {/* Prev */} +
{adjacent?.prev ? ( - - « Prev + + + « #{sessionIndex - 1} ·{" "} + {formatSessionTimeDiff(currentSession, adjacent.prev)} + + + {adjacent.prev.name} + ) : ( - +
+ + « Prev + + + (This is your first session!) + +
)}
- {currentName} -
+ + {/* Current session — children */} +
+ {children} +
+ + {/* Next */} +
{adjacent?.next ? ( - - Next » + + + {formatSessionTimeDiff(currentSession, adjacent.next)} · + #{sessionIndex + 1} » + + + {adjacent.next.name} + ) : ( - +
+ + Next » + + + (This is your most recent session!) + +
)}
diff --git a/typescript/client/src/types/api-returns.ts b/typescript/client/src/types/api-returns.ts index 118759772..da3b607d4 100644 --- a/typescript/client/src/types/api-returns.ts +++ b/typescript/client/src/types/api-returns.ts @@ -95,6 +95,7 @@ export interface SessionReturns { songs: SongDocument[]; charts: ChartDocument[]; user: UserDocument; + index: number; } export interface SessionAdjacentReturns { diff --git a/typescript/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts b/typescript/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts index 47768e87d..54cb332de 100644 --- a/typescript/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts +++ b/typescript/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts @@ -125,12 +125,9 @@ describe("CreatePBDoc", () => { } as ScoreData<"iidx-sp">, }); - const pb = (await CreatePBDoc( - "iidx-sp", - userId, - Testing511SPA, - log, - )) as PBScoreDocumentNoRank<"iidx-sp"> | undefined; + const pb = (await CreatePBDoc("iidx-sp", userId, Testing511SPA, log)) as + | PBScoreDocumentNoRank<"iidx-sp"> + | undefined; expect(pb).toBeDefined(); expect(pb!.scoreData.score).toBe(1926); @@ -171,12 +168,9 @@ describe("CreatePBDoc", () => { } as ScoreData<"iidx-sp">, }); - const pb = (await CreatePBDoc( - "iidx-sp", - userId, - Testing511SPA, - log, - )) as PBScoreDocumentNoRank<"iidx-sp"> | undefined; + const pb = (await CreatePBDoc("iidx-sp", userId, Testing511SPA, log)) as + | PBScoreDocumentNoRank<"iidx-sp"> + | undefined; expect(pb).toBeDefined(); expect(pb!.scoreData.score).toBe(2040); diff --git a/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.ts b/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.ts index 7b43b41da..ed2484add 100644 --- a/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.ts +++ b/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.ts @@ -4,7 +4,7 @@ import { GetSessionFolderRaises } from "#lib/folders/get-session-folder-raises"; import { withSession, withSessionOwner } from "#lib/router/middleware"; import { success } from "#lib/router/typed-router"; import { API_V1_ROUTER } from "#server/router/api/v1/router"; -import { GetAdjacentSessions, GetSessionData } from "#utils/queries/sessions"; +import { GetAdjacentSessions, GetSessionData, GetSessionIndex } from "#utils/queries/sessions"; import { GetUserWithID } from "#utils/user"; import { ExpectedErr } from "bliss"; @@ -15,10 +15,14 @@ import { ExpectedErr } from "bliss"; */ API_V1_ROUTER.add("GET /sessions/:sessionID", withSession, async ({ ctx }) => { const { sessionDoc: session } = ctx; - const sessionData = await GetSessionData(session); + const [sessionData, index] = await Promise.all([ + GetSessionData(session), + GetSessionIndex(session), + ]); return success(`Successfully returned session ${session.name}.`, { charts: sessionData.charts, + index, scoreInfo: sessionData.scoreInfo, scores: sessionData.scores, session, diff --git a/typescript/server/src/utils/queries/sessions.ts b/typescript/server/src/utils/queries/sessions.ts index e119ef9a3..e5d7be210 100644 --- a/typescript/server/src/utils/queries/sessions.ts +++ b/typescript/server/src/utils/queries/sessions.ts @@ -97,6 +97,31 @@ export async function GetAdjacentSessions( }; } +/** + * Returns the 1-based chronological index of this session among all sessions + * for the same user and game, using (time_ended, id) as the sort key. + */ +export async function GetSessionIndex(session: SessionDocument): Promise { + const timeEnded = UnixMillisecondsToISO8601(session.timeEnded); + + const row = await DB.selectFrom("session") + .select(DB.fn.countAll().as("count")) + .where("session.user_id", "=", session.userID) + .where("session.game", "=", session.game) + .where((eb) => + eb.or([ + eb("session.time_ended", "<", timeEnded), + eb.and([ + eb("session.time_ended", "=", timeEnded), + eb("session.id", "<=", session.sessionID), + ]), + ]), + ) + .executeTakeFirstOrThrow(); + + return Number(row.count); +} + export async function GetSessionData(session: SessionDocument): Promise<{ charts: Array; scoreInfo: Array;