From d2282a263e331b17fb1e0832585be768a695cee8 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Wed, 29 Apr 2026 08:31:16 -0700 Subject: [PATCH] Fix BetterAuth field mapping and add owner-prefixed team names - Fix auth.server.ts: use camelCase Drizzle field names for BetterAuth adapter (was snake_case, causing user inserts to fail); add generateId: "uuid" so PostgreSQL UUID columns accept generated IDs - Add prependOwnerToTeamName / stripOwnerFromTeamName utilities - Invite join, league creation, and settings assign/remove all now manage the owner-prefix on team names atomically Co-Authored-By: Claude Sonnet 4.6 --- app/lib/auth.server.ts | 15 ++++++---- app/routes/i.$inviteCode.tsx | 5 ++-- app/routes/leagues/$leagueId.settings.tsx | 34 +++++++++++++++++++---- app/routes/leagues/new.tsx | 8 ++++-- app/utils/team-names.ts | 34 +++++++++++++++++++++++ 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/app/lib/auth.server.ts b/app/lib/auth.server.ts index c942df1..0dca6c5 100644 --- a/app/lib/auth.server.ts +++ b/app/lib/auth.server.ts @@ -23,16 +23,21 @@ export const auth = betterAuth({ provider: "pg", usePlural: true, }), + advanced: { + database: { + generateId: "uuid", + }, + }, user: { fields: { - name: "display_name", - image: "image_url", + name: "displayName", + image: "imageUrl", }, additionalFields: { - firstName: { type: "string", required: false, fieldName: "first_name" }, - lastName: { type: "string", required: false, fieldName: "last_name" }, + firstName: { type: "string", required: false, fieldName: "firstName" }, + lastName: { type: "string", required: false, fieldName: "lastName" }, username: { type: "string", required: false, fieldName: "username" }, - isAdmin: { type: "boolean", defaultValue: false, fieldName: "is_admin" }, + isAdmin: { type: "boolean", defaultValue: false, fieldName: "isAdmin" }, }, }, emailAndPassword: { diff --git a/app/routes/i.$inviteCode.tsx b/app/routes/i.$inviteCode.tsx index 9294b54..0c873d9 100644 --- a/app/routes/i.$inviteCode.tsx +++ b/app/routes/i.$inviteCode.tsx @@ -10,8 +10,8 @@ import { claimTeam, isUserLeagueMember, findUserById, - getUserDisplayName, } from "~/models"; +import { prependOwnerToTeamName } from "~/utils/team-names"; import { Button } from "~/components/ui/button"; import { Card, @@ -105,7 +105,8 @@ export async function action(args: Route.ActionArgs) { // Claim the first available team, setting owner and name atomically const team = availableTeams[0]; - const teamName = `Team ${getUserDisplayName(user) ?? "Member"}`; + const username = user.username ?? user.displayName ?? "Member"; + const teamName = prependOwnerToTeamName(team.name, username); await claimTeam(team.id, userId, teamName); // Redirect to league page diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index 1b5e74d..c32b15f 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -15,8 +15,9 @@ import { removeCommissionerByLeagueAndUser, } from "~/models/commissioner"; import { findCurrentSeasonWithSports, updateSeason, type NewSeason } from "~/models/season"; -import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team"; +import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam, findTeamById } from "~/models/team"; import { findAllUsers, findUsersByIds, findUserById, getUserDisplayName, isUserAdmin } from "~/models/user"; +import { prependOwnerToTeamName, stripOwnerFromTeamName, generateUniqueTeamNames } from "~/utils/team-names"; import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport"; import { findDraftableSportsSeasons, findSportsSeasonsByIds } from "~/models/sports-season"; import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot"; @@ -400,7 +401,23 @@ export async function action(args: Route.ActionArgs) { } try { - await removeTeamOwner(teamId); + const team = await findTeamById(teamId); + if (team?.ownerId) { + const owner = await findUserById(team.ownerId); + const username = owner?.username ?? owner?.displayName; + if (username) { + const strippedName = stripOwnerFromTeamName(team.name, username); + await removeTeamOwner(teamId); + if (strippedName !== team.name) { + const { renameTeam } = await import("~/models/team"); + await renameTeam(teamId, strippedName); + } + } else { + await removeTeamOwner(teamId); + } + } else { + await removeTeamOwner(teamId); + } return { success: true, message: "Owner removed successfully" }; } catch (error) { logger.error("Error removing team owner:", error); @@ -438,12 +455,18 @@ export async function action(args: Route.ActionArgs) { // Check if the target team already has an owner const targetTeam = teams.find(team => team.id === teamId); - if (targetTeam?.ownerId) { + if (!targetTeam) { + return { error: "Team not found" }; + } + if (targetTeam.ownerId) { return { error: "This team already has an owner. Remove the current owner first." }; } try { - const teamName = `Team ${getUserDisplayName(assignedUser) ?? "Member"}`; + const teamName = prependOwnerToTeamName( + targetTeam.name, + assignedUser.username ?? assignedUser.displayName ?? "Member" + ); await claimTeam(teamId, assignedUserId, teamName); return { success: true, message: "Owner assigned successfully" }; @@ -718,11 +741,12 @@ export async function action(args: Route.ActionArgs) { // Add or remove teams as needed if (newTeamCount > currentTeamCount) { + const newNames = generateUniqueTeamNames(newTeamCount - currentTeamCount); const teamsToAdd = Array.from( { length: newTeamCount - currentTeamCount }, (_, i) => ({ seasonId: season.id, - name: `Team ${currentTeamCount + i + 1}`, + name: newNames[i], ownerId: null as null, }) ); diff --git a/app/routes/leagues/new.tsx b/app/routes/leagues/new.tsx index 9744b08..b952755 100644 --- a/app/routes/leagues/new.tsx +++ b/app/routes/leagues/new.tsx @@ -13,7 +13,7 @@ import { createManyTeams } from "~/models/team"; import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template"; import { findDraftableSportsSeasons } from "~/models/sports-season"; import { findUserById, updateUser } from "~/models/user"; -import { generateUniqueTeamNames } from "~/utils/team-names"; +import { generateUniqueTeamNames, prependOwnerToTeamName } from "~/utils/team-names"; import { format } from "date-fns"; import { CalendarClock, Check, Star, Swords, Trophy } from "lucide-react"; import { Badge } from "~/components/ui/badge"; @@ -247,9 +247,13 @@ export async function action(args: Route.ActionArgs) { const joinAsPlayer = formData.get("joinAsPlayer") === "on"; const teamNames = generateUniqueTeamNames(teamCountNum); + const creator = await findUserById(userId); + const creatorUsername = creator?.username ?? creator?.displayName; const teams = teamNames.map((teamName, i) => ({ seasonId: season.id, - name: teamName, + name: joinAsPlayer && i === 0 && creatorUsername + ? prependOwnerToTeamName(teamName, creatorUsername) + : teamName, ownerId: joinAsPlayer && i === 0 ? userId : null, })); diff --git a/app/utils/team-names.ts b/app/utils/team-names.ts index a42ff6f..b379639 100644 --- a/app/utils/team-names.ts +++ b/app/utils/team-names.ts @@ -75,3 +75,37 @@ export function generateUniqueTeamNames(count: number): string[] { return Array.from(names); } + +function toTitleCase(str: string): string { + if (!str) return str; + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function possessiveSuffix(name: string): string { + return name.endsWith("s") ? "'" : "'s"; +} + +export function prependOwnerToTeamName( + baseName: string, + username: string +): string { + const titleName = toTitleCase(username); + return `${titleName}${possessiveSuffix(titleName)} ${baseName}`; +} + +export function stripOwnerFromTeamName( + teamName: string, + username: string +): string { + const titleName = toTitleCase(username); + const prefixWithS = `${titleName}'s `; + const prefixWithoutS = `${titleName}' `; + + if (teamName.startsWith(prefixWithS)) { + return teamName.slice(prefixWithS.length); + } + if (teamName.startsWith(prefixWithoutS)) { + return teamName.slice(prefixWithoutS.length); + } + return teamName; +}