Member join number widget (#2939)
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle 2026-04-04 18:17:28 +03:00 committed by GitHub
parent fab0f7840c
commit ff27d6739b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 55774 additions and 1 deletions

View File

@ -1050,6 +1050,7 @@ export interface User {
preferences: JSONColumnTypeNullable<UserPreferences>;
/** User creation date. Can be null because we did not always save this. */
createdAt: number | null;
joinOrder: number | null;
/** Last message used when creating a tournament sub post */
lastSubMessage: string | null;
}

View File

@ -30,6 +30,7 @@ import { sortBadgesByFavorites } from "./core/badge-sorting.server";
import { findWidgetById } from "./core/widgets/portfolio";
import { WIDGET_LOADERS } from "./core/widgets/portfolio-loaders.server";
import type { LoadedWidget } from "./core/widgets/types";
import { SPL2_JOIN_ORDER_CUTOFF } from "./user-page-constants";
export const identifierToUserIdQuery = (identifier: string) =>
db
@ -926,6 +927,21 @@ export async function patronSinceByUserId(userId: number) {
)?.patronSince;
}
export async function joinOrderByUserId(userId: number) {
const row = await db
.selectFrom("User")
.select("User.joinOrder")
.where("id", "=", userId)
.executeTakeFirst();
if (!row?.joinOrder) return null;
return {
joinOrder: row.joinOrder,
isSpl2: row.joinOrder <= SPL2_JOIN_ORDER_CUTOFF,
};
}
export async function commissionsByUserId(userId: number) {
return await db
.selectFrom("User")
@ -958,7 +974,19 @@ export function upsert(
) {
return db
.insertInto("User")
.values({ ...args, createdAt: databaseTimestampNow() })
.values((eb) => ({
...args,
createdAt: databaseTimestampNow(),
joinOrder: eb
.selectFrom("User")
.select(
eb(
eb.fn.coalesce(eb.fn.max("joinOrder"), eb.val(0)),
"+",
eb.val(1),
).as("nextJoinOrder"),
),
}))
.onConflict((oc) => {
return oc.column("discordId").doUpdateSet({
...R.omit(args, ["discordId"]),

View File

@ -62,6 +62,49 @@ describe("UserRepository", () => {
expect(updatedUser?.createdAt).toEqual(createdAt);
});
test("new user gets joinOrder of 1", async () => {
const { id } = await UserRepository.upsert({
discordId: "1",
discordName: "TestUser",
discordAvatar: null,
});
const result = await UserRepository.joinOrderByUserId(id);
expect(result?.joinOrder).toBe(1);
});
test("joinOrder increments for each new user and does not change on update", async () => {
const { id: firstId } = await UserRepository.upsert({
discordId: "1",
discordName: "FirstUser",
discordAvatar: null,
});
const { id: secondId } = await UserRepository.upsert({
discordId: "2",
discordName: "SecondUser",
discordAvatar: null,
});
expect((await UserRepository.joinOrderByUserId(firstId))?.joinOrder).toBe(
1,
);
expect((await UserRepository.joinOrderByUserId(secondId))?.joinOrder).toBe(
2,
);
await UserRepository.upsert({
discordId: "1",
discordName: "UpdatedFirstUser",
discordAvatar: null,
});
expect((await UserRepository.joinOrderByUserId(firstId))?.joinOrder).toBe(
1,
);
});
describe("userRoles", () => {
test("returns empty array for basic user", async () => {
await UserRepository.upsert({

View File

@ -174,6 +174,14 @@ export function Widget({
})}
/>
);
case "join-date":
if (!widget.data) return null;
return (
<BigValue
value={`#${widget.data.joinOrder}`}
footer={widget.data.isSpl2 ? "SPL2" : undefined}
/>
);
case "timezone":
return <TimezoneWidget timezone={widget.data.timezone} />;
case "favorite-stage":

View File

@ -191,6 +191,9 @@ export const WIDGET_LOADERS = {
"patron-since": async (userId: number) => {
return UserRepository.patronSinceByUserId(userId);
},
"join-date": async (userId: number) => {
return UserRepository.joinOrderByUserId(userId);
},
videos: async (userId: number) => {
return VodRepository.findByUserId(userId, 3);
},

View File

@ -34,6 +34,7 @@ export const ALL_WIDGETS = {
}),
defineWidget({ id: "organizations", slot: "side" }),
defineWidget({ id: "patron-since", slot: "side" }),
defineWidget({ id: "join-date", slot: "side" }),
defineWidget({
id: "timezone",
slot: "side",

View File

@ -18,6 +18,8 @@ export const USER = {
GAME_BADGES_SMALL_MAX: 4,
};
export const SPL2_JOIN_ORDER_CUTOFF = 13_589;
export const IN_GAME_NAME_REGEXP = /^.{1,10}#[0-9a-z]{4,5}$/u;
export const MATCHES_PER_SEASONS_PAGE = 8;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "Tournament Results",
"widget.placement-results": "Tournament Placements",
"widget.patron-since": "Supporter Since",
"widget.join-date": "Member #",
"widget.timezone": "User's Current Time",
"widget.favorite-stage": "Favorite Stage",
"widget.videos": "Videos",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "Display your (highlighted) tournament results",
"widgets.description.placement-results": "Show your tournament placements by 1st, 2nd, and 3rd place",
"widgets.description.patron-since": "Show the date when you became a supporter",
"widgets.description.join-date": "Display your sendou.ink member number",
"widgets.description.timezone": "Display your current local time",
"widgets.description.favorite-stage": "Show your favorite stage",
"widgets.description.videos": "Display your 3 most recent videos",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "Resultados de torneos",
"widget.placement-results": "Clasificaciones en torneos",
"widget.patron-since": "Mecenas desde",
"widget.join-date": "",
"widget.timezone": "Hora actual del usuario",
"widget.favorite-stage": "Escenario favorito",
"widget.videos": "Vídeos",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "Muestra tus resultados de torneos (resaltados)",
"widgets.description.placement-results": "Muestra tus clasificaciones en torneos por 1º, 2º y 3º lugar",
"widgets.description.patron-since": "Muestra la fecha desde la que eres mecenas",
"widgets.description.join-date": "",
"widgets.description.timezone": "Muestra tu hora local actual",
"widgets.description.favorite-stage": "Muestra tu escenario favorito",
"widgets.description.videos": "Muestra tus 3 vídeos más recientes",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -17,6 +17,7 @@
"widget.highlighted-results": "",
"widget.placement-results": "",
"widget.patron-since": "",
"widget.join-date": "",
"widget.timezone": "",
"widget.favorite-stage": "",
"widget.videos": "",
@ -85,6 +86,7 @@
"widgets.description.highlighted-results": "",
"widgets.description.placement-results": "",
"widgets.description.patron-since": "",
"widgets.description.join-date": "",
"widgets.description.timezone": "",
"widgets.description.favorite-stage": "",
"widgets.description.videos": "",

View File

@ -0,0 +1,34 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
export function up(db) {
const dir = dirname(fileURLToPath(import.meta.url));
const jsonPath = resolve(dir, "../scripts/join-order.json");
const mapping = JSON.parse(readFileSync(jsonPath, "utf-8"));
db.transaction(() => {
db.prepare(/* sql */ `alter table "User" add "joinOrder" integer`).run();
const stmt = db.prepare(
/* sql */ `update "User" set "joinOrder" = ? where "id" = ?`,
);
for (const [sqliteId, joinOrder] of Object.entries(mapping)) {
stmt.run(joinOrder, Number(sqliteId));
}
const maxJoinOrder = db
.prepare(/* sql */ `select max("joinOrder") as m from "User"`)
.get().m;
const unmapped = db
.prepare(
/* sql */ `select "id" from "User" where "joinOrder" is null order by "id" asc`,
)
.all();
for (let i = 0; i < unmapped.length; i++) {
stmt.run(maxJoinOrder + 1 + i, unmapped[i].id);
}
})();
}

55621
scripts/join-order.json Normal file

File diff suppressed because it is too large Load Diff