Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
// Generated coverage artifacts:
"coverage/**",
]),
]);

Expand Down
108 changes: 108 additions & 0 deletions src/app/api/drivers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getIracingCustIdFromJwt } from "@/lib/auth/iracing";

/**
* GET /api/drivers?q=search&limit=30
* Search drivers by display name, nickname, or car number.
* Requires a valid session.
*/
export async function GET(request: NextRequest) {
const accessToken = request.cookies.get("irh_access_token")?.value;
if (!accessToken) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}

const iracingCustId = getIracingCustIdFromJwt(accessToken);
if (!iracingCustId) {
return NextResponse.json({ error: "invalid_token" }, { status: 401 });
}

const q = (request.nextUrl.searchParams.get("q") ?? "").trim();
const limit = Math.min(
Math.max(
1,
parseInt(request.nextUrl.searchParams.get("limit") ?? "30", 10),
),
100,
);
Comment on lines +22 to +28

try {
// If q is a plain integer treat it as a custId exact lookup
const asInt = /^\d+$/.test(q) ? parseInt(q, 10) : null;

// Build the set of custIds to fetch from User table
let custIdSet: Set<number> | null = null;

if (q) {
if (asInt) {
Comment thread
DarrellRichards marked this conversation as resolved.
custIdSet = new Set([asInt]);
} else {
// Parallel: search displayName on User AND nickName/carNumber on Member
const [userHits, memberHits] = await Promise.all([
prisma.user.findMany({
where: { displayName: { contains: q, mode: "insensitive" } },
select: { iracingCustId: true },
take: limit,
}),
prisma.member.findMany({
where: {
OR: [
{ nickName: { contains: q, mode: "insensitive" } },
{ carNumber: { contains: q, mode: "insensitive" } },
],
},
select: { custId: true },
distinct: ["custId"],
take: limit,
}),
]);

custIdSet = new Set<number>([
...userHits.map((u) => u.iracingCustId),
...memberHits.map((m) => m.custId),
]);
}
Comment on lines +41 to +65
}

// Fetch full User rows for the resolved custIds (or all if no query)
const users = await prisma.user.findMany({
where: custIdSet ? { iracingCustId: { in: [...custIdSet] } } : undefined,
select: {
iracingCustId: true,
displayName: true,
country: true,
memberSince: true,
},
orderBy: { displayName: "asc" },
take: limit,
});
Comment thread
DarrellRichards marked this conversation as resolved.
Comment thread
DarrellRichards marked this conversation as resolved.
Comment on lines +37 to +79

// Count distinct leagues each custId appears in via Member table
const custIds = users.map((u) => u.iracingCustId);
const leagueCounts = await prisma.member.groupBy({
by: ["custId"],
where: { custId: { in: custIds } },
_count: { leagueId: true },
});
const leagueCountMap = new Map(
leagueCounts.map((r) => [r.custId, r._count.leagueId]),
);

const results = users.map((u) => ({
custId: u.iracingCustId,
displayName: u.displayName ?? `Driver #${u.iracingCustId}`,
country: u.country ?? null,
memberSince: u.memberSince ? u.memberSince.toISOString() : null,
leagueCount: leagueCountMap.get(u.iracingCustId) ?? 0,
}));

return NextResponse.json({ results, total: results.length });
} catch (error) {
console.error("[drivers.search]", error);
return NextResponse.json(
{ error: "internal_server_error" },
{ status: 500 },
);
}
}
2 changes: 1 addition & 1 deletion src/app/api/leagues/[leagueId]/landing/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function buildRequest(accessToken = "token"): NextRequest {
} as unknown as NextRequest;
}

