Extract getUserDisplayName helper and use consistently throughout

Add a single getUserDisplayName(user) function to app/models/user.ts that
encapsulates the username → displayName fallback logic. Replace 9 scattered
inline expressions across the codebase (owner-map, scoring-calculator,
league routes, settings, invite flow, draft API, Clerk webhook) with calls
to the shared helper.

No behaviour change — all existing logic preserved, just centralised.

https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
This commit is contained in:
Claude 2026-03-18 23:15:01 +00:00
parent 05ba57651f
commit 3ba9d15f9e
No known key found for this signature in database
9 changed files with 31 additions and 15 deletions

View file

@ -1,4 +1,4 @@
import { findUserByClerkId } from "~/models/user"; import { findUserByClerkId, getUserDisplayName } from "~/models/user";
/** /**
* Builds a map of teamId -> manager username (or displayName fallback). * Builds a map of teamId -> manager username (or displayName fallback).
@ -15,7 +15,7 @@ export async function buildOwnerMap(
const ownerEntries = await Promise.all( const ownerEntries = await Promise.all(
uniqueOwnerIds.map(async (ownerId) => { uniqueOwnerIds.map(async (ownerId) => {
const user = await findUserByClerkId(ownerId); const user = await findUserByClerkId(ownerId);
const name = user?.username || user?.displayName || null; const name = user ? getUserDisplayName(user) : null;
return name ? { ownerId, name } : null; return name ? { ownerId, name } : null;
}) })
); );

View file

@ -6,6 +6,7 @@ import { getSeasonResults } from "./participant-season-result";
import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/discord"; import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/discord";
import { BRACKET_TEMPLATES } from "~/lib/bracket-templates"; import { BRACKET_TEMPLATES } from "~/lib/bracket-templates";
import { getUserDisplayName } from "~/models/user";
/** /**
* Core scoring calculation engine * Core scoring calculation engine
@ -1313,7 +1314,7 @@ export async function recalculateAffectedLeagues(
: []; : [];
const usernameByClerkId = new Map( const usernameByClerkId = new Map(
teamOwnerUsers teamOwnerUsers
.map((u) => [u.clerkId, u.username ?? u.displayName]) .map((u) => [u.clerkId, getUserDisplayName(u)])
.filter((entry): entry is [string, string] => entry[1] != null) .filter((entry): entry is [string, string] => entry[1] != null)
); );

View file

@ -5,6 +5,17 @@ import * as schema from "~/database/schema";
export type User = typeof schema.users.$inferSelect; export type User = typeof schema.users.$inferSelect;
export type NewUser = typeof schema.users.$inferInsert; export type NewUser = typeof schema.users.$inferInsert;
/**
* Returns the best available display identifier for a user.
* Prefers Clerk username; falls back to displayName (always set from
* firstName+lastName or email prefix). Returns null only if both are absent.
*/
export function getUserDisplayName(
user: Pick<User, "username" | "displayName">
): string | null {
return user.username ?? user.displayName ?? null;
}
export async function createUser(data: NewUser): Promise<User> { export async function createUser(data: NewUser): Promise<User> {
const db = database(); const db = database();
const [user] = await db.insert(schema.users).values(data).returning(); const [user] = await db.insert(schema.users).values(data).returning();

View file

@ -2,6 +2,7 @@ import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import { calculatePickInfo } from "~/models/draft-utils"; import { calculatePickInfo } from "~/models/draft-utils";
import { getUserDisplayName } from "~/models/user";
import type { LoaderFunctionArgs } from "react-router"; import type { LoaderFunctionArgs } from "react-router";
@ -77,7 +78,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
.from(schema.users) .from(schema.users)
.where(inArray(schema.users.clerkId, allOwnerIds)); .where(inArray(schema.users.clerkId, allOwnerIds));
for (const owner of owners) { for (const owner of owners) {
usernameByClerkId.set(owner.clerkId, owner.username ?? owner.displayName ?? null); usernameByClerkId.set(owner.clerkId, getUserDisplayName(owner));
} }
} }

View file

@ -1,6 +1,6 @@
import { Webhook } from "svix"; import { Webhook } from "svix";
import type { Route } from "./+types/clerk"; import type { Route } from "./+types/clerk";
import { findOrCreateUser } from "~/models/user"; import { findOrCreateUser, getUserDisplayName } from "~/models/user";
export async function action({ request }: Route.ActionArgs) { export async function action({ request }: Route.ActionArgs) {
// Get the webhook secret from environment // Get the webhook secret from environment
@ -62,7 +62,7 @@ export async function action({ request }: Route.ActionArgs) {
imageUrl: image_url, imageUrl: image_url,
}); });
console.log( console.log(
`User created in database: ${user.id} (${user.username || user.displayName})` `User created in database: ${user.id} (${getUserDisplayName(user)})`
); );
} else { } else {
// Update existing user (or create if doesn't exist) // Update existing user (or create if doesn't exist)
@ -78,7 +78,7 @@ export async function action({ request }: Route.ActionArgs) {
imageUrl: image_url, imageUrl: image_url,
}); });
console.log( console.log(
`User updated in database: ${user.id} (${user.username || user.displayName})` `User updated in database: ${user.id} (${getUserDisplayName(user)})`
); );
} }
} catch (error) { } catch (error) {

View file

@ -11,6 +11,7 @@ import {
claimTeam, claimTeam,
isUserLeagueMember, isUserLeagueMember,
findUserByClerkId, findUserByClerkId,
getUserDisplayName,
} from "~/models"; } from "~/models";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
@ -103,7 +104,7 @@ export async function action(args: Route.ActionArgs) {
// Claim the first available team, setting owner and name atomically // Claim the first available team, setting owner and name atomically
const team = availableTeams[0]; const team = availableTeams[0];
const teamName = `Team ${user.username || user.displayName || "Member"}`; const teamName = `Team ${getUserDisplayName(user) ?? "Member"}`;
await claimTeam(team.id, userId, teamName); await claimTeam(team.id, userId, teamName);
// Redirect to league page // Redirect to league page

View file

@ -8,6 +8,7 @@ import {
isUserLeagueMember, isUserLeagueMember,
isCommissioner, isCommissioner,
findUserByClerkId, findUserByClerkId,
getUserDisplayName,
findDraftSlotsBySeasonId, findDraftSlotsBySeasonId,
} from "~/models"; } from "~/models";
import { findCurrentSeasonWithSports } from "~/models/season"; import { findCurrentSeasonWithSports } from "~/models/season";
@ -83,7 +84,7 @@ export async function loader(args: Route.LoaderArgs) {
uniqueOwnerIds.map(async (ownerId) => { uniqueOwnerIds.map(async (ownerId) => {
const user = await findUserByClerkId(ownerId); const user = await findUserByClerkId(ownerId);
return user return user
? { clerkId: ownerId, name: user.username || user.displayName } ? { clerkId: ownerId, name: getUserDisplayName(user) }
: null; : null;
}) })
); );
@ -99,7 +100,7 @@ export async function loader(args: Route.LoaderArgs) {
commissionerIds.map(async (commissionerId) => { commissionerIds.map(async (commissionerId) => {
const user = await findUserByClerkId(commissionerId); const user = await findUserByClerkId(commissionerId);
return user return user
? { clerkId: commissionerId, name: user.username || user.displayName } ? { clerkId: commissionerId, name: getUserDisplayName(user) }
: null; : null;
}) })
); );

View file

@ -15,7 +15,7 @@ import {
} from "~/models/commissioner"; } from "~/models/commissioner";
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season"; import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team"; import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team";
import { findAllUsers, findUserByClerkId, isUserAdminByClerkId } from "~/models/user"; import { findAllUsers, findUserByClerkId, getUserDisplayName, isUserAdminByClerkId } from "~/models/user";
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport"; import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
import { findAllSportsSeasons } from "~/models/sports-season"; import { findAllSportsSeasons } from "~/models/sports-season";
import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot"; import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot";
@ -117,7 +117,7 @@ export async function loader(args: Route.LoaderArgs) {
const user = await findUserByClerkId(c.userId); const user = await findUserByClerkId(c.userId);
return { return {
...c, ...c,
userName: user?.username || user?.displayName || "Unknown User", userName: user ? (getUserDisplayName(user) ?? "Unknown User") : "Unknown User",
}; };
}) })
); );
@ -131,7 +131,7 @@ export async function loader(args: Route.LoaderArgs) {
uniqueOwnerIds.map(async (ownerId) => { uniqueOwnerIds.map(async (ownerId) => {
const user = await findUserByClerkId(ownerId); const user = await findUserByClerkId(ownerId);
return user return user
? { clerkId: ownerId, name: user.username || user.displayName, id: user.id } ? { clerkId: ownerId, name: getUserDisplayName(user), id: user.id }
: null; : null;
}) })
); );
@ -382,7 +382,7 @@ export async function action(args: Route.ActionArgs) {
} }
try { try {
const teamName = `Team ${assignedUser.username || assignedUser.displayName || "Member"}`; const teamName = `Team ${getUserDisplayName(assignedUser) ?? "Member"}`;
await claimTeam(teamId, userClerkId, teamName); await claimTeam(teamId, userClerkId, teamName);
return { success: true, message: "Owner assigned successfully" }; return { success: true, message: "Owner assigned successfully" };

View file

@ -5,6 +5,7 @@ import {
isUserLeagueMember, isUserLeagueMember,
isCommissioner, isCommissioner,
findTeamsBySeasonId, findTeamsBySeasonId,
getUserDisplayName,
} from "~/models"; } from "~/models";
import { getDraftPicks } from "~/models/draft-pick"; import { getDraftPicks } from "~/models/draft-pick";
import { getSeasonResults } from "~/models/participant-season-result"; import { getSeasonResults } from "~/models/participant-season-result";
@ -98,7 +99,7 @@ export async function loader(args: Route.LoaderArgs) {
}) })
: []; : [];
const ownerMap = new Map( const ownerMap = new Map(
ownerUserRows.map((u) => [u.clerkId, u.username || u.displayName || "Unknown"]) ownerUserRows.map((u) => [u.clerkId, getUserDisplayName(u) ?? "Unknown"])
); );
// Map draft picks to ownership // Map draft picks to ownership