Spaces:
Running
Running
Make player last name optional
Browse files- src/admin.ts +4 -8
- src/server.ts +20 -10
src/admin.ts
CHANGED
|
@@ -15,7 +15,8 @@ const profileSchema = z.object({
|
|
| 15 |
academy: z.string().trim().max(150).nullable(), role: z.string().trim().max(50).nullable(),
|
| 16 |
}).strict();
|
| 17 |
const playerSchema = z.object({
|
| 18 |
-
|
|
|
|
| 19 |
heightCm: z.number().min(100).max(230).nullable().optional(), weightKg: z.number().min(20).max(150).nullable().optional(),
|
| 20 |
club: z.string().trim().max(150).nullable().optional(), position: z.string().trim().max(50).nullable().optional(),
|
| 21 |
gender: z.enum(["male", "female"]).nullable().optional(), userId: uuid.nullable().optional(),
|
|
@@ -26,10 +27,6 @@ function asyncRoute(handler: (req: Request, res: Response) => Promise<unknown>)
|
|
| 26 |
return (req: Request, res: Response, next: NextFunction) => { void handler(req, res).catch(next); };
|
| 27 |
}
|
| 28 |
async function admin(req: Request, res: Response) { return requireAdmin(req, res); }
|
| 29 |
-
function playerName(name: string) {
|
| 30 |
-
const [fname, ...parts] = name.split(/\s+/);
|
| 31 |
-
return { fname, lname: parts.join(" ") };
|
| 32 |
-
}
|
| 33 |
function ageDate(age?: number | null) {
|
| 34 |
if (age == null) return null;
|
| 35 |
const birth = new Date();
|
|
@@ -200,11 +197,10 @@ export function registerAdminRoutes(app: Express) {
|
|
| 200 |
if (error) throw new Error(`Unable to verify player owner: ${error.message}`);
|
| 201 |
if (!data) return fail(res, 404, "Player owner not found.");
|
| 202 |
}
|
| 203 |
-
const { fname, lname } = playerName(input.data.name);
|
| 204 |
const { data, error } = await supabaseAdmin.rpc("admin_create_player", {
|
| 205 |
p_user_id: input.data.userId ?? null,
|
| 206 |
-
p_fname: fname,
|
| 207 |
-
p_lname: lname,
|
| 208 |
p_bdate: ageDate(input.data.age),
|
| 209 |
p_height: input.data.heightCm ?? null,
|
| 210 |
p_weight: input.data.weightKg ?? null,
|
|
|
|
| 15 |
academy: z.string().trim().max(150).nullable(), role: z.string().trim().max(50).nullable(),
|
| 16 |
}).strict();
|
| 17 |
const playerSchema = z.object({
|
| 18 |
+
fname: z.string().trim().min(1).max(100), lname: z.string().trim().max(100).optional().default(""),
|
| 19 |
+
age: z.number().int().min(4).max(60).nullable().optional(),
|
| 20 |
heightCm: z.number().min(100).max(230).nullable().optional(), weightKg: z.number().min(20).max(150).nullable().optional(),
|
| 21 |
club: z.string().trim().max(150).nullable().optional(), position: z.string().trim().max(50).nullable().optional(),
|
| 22 |
gender: z.enum(["male", "female"]).nullable().optional(), userId: uuid.nullable().optional(),
|
|
|
|
| 27 |
return (req: Request, res: Response, next: NextFunction) => { void handler(req, res).catch(next); };
|
| 28 |
}
|
| 29 |
async function admin(req: Request, res: Response) { return requireAdmin(req, res); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
function ageDate(age?: number | null) {
|
| 31 |
if (age == null) return null;
|
| 32 |
const birth = new Date();
|
|
|
|
| 197 |
if (error) throw new Error(`Unable to verify player owner: ${error.message}`);
|
| 198 |
if (!data) return fail(res, 404, "Player owner not found.");
|
| 199 |
}
|
|
|
|
| 200 |
const { data, error } = await supabaseAdmin.rpc("admin_create_player", {
|
| 201 |
p_user_id: input.data.userId ?? null,
|
| 202 |
+
p_fname: input.data.fname,
|
| 203 |
+
p_lname: input.data.lname,
|
| 204 |
p_bdate: ageDate(input.data.age),
|
| 205 |
p_height: input.data.heightCm ?? null,
|
| 206 |
p_weight: input.data.weightKg ?? null,
|
src/server.ts
CHANGED
|
@@ -72,17 +72,24 @@ const actionReviewSchema = z.object({
|
|
| 72 |
feedback: z.array(z.string().trim().min(1).max(2000)).max(30),
|
| 73 |
}).strict();
|
| 74 |
const playerProfileSchema = z.object({
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
age: z.string().regex(/^(|\d{1,2})$/),
|
| 77 |
heightCm: z.string().regex(/^(|\d{2,3}(?:\.\d{1,2})?)$/),
|
| 78 |
weightKg: z.string().regex(/^(|\d{1,3}(?:\.\d{1,2})?)$/),
|
| 79 |
club: z.string().trim().max(150),
|
| 80 |
position: z.string().trim().max(50),
|
| 81 |
gender: z.union([z.enum(["male", "female"]), z.literal("")]),
|
| 82 |
-
}).strict()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
const playerCreateSchema = z.object({
|
| 84 |
fname: z.string().trim().min(1).max(100),
|
| 85 |
-
lname: z.string().trim().
|
| 86 |
age: z.string().regex(/^(|\d{1,2})$/),
|
| 87 |
heightCm: z.string().regex(/^(|\d{2,3}(?:\.\d{1,2})?)$/),
|
| 88 |
weightKg: z.string().regex(/^(|\d{1,3}(?:\.\d{1,2})?)$/),
|
|
@@ -290,6 +297,8 @@ app.get("/api/owned-players", async (req, res, next) => {
|
|
| 290 |
const ownedPlayers = admin ? data ?? [] : (data ?? []).filter((player) => player.user_id === current.user.id);
|
| 291 |
return res.json({ players: ownedPlayers.map((player) => ({
|
| 292 |
playerId: player.player_id,
|
|
|
|
|
|
|
| 293 |
name: [player.fname, player.lname].filter(Boolean).join(" "),
|
| 294 |
isOwner: player.user_id === current.user.id,
|
| 295 |
position: player.position ?? "",
|
|
@@ -321,7 +330,7 @@ app.post("/api/players", async (req, res, next) => {
|
|
| 321 |
const { data, error } = await supabaseAdmin.from("players").insert({
|
| 322 |
user_id: current.user.id,
|
| 323 |
fname: input.fname,
|
| 324 |
-
lname: input.lname,
|
| 325 |
bdate: birthday,
|
| 326 |
height,
|
| 327 |
weight,
|
|
@@ -338,6 +347,8 @@ app.post("/api/players", async (req, res, next) => {
|
|
| 338 |
if (error) throw new Error(`Unable to create player: ${error.message}`);
|
| 339 |
return res.status(201).json({ player: {
|
| 340 |
playerId: data.player_id,
|
|
|
|
|
|
|
| 341 |
name: [data.fname, data.lname].filter(Boolean).join(" "),
|
| 342 |
position: data.position ?? "",
|
| 343 |
club: data.club ?? "",
|
|
@@ -364,9 +375,6 @@ app.put("/api/players/:playerId", async (req, res, next) => {
|
|
| 364 |
.select("player_id,bdate").eq("player_id", id.data).eq("user_id", current.user.id).maybeSingle();
|
| 365 |
if (readError) throw new Error(`Unable to load player: ${readError.message}`);
|
| 366 |
if (!existing) return res.status(404).json({ error: "Player not found." });
|
| 367 |
-
const [fname, ...lastParts] = input.name.split(/\s+/);
|
| 368 |
-
const lname = lastParts.join(" ");
|
| 369 |
-
if (fname.length > 100 || lname.length > 100) return res.status(400).json({ error: "Player first and last names must be 100 characters or fewer." });
|
| 370 |
let birthday = existing.bdate;
|
| 371 |
if (age === null) birthday = null;
|
| 372 |
else if (ageFromBirthday(existing.bdate) !== String(age)) {
|
|
@@ -375,8 +383,8 @@ app.put("/api/players/:playerId", async (req, res, next) => {
|
|
| 375 |
birthday = date.toISOString().slice(0, 10);
|
| 376 |
}
|
| 377 |
const { error } = await supabaseAdmin.from("players").update({
|
| 378 |
-
fname,
|
| 379 |
-
lname,
|
| 380 |
bdate: birthday,
|
| 381 |
height,
|
| 382 |
weight,
|
|
@@ -491,6 +499,8 @@ app.get("/api/players-overview", async (req, res, next) => {
|
|
| 491 |
return {
|
| 492 |
playerId: row.player_id,
|
| 493 |
isOwner: row.user_id === current.user.id,
|
|
|
|
|
|
|
| 494 |
name: [row.fname, row.lname].filter(Boolean).join(" "),
|
| 495 |
age: ageFromBirthday(row.bdate),
|
| 496 |
heightCm: row.height == null ? "" : String(row.height),
|
|
@@ -573,7 +583,7 @@ app.get("/api/players-overview/:playerId", async (req, res, next) => {
|
|
| 573 |
const rankIndex = rankedPlayers.findIndex((entry) => entry.id === playerId.data);
|
| 574 |
const squadAverage = { byQuality: [], byAction: squadByAction };
|
| 575 |
return res.json({
|
| 576 |
-
playerId: player.player_id, name: [player.fname, player.lname].filter(Boolean).join(" "),
|
| 577 |
isOwner: player.user_id === current.user.id,
|
| 578 |
age: ageFromBirthday(player.bdate),
|
| 579 |
heightCm: player.height == null ? "" : String(player.height), weightKg: player.weight == null ? "" : String(player.weight),
|
|
|
|
| 72 |
feedback: z.array(z.string().trim().min(1).max(2000)).max(30),
|
| 73 |
}).strict();
|
| 74 |
const playerProfileSchema = z.object({
|
| 75 |
+
fname: z.string().trim().min(1).max(100).optional(),
|
| 76 |
+
lname: z.string().trim().max(100).optional(),
|
| 77 |
+
name: z.string().trim().max(200).optional(),
|
| 78 |
age: z.string().regex(/^(|\d{1,2})$/),
|
| 79 |
heightCm: z.string().regex(/^(|\d{2,3}(?:\.\d{1,2})?)$/),
|
| 80 |
weightKg: z.string().regex(/^(|\d{1,3}(?:\.\d{1,2})?)$/),
|
| 81 |
club: z.string().trim().max(150),
|
| 82 |
position: z.string().trim().max(50),
|
| 83 |
gender: z.union([z.enum(["male", "female"]), z.literal("")]),
|
| 84 |
+
}).strict().transform((input) => {
|
| 85 |
+
const legacyNameParts = (input.name ?? "").split(/\s+/).filter(Boolean);
|
| 86 |
+
const fname = input.fname ?? legacyNameParts.shift() ?? "";
|
| 87 |
+
const lname = input.lname ?? legacyNameParts.join(" ");
|
| 88 |
+
return { ...input, fname, lname };
|
| 89 |
+
}).refine((input) => Boolean(input.fname), { message: "First name is required." });
|
| 90 |
const playerCreateSchema = z.object({
|
| 91 |
fname: z.string().trim().min(1).max(100),
|
| 92 |
+
lname: z.string().trim().max(100).optional().default(""),
|
| 93 |
age: z.string().regex(/^(|\d{1,2})$/),
|
| 94 |
heightCm: z.string().regex(/^(|\d{2,3}(?:\.\d{1,2})?)$/),
|
| 95 |
weightKg: z.string().regex(/^(|\d{1,3}(?:\.\d{1,2})?)$/),
|
|
|
|
| 297 |
const ownedPlayers = admin ? data ?? [] : (data ?? []).filter((player) => player.user_id === current.user.id);
|
| 298 |
return res.json({ players: ownedPlayers.map((player) => ({
|
| 299 |
playerId: player.player_id,
|
| 300 |
+
fname: player.fname ?? "",
|
| 301 |
+
lname: player.lname ?? "",
|
| 302 |
name: [player.fname, player.lname].filter(Boolean).join(" "),
|
| 303 |
isOwner: player.user_id === current.user.id,
|
| 304 |
position: player.position ?? "",
|
|
|
|
| 330 |
const { data, error } = await supabaseAdmin.from("players").insert({
|
| 331 |
user_id: current.user.id,
|
| 332 |
fname: input.fname,
|
| 333 |
+
lname: input.lname || "",
|
| 334 |
bdate: birthday,
|
| 335 |
height,
|
| 336 |
weight,
|
|
|
|
| 347 |
if (error) throw new Error(`Unable to create player: ${error.message}`);
|
| 348 |
return res.status(201).json({ player: {
|
| 349 |
playerId: data.player_id,
|
| 350 |
+
fname: data.fname ?? "",
|
| 351 |
+
lname: data.lname ?? "",
|
| 352 |
name: [data.fname, data.lname].filter(Boolean).join(" "),
|
| 353 |
position: data.position ?? "",
|
| 354 |
club: data.club ?? "",
|
|
|
|
| 375 |
.select("player_id,bdate").eq("player_id", id.data).eq("user_id", current.user.id).maybeSingle();
|
| 376 |
if (readError) throw new Error(`Unable to load player: ${readError.message}`);
|
| 377 |
if (!existing) return res.status(404).json({ error: "Player not found." });
|
|
|
|
|
|
|
|
|
|
| 378 |
let birthday = existing.bdate;
|
| 379 |
if (age === null) birthday = null;
|
| 380 |
else if (ageFromBirthday(existing.bdate) !== String(age)) {
|
|
|
|
| 383 |
birthday = date.toISOString().slice(0, 10);
|
| 384 |
}
|
| 385 |
const { error } = await supabaseAdmin.from("players").update({
|
| 386 |
+
fname: input.fname,
|
| 387 |
+
lname: input.lname || "",
|
| 388 |
bdate: birthday,
|
| 389 |
height,
|
| 390 |
weight,
|
|
|
|
| 499 |
return {
|
| 500 |
playerId: row.player_id,
|
| 501 |
isOwner: row.user_id === current.user.id,
|
| 502 |
+
fname: row.fname ?? "",
|
| 503 |
+
lname: row.lname ?? "",
|
| 504 |
name: [row.fname, row.lname].filter(Boolean).join(" "),
|
| 505 |
age: ageFromBirthday(row.bdate),
|
| 506 |
heightCm: row.height == null ? "" : String(row.height),
|
|
|
|
| 583 |
const rankIndex = rankedPlayers.findIndex((entry) => entry.id === playerId.data);
|
| 584 |
const squadAverage = { byQuality: [], byAction: squadByAction };
|
| 585 |
return res.json({
|
| 586 |
+
playerId: player.player_id, fname: player.fname ?? "", lname: player.lname ?? "", name: [player.fname, player.lname].filter(Boolean).join(" "),
|
| 587 |
isOwner: player.user_id === current.user.id,
|
| 588 |
age: ageFromBirthday(player.bdate),
|
| 589 |
heightCm: player.height == null ? "" : String(player.height), weightKg: player.weight == null ? "" : String(player.weight),
|