npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pitvox/partner-react

v0.7.1

Published

React hooks and styled components for PitVox partner websites — leaderboards, competitions, driver dashboards

Downloads

3,029

Readme

@pitvox/partner-react

React SDK for PitVox partner websites. Provides hooks, styled components, and a driver dashboard for sim racing communities affiliated with PitVox.

Hooks-first: Use the data hooks with any UI framework (Tailwind, DaisyUI, Shadcn, Preline, etc.). The styled pvx-* components are optional building blocks for quick starts.

Installation

npm install @pitvox/partner-react

Peer dependencies

npm install react react-dom @tanstack/react-query

Quick start

Wrap your app with the provider:

import { PitVoxPartnerProvider } from '@pitvox/partner-react'

function App() {
  return (
    <PitVoxPartnerProvider
      partnerSlug="your-slug"
      getSteamId={() => currentUser?.steamId ?? null}
    >
      {/* your app */}
    </PitVoxPartnerProvider>
  )
}

The provider auto-creates a QueryClient if your app doesn't already have one. If you use React Query elsewhere, wrap with your own QueryClientProvider first and the SDK will share it.

Global mode

partnerSlug is optional. Omit it (or pass null) to use global CDN paths instead of partner-scoped ones. This is useful for sites like pitvox.com that display leaderboards and competitions across all partners.

<PitVoxPartnerProvider cdnUrl="https://cdn.pitvox.com">
  {/* hooks return global data */}
</PitVoxPartnerProvider>

Leaderboards

Hooks

import {
  useLeaderboardIndex,
  useTrackLeaderboard,
  useDriverLaps,
  useRecentLaps,
  useUserLookup,
  useCarMetadata,
} from '@pitvox/partner-react'

useLeaderboardIndex(options?) — Fetch all tracks with record holders.

  • options.game — Filter by game ('evo' | 'acc')
  • Returns { data: Track[], isLoading, generatedAt, totalLaps, totalUsers, versions }

useTrackLeaderboard(trackId, layout?, options?) — Fetch track entries.

  • Without options.carId: returns best lap per car (car-level)
  • With options.carId: returns all drivers for that car (driver-level)
  • Returns { data: Entry[], isLoading, error }

useDriverLaps(userId, trackId, layout, carId, options?) — Fetch a driver's lap history.

  • options.showInvalid — Include invalid laps (default false)
  • Returns { data: Lap[], isLoading, driverName, theoreticalBest }
  • theoreticalBest{ lapTimeMs, sector1Ms, sector2Ms, sector3Ms } or null. Computed from the best individual sectors across all valid laps. Only returned when it's faster than the actual best lap and there are at least 2 valid laps.

useRecentLaps() — Fetch recent lap activity.

  • Returns { groups: Activity[], generatedAt, isLoading }

useUserLookup() — Returns a lookup function: (userId, fallback?) => { displayName, avatarUrl, affiliations }

useCarMetadata() — Returns { tags: string[], cars: Record<string, { tags }> } for tag filtering.

Styled components

For building leaderboard pages with the SDK's pvx-* styles:

import { TracksTable, CarsTable, DriversTable, LapHistoryTable, RankingsTable } from '@pitvox/partner-react'
import '@pitvox/partner-react/styles.css'
  • <TracksTable> — All tracks with record holders, tag filtering, sorting
  • <CarsTable> — Cars for a selected track with tag filtering
  • <DriversTable> — Drivers for a car with sectors (S1/S2/S3), tyre, fuel. Accepts optional highlightId to visually highlight a specific driver row.
  • <LapHistoryTable> — Driver's lap history with validity and personal best highlighting
  • <RankingsTable> — Driver rankings across all car/track combos, with expandable combo details. Accepts onComboSelect callback for drill-down navigation.

You compose these into your own page layout and wire up navigation between layers. See the partner templates for a complete example using React Router.

Competitions

Hooks

import {
  useCompetitions,
  useCompetitionConfig,
  useCompetitionStandings,
  useCompetitionRound,
  useCompetitionAllRounds,
  useCompetitionEntryList,
} from '@pitvox/partner-react'

useCompetitions() — All competitions for this partner (or all competitions in global mode).

useCompetitionConfig(competitionId, options?) — Single competition config (name, rounds, countingRounds, etc.).

useCompetitionStandings(competitionId, options?) — Championship standings with per-round breakdowns.

useCompetitionRound(competitionId, roundNumber, options?) — Single round results with session data.

useCompetitionAllRounds(competitionId, roundNumbers, options?) — Fetch multiple round results in parallel.

useCompetitionEntryList(competitionId, options?) — Registered drivers.

All competition detail hooks accept options.partnerSlug to override the provider's slug. This is useful in global mode where the partner slug comes from the competition data rather than from context.

Styled components

import {
  CompetitionCards, CompetitionCard, CompetitionResultsTabs,
  StandingsTable, RoundResults, RoundSessionResults,
  EntryList, RegisterButton, RegistrationPanel,
} from '@pitvox/partner-react'
import '@pitvox/partner-react/styles.css'
  • <CompetitionResultsTabs> — Tabbed results view for a competition. Championships show a "Standings" tab (default) plus one tab per finalized round. Series/Events show round tabs only, defaulting to the most recent. Self-contained — fetches all data via hooks. Props: competitionId, className.
  • <CompetitionCards> — Card grid with posters, type badges, schedule, registration status. Bundles its own CSS grid layout.
  • <CompetitionCard> — Individual competition card. Use this when you want to control the grid layout yourself (e.g. with Tailwind). Props: comp, onSelect, onRegister.
  • <StandingsTable> — Championship standings with per-round breakdowns and per-position podium cell highlighting
  • <RoundResults> — Standalone round results (fetches data, renders header + sessions)
  • <RoundSessionResults> — Session tabs + results table (data-prop driven, no fetch)
  • <EntryList> — Registered drivers grid with avatars
  • <RegisterButton> — Register/withdraw toggle (render prop or default button)
  • <RegistrationPanel> — Registration form + entry list with unregister

Shared utilities

Useful when composing competition pages:

import { TypeBadge, InfoPill, PODIUM_MEDALS, CompLoadingState, CompEmptyState } from '@pitvox/partner-react'

Driver Dashboard

Drop-in composite

The DriverDashboard is a self-contained component with no routing dependency:

import { DriverDashboard } from '@pitvox/partner-react'
import '@pitvox/partner-react/styles.css'

function DashboardPage() {
  return (
    <DriverDashboard
      steamId={user.steamId}
      avatarUrl={user.avatarUrl}
      memberSince={user.createdAt}
    />
  )
}

| Prop | Type | Default | Description | |------|------|---------|-------------| | steamId | string | — | Driver's Steam ID (required) | | avatarUrl | string | — | Avatar URL from your auth provider | | memberSince | string | — | ISO date for "Racing since" display | | className | string | — | Additional class on root container |

The dashboard automatically includes:

  • Upcoming Events — competition rounds the driver is registered for (CDN-based, always available)
  • Notifications — only when onFetchNotifications is provided to the provider (see Notifications)

Layer components

import { DriverProfile, StatsCards, RecordsTable, UpcomingEvents, NotificationsCard } from '@pitvox/partner-react'
  • <UpcomingEvents> — Upcoming competition rounds card (accepts events array from useUpcomingEvents())
  • <NotificationsCard> — Notifications list with read/unread state (accepts notifications, unreadCount, onMarkRead, onMarkAllRead)

Hooks

import { useDriverStats, useDriverRating, useDriverRatings, useUpcomingEvents } from '@pitvox/partner-react'

useDriverStats(steamId) — Driver stats, records, and ranking from CDN.

useDriverRating(steamId) — Single driver's rating from the partner ratings file.

useDriverRatings(options?) — All driver ratings for the rankings table.

  • options.gameVersion — EVO version filter (null/undefined for ACC)
  • options.enabled — Whether to enable the query (default true)
  • Returns { data: { drivers: [...], driverCount }, isLoading, error }

useUpcomingEvents() — Upcoming competition rounds the current user is registered for (CDN-based). When onFetchServerPassword is provided to the provider, each event includes serverAddress and serverPassword fields.

Notifications

Notifications require a backend to proxy requests to pitvox-api (keeping the API key server-side). Provide callbacks to the provider:

<PitVoxPartnerProvider
  partnerSlug="your-slug"
  getSteamId={() => user?.steamId ?? null}
  onFetchNotifications={async (params) => {
    const res = await fetch(`/api/notifications?limit=${params.limit || 20}`)
    return res.json() // { notifications: [...], unreadCount: number }
  }}
  onMarkNotificationRead={async (id) => {
    await fetch(`/api/notifications/${id}/read`, { method: 'PATCH' })
  }}
  onMarkAllNotificationsRead={async () => {
    await fetch('/api/notifications/read-all', { method: 'PATCH' })
  }}
>

When no callbacks are provided, notification hooks return disabled/empty state and DriverDashboard hides the notifications section.

Hooks

import {
  useNotifications, useUnreadCount, useMarkNotificationRead,
  useMarkAllNotificationsRead, useNotificationsEnabled,
} from '@pitvox/partner-react'

useNotifications(options?) — Fetch notifications (polls every 30s). Returns { data: { notifications, unreadCount }, isLoading }.

useUnreadCount() — Unread count for navbar badges. Returns { count, isLoading }.

useMarkNotificationRead() — Mutation to mark a notification as read.

useMarkAllNotificationsRead() — Mutation to mark all as read.

useNotificationsEnabled() — Returns boolean — whether notification callbacks are provided.

Server Password

Registered drivers can see server connection details (address + password) for their upcoming events. This requires a backend query that validates registration before returning the password.

Provide the callback to the provider:

<PitVoxPartnerProvider
  partnerSlug="your-slug"
  getSteamId={() => user?.steamId ?? null}
  onFetchServerPassword={async (competitionId, roundNumber) => {
    // Call your backend (e.g. AppSync query) which validates registration
    // and returns the server details
    const result = await client.queries.getServerPassword({ competitionId, roundNumber })
    return result.data // { success, serverAddress?, serverPassword?, error? }
  }}
>

When provided, useUpcomingEvents() automatically fetches server info for each event and the <UpcomingEvents> component displays the password with copy-to-clipboard.

When no callback is provided, server info is simply omitted from events.

Registration

The SDK supports two registration modes, determined by whether you provide callbacks to the provider.

Basic mode (default)

No configuration needed. Registration components render links to pitvox.com where users register with Steam.

Power mode

For partners with a backend (e.g. Amplify Lambda proxying to pitvox-api), provide callbacks:

<PitVoxPartnerProvider
  partnerSlug="your-slug"
  getSteamId={() => user?.steamId ?? null}
  onRegister={async (competitionId, driverData) => {
    await fetch('/api/register', { method: 'POST', body: JSON.stringify({ competitionId, ...driverData }) })
  }}
  onWithdraw={async (competitionId, steamId) => {
    await fetch('/api/withdraw', { method: 'POST', body: JSON.stringify({ competitionId, steamId }) })
  }}
>

Registration hooks

import { useRegistrationStatus, useRegister, useWithdraw, useRegistrationMode, useRegistrationUrl } from '@pitvox/partner-react'

useRegistrationStatus(competitionId) — Check if current user is registered.

useRegister(competitionId) — Mutation delegating to onRegister callback.

useWithdraw(competitionId) — Mutation delegating to onWithdraw callback.

useRegistrationMode() — Returns { isPowerMode, isBasicMode }.

useRegistrationUrl(competitionId) — Returns pitvox.com registration URL for basic mode.

Formatting utilities

import {
  formatLapTime,       // 92365 → "1:32.365"
  formatSectorTime,    // 34567 → "34.567", 197487 → "3:17.487"
  formatCarName,       // "ks_ferrari_296_gt3" → "Ferrari 296 Gt3"
  formatTrackName,     // "donington_park", "national" → "Donington Park National"
  formatDate,          // ISO string → "27 Feb 2024"
  formatRelativeTime,  // ISO string → "2h ago"
  formatDelta,         // 542 → "+0.542"
  formatTyreCompound,           // "SR" → "Soft Race"
  formatNotificationMessage,    // notification → "X beat your record on Track — Car"
} from '@pitvox/partner-react'

Theming

The default stylesheet uses CSS custom properties. Override them to match your brand:

:root {
  --pvx-accent: #e11d48;
  --pvx-bg-card: #1a1a2e;
  --pvx-sector-best: #22d3ee;
  --pvx-rank-gold: #fbbf24;
}

All classes are prefixed with pvx- to avoid collisions. See styles.css for the full list of variables.

Partner templates

For a complete working site using this SDK, see:

The templates demonstrate how to compose SDK hooks and components into full pages with routing.

Local development

# In the SDK repo
npm link

# In your app
npm link @pitvox/partner-react

Add resolve.dedupe to your Vite config to avoid duplicate React instances:

// vite.config.js
export default defineConfig({
  resolve: {
    dedupe: ['react', 'react-dom', '@tanstack/react-query'],
  },
})

License

MIT