+
+ {!bottom && prev ? (
+
+ «
+
+
+ « #{sessionIndex - 1} · {formatSessionTimeDiff(currentSession, prev)}
+
+
+ {prev.name}
+
+
+
+ ) : !bottom ? (
+
+
«
+
No earlier session
+
+ ) : null}
+
+ {bottom && next ? (
+
+
+
+ #{sessionIndex + 1} · {formatSessionTimeDiff(currentSession, next)} »
+
+
+ {next.name}
+
+
+ »
+
+ ) : bottom ? (
+
+ ) : 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;