Adding editable and driver lookup to the iracehub#8
Merged
Conversation
There was a problem hiding this comment.
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
DriverSearchBarcomponent and/app/driverssearch page backed byGET /api/drivers(name/nickname/carNumber/custId search). - League member profile API: returns
countryandlastSyncedAt; addsPOSTto 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 (
SessionTimerhydration) and lint cleanups inadmin/widgetsandadmin/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 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Changes made
How to test
npm run lintnpm run typechecknpm run testnpm run buildPrisma / schema impact
Checklist