function mockLeague(overrides?: Partial<any>) {
function mockLeague(overrides?: Partial<Record<string, unknown>>) {
mocks.prisma.league.findUnique.mockResolvedValue({
id: "league-1",
iracingLeagueId: 101,
Expand Down
145 changes: 110 additions & 35 deletions src/app/api/leagues/[leagueId]/members/profile/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getIracingCustIdFromJwt } from "@/lib/auth/iracing";
import { fetchMemberProfileFromIracing } from "@/lib/auth/iracing";
Comment thread
DarrellRichards marked this conversation as resolved.
Comment on lines 2 to +3
import { prisma } from "@/lib/prisma";

async function getLeagueMemberContext(request: NextRequest, leagueId: string) {
Expand Down Expand Up @@ -62,43 +63,49 @@ export async function GET(
return NextResponse.json({ error: "invalid_cust_id" }, { status: 400 });
}

const [leagueSettings, targetMember, viewerMember] = await Promise.all([
prisma.league.findUnique({
where: { id: leagueId },
select: {
id: true,
virtualModeEnabled: true,
virtualStartingMoney: true,
},
}),
prisma.member.findUnique({
where: {
leagueId_custId: {
leagueId,
custId: targetCustId,
const [leagueSettings, targetMember, viewerMember, targetUser] =
await Promise.all([
prisma.league.findUnique({
where: { id: leagueId },
select: {
id: true,
virtualModeEnabled: true,
virtualStartingMoney: true,
},
},
select: {
id: true,
custId: true,
displayName: true,
profileHeadline: true,
profileBio: true,
carNumber: true,
nickName: true,
earnedVirtual: true,
},
}),
prisma.member.findUnique({
where: {
leagueId_custId: {
leagueId,
custId: context.user.iracingCustId,
}),
prisma.member.findUnique({
where: {
leagueId_custId: {
leagueId,
custId: targetCustId,
},
},
},
select: { id: true, custId: true },
}),
]);
select: {
id: true,
custId: true,
displayName: true,
profileHeadline: true,
profileBio: true,
carNumber: true,
nickName: true,
lastSyncedAt: true,
earnedVirtual: true,
},
}),
prisma.member.findUnique({
where: {
leagueId_custId: {
leagueId,
custId: context.user.iracingCustId,
},
},
select: { id: true, custId: true },
}),
prisma.user.findUnique({
where: { iracingCustId: targetCustId },
select: { country: true },
}),
]);

if (!leagueSettings) {
return NextResponse.json({ error: "league_not_found" }, { status: 404 });
Expand Down Expand Up @@ -166,10 +173,12 @@ export async function GET(
id: targetMember.id,
custId: targetMember.custId,
displayName: targetMember.displayName,
country: targetUser?.country ?? null,
carNumber: targetMember.carNumber,
nickName: targetMember.nickName,
profileHeadline: targetMember.profileHeadline,
profileBio: targetMember.profileBio,
lastSyncedAt: targetMember.lastSyncedAt,
},
virtualMoney: {
raceCount,
Expand All @@ -190,6 +199,72 @@ export async function GET(
}
}

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ leagueId: string }> },
) {
const { leagueId } = await params;

try {
const context = await getLeagueMemberContext(request, leagueId);
if ("error" in context) {
return context.error;
}

const accessToken = request.cookies.get("irh_access_token")?.value;
if (!accessToken) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}

const profile = await fetchMemberProfileFromIracing(accessToken);

const updated = await prisma.$transaction(async (tx) => {
await tx.user.update({
where: { id: context.user.id },
data: {
displayName: profile.displayName,
country: profile.country,
memberSince: profile.memberSince,
},
});

return tx.member.update({
where: {
leagueId_custId: {
leagueId,
custId: context.user.iracingCustId,
},
},
data: {
displayName: profile.displayName ?? undefined,
lastSyncedAt: new Date(),
},
select: {
id: true,
custId: true,
displayName: true,
profileHeadline: true,
profileBio: true,
lastSyncedAt: true,
},
});
});
Comment on lines +202 to +251

return NextResponse.json({
profile: {
...updated,
country: profile.country,
},
});
} catch (error) {
console.error("[members.profile.post]", error);
return NextResponse.json(
{ error: "internal_server_error" },
{ status: 500 },
);
}
}
Comment thread
DarrellRichards marked this conversation as resolved.

interface UpdateProfileBody {
profileHeadline?: string;
profileBio?: string;
Expand Down
Loading
Loading