Skip to content

Adding editable and driver lookup to the iracehub#8

Merged
DarrellRichards merged 3 commits into
mainfrom
driver-passport-editable
May 16, 2026
Merged

Adding editable and driver lookup to the iracehub#8
DarrellRichards merged 3 commits into
mainfrom
driver-passport-editable

Conversation

@DarrellRichards
Copy link
Copy Markdown
Owner

Summary

Changes made

How to test

  • npm run lint
  • npm run typecheck
  • npm run test
  • npm run build

Prisma / schema impact

  • No schema changes
  • Schema updated and migration included

Checklist

  • Documentation updated if needed
  • No secrets or local-only config committed
  • Screenshots included for UI changes when helpful

Copilot AI review requested due to automatic review settings May 16, 2026 13:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a global driver search experience and an editable league profile with iRacing resync. A new DriverSearchBar is embedded across page headers, a /app/drivers search page and /api/drivers endpoint are introduced, and the league member profile route/UI is extended with country/lastSyncedAt and a POST resync action.

Changes:

  • New DriverSearchBar component and /app/drivers search page backed by GET /api/drivers (name/nickname/carNumber/custId search).
  • League member profile API: returns country and lastSyncedAt; adds POST to resync the caller's iRacing identity, with corresponding UI on the driver passport.
  • Header wiring across dashboard, league, standings, calendar, teams and admin pages to include the search bar; small SSR fixes (SessionTimer hydration) and lint cleanups in admin/widgets and admin/dashboard.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
src/components/DriverSearchBar.tsx New debounced search input that navigates to /app/drivers?q=
src/app/app/drivers/page.tsx New driver search results page
src/app/app/drivers/[custId]/page.tsx Adds viewer detection, resync action, league-profile section with synced identity
src/app/api/drivers/route.ts New search endpoint joining User and Member tables
src/app/api/leagues/[leagueId]/members/profile/route.ts Adds country/lastSyncedAt to GET; adds POST resync
src/app/dashboard/page.tsx Hydration-safe SessionTimer; adds search bar and profile/find links
src/app/app/[leagueId]/page.tsx Adds search bar in header (auth-gated)
src/app/app/[leagueId]/standings/page.tsx Adds search bar in header
src/app/app/[leagueId]/calendar/page.tsx Adds search bar in header
src/app/app/[leagueId]/teams/page.tsx Adds search bar in header
src/app/app/[leagueId]/admin/page.tsx Adds search bar in header
src/app/app/[leagueId]/admin/dashboard.tsx Adds search bar; lint fixes (typed filter, removed unused icon)
src/app/app/[leagueId]/admin/join-requests/page.tsx Adds search bar in header
src/app/app/[leagueId]/admin/widgets/page.tsx Lint cleanups (read-only state, removed unused feed url, apostrophe escape)

Comment thread src/components/DriverSearchBar.tsx
Comment thread src/components/DriverSearchBar.tsx
Comment thread src/app/api/leagues/[leagueId]/members/profile/route.ts
Comment thread src/app/api/leagues/[leagueId]/members/profile/route.ts
Comment thread src/app/api/drivers/route.ts
Comment thread src/app/app/drivers/[custId]/page.tsx
Comment thread src/app/app/drivers/[custId]/page.tsx
Comment thread src/app/app/[leagueId]/admin/widgets/page.tsx
Comment thread src/app/app/[leagueId]/page.tsx
Comment thread src/app/app/[leagueId]/standings/page.tsx
Copilot AI review requested due to automatic review settings May 16, 2026 14:06
@DarrellRichards DarrellRichards merged commit 74866ce into main May 16, 2026
3 checks passed
@DarrellRichards DarrellRichards deleted the driver-passport-editable branch May 16, 2026 14:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 12 comments.

Comment on lines +22 to +28
const limit = Math.min(
Math.max(
1,
parseInt(request.nextUrl.searchParams.get("limit") ?? "30", 10),
),
100,
);
Comment on lines +37 to +79
if (q) {
if (asInt) {
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),
]);
}
}

// 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 on lines +41 to +65
// 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 2 to +3
import { getIracingCustIdFromJwt } from "@/lib/auth/iracing";
import { fetchMemberProfileFromIracing } from "@/lib/auth/iracing";
Comment on lines +202 to +251
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 +41 to +45
const [widgetTargetSelector] = useState("#irh-widget");
const [copiedField, setCopiedField] = useState<string | null>(null);
const [standingsLimitInput, setStandingsLimitInput] = useState(10);
const [scheduleLimitInput, setScheduleLimitInput] = useState(12);
const [resultsLimitInput, setResultsLimitInput] = useState(20);
const [resultsLimitInput] = useState(20);
@@ -956,7 +957,7 @@ function EventCard({
// ─── Page ─────────────────────────────────────────────────────────────────────

export default function CalendarPage() {
Comment on lines +827 to +834
{isViewingOwnDriverProfile && data?.leagues.length ? (
<Link
href={`/app/drivers/${data.driver.custId}?league=${data.leagues[0].leagueId}#league-profile`}
className="rounded-lg border border-zinc-700 px-3 py-1.5 text-xs font-semibold text-zinc-200 transition-colors hover:border-zinc-500"
>
Edit Profile
</Link>
) : null}
Comment on lines +481 to +484
const timer = setTimeout(() => {
setLeagueMembers([]);
}, 0);
return () => clearTimeout(timer);
Comment on lines +59 to +73
const [now, setNow] = useState<number | null>(null);

useEffect(() => {
const interval = setInterval(() => {
setNow(Date.now());
}, 1000);
const tick = () => setNow(Date.now());
const initTimer = setTimeout(tick, 0);

return () => clearInterval(interval);
const interval = setInterval(tick, 1000);

return () => {
clearTimeout(initTimer);
clearInterval(interval);
};
}, []);

if (!expiresAt) return null;
if (!expiresAt || now == null) return null;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants