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.25

Published

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

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, WeatherIcon,
} 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. Completed championships automatically show a podium badge and hide the registration section.
  • <CompetitionCard> — Individual competition card. Use this when you want to control the grid layout yourself (e.g. with Tailwind). Props: comp, onSelect, onRegister.
  • <CompletedBadge> — Podium winners badge for completed championships. Fetches standings automatically. Props: competitionId, topN (default 3), label (default "Completed"), className.
  • <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
  • <WeatherIcon> — Lucide-style weather glyph (8 conditions: clear, scattered/broken clouds, overcast, drizzle, damp, rain, heavy rain) with a refresh corner badge when weatherBehaviour is dynamic. Sized in em so it scales with surrounding text. Renders nothing when weatherType is null/unknown, so it degrades cleanly while older CDN snapshots roll through. Props: weatherType, weatherBehaviour, className.

Shared utilities

Useful when composing competition pages:

import {
  TypeBadge, InfoPill, PODIUM_MEDALS, CompLoadingState, CompEmptyState,
  isCompetitionComplete, getCompletionDate, getCompetitionStatus,
  filterCompetitionsByStatus, getCompetitionPodium,
  DEFAULT_COMPLETION_GRACE_DAYS,
} from '@pitvox/partner-react'

isCompetitionComplete(comp) — Returns true if all rounds are finalised.

getCompletionDate(comp) — Returns the latest round's startTime as a Date (or null).

getCompetitionStatus(comp, graceDays?) — Returns 'active' | 'recently-completed' | 'archived'. A competition is "recently completed" for graceDays (default 3) after its final round, then becomes "archived".

filterCompetitionsByStatus(competitions, statuses, graceDays?) — Filter a list by one or more statuses.

getCompetitionPodium(standings, topN?) — Extract the top N drivers from a standings payload.

Promotions

Partner-run community promotions. The first (and currently only) type is giveaway — a one-click prize draw: drivers sign in with Steam, hit Enter, and the partner picks winners manually and publishes them. The type field is designed to grow (e.g. discount codes, free trials) without new hooks or components.

Promotions are managed by the partner on pitvox.com; the SDK is the read + entry surface for partner sites. Public data (config, entrants, winners) comes from the CDN; entry/withdraw go through the same backend-proxy pattern as competition registration.

Drop-in composite (recommended)

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

function GiveawaysPage() {
  return <PromotionExplorer title="Giveaways" />
}

PromotionExplorer is the whole experience in one component: a card grid that drills into a detail view (poster, markdown description, one-click entry with a public-display disclosure, live entrants grid, and the winners panel once announced). Selection lives in the ?promotion= URL search param via the History API — links are shareable and the browser back button works — so it needs no router.

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string | 'Promotions' | Page heading (pass '' to hide it) | | driverData | object | — | Extra fields sent to the enter callback (e.g. displayName, avatarUrl); usually unnecessary since the backend derives identity from the Steam token | | className | string | — | Additional class on root container |

Hooks

import {
  usePromotions,
  usePromotionConfig,
  usePromotionEntryList,
  usePromotionEntryStatus,
} from '@pitvox/partner-react'

usePromotions() — All active promotions for this partner (or all promotions in global mode). The CDN index carries active promotions only, so an empty result means nothing is running — handy for conditionally showing a nav link.

usePromotionConfig(promotionId, options?) — Single promotion config (title, prize, markdown description, entry window, winners).

usePromotionEntryList(promotionId, options?) — Entrants (Steam ID, display name, avatar, entered-at). null until the first entry.

usePromotionEntryStatus(promotionId) — Whether the current user (via getSteamId) has entered, plus the entry list. Lightweight CDN check.

All detail hooks accept options.partnerSlug to override the provider's slug (useful in global mode).

Styled components

import {
  PromotionExplorer, PromotionCards, PromotionCard,
  PromotionDetail, EnterButton,
} from '@pitvox/partner-react'
import '@pitvox/partner-react/styles.css'
  • <PromotionExplorer> — The drop-in composite above.
  • <PromotionCards> — Card grid with posters, status/type badges, prize, entry window, and entry count. Adapts the layout to the card count (a lone promotion renders as one wide centred card). Props: promotions, isLoading, onSelect, className.
  • <PromotionCard> — Individual card, for when you want to control the grid yourself. Props: promo, onSelect.
  • <PromotionDetail> — Full detail view: poster, markdown body, entrants grid, the entry action with public-display disclosure, and the winners panel. Self-contained — fetches via hooks. Props: promotionId, driverData, onBack (omit to hide the back link), className.
  • <EnterButton> — Enter/withdraw toggle. Render-prop or default button, with the same basic/power-mode behaviour as RegisterButton (see below). Props: promotionId, driverData, className, children (render prop).

Entry modes

Like registration, entry has two modes determined by whether you provide callbacks to the provider.

Basic mode (default) — no configuration; EnterButton and the detail view render a link to pitvox.com where the user enters with Steam.

Power mode — for partners with a backend proxying to pitvox-api (keeping the partner API key server-side), provide the callbacks. The Steam ID is derived server-side from the user's token, not from client input:

<PitVoxPartnerProvider
  partnerSlug="your-slug"
  getSteamId={() => user?.steamId ?? null}
  onEnterPromotion={async (promotionId, driverData) => {
    // Call your backend (e.g. AppSync mutation) → pitvox-api
    await client.graphql({ query: ENTER_PROMOTION, variables: { promotionId, ...driverData } })
  }}
  onWithdrawPromotionEntry={async (promotionId, steamId) => {
    await client.graphql({ query: WITHDRAW_PROMOTION_ENTRY, variables: { promotionId } })
  }}
>

Entry/withdraw hooks for fully custom UIs:

import { useEnterPromotion, useWithdrawPromotionEntry, usePromotionMode, usePromotionUrl } from '@pitvox/partner-react'

useEnterPromotion(promotionId) — Mutation delegating to onEnterPromotion, with optimistic entry-list updates.

useWithdrawPromotionEntry(promotionId) — Mutation delegating to onWithdrawPromotionEntry. Withdrawals are only accepted while entries are open.

usePromotionMode() — Returns { isPowerMode, isBasicMode }.

usePromotionUrl(promotionId) — pitvox.com promotion URL for basic mode.

Status helper

import { getPromotionStatus, PROMOTION_STATUS_LABELS } from '@pitvox/partner-react'

getPromotionStatus(promotion, now?) — Returns 'upcoming' | 'open' | 'closed' | 'winners', derived from the clock against opensAt/closesAt (the CDN never stores a computed open/closed flag, so files can't go stale between syncs). PROMOTION_STATUS_LABELS maps each to a display string.

Driver Dashboard

Drop-in composite

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

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

function DashboardPage() {
  const navigate = useNavigate()

  const handleComboSelect = (combo) => {
    const params = new URLSearchParams()
    params.set('track', `${combo.trackId}|${combo.trackLayout || ''}`)
    params.set('car', combo.carId)
    params.set('highlight', user.steamId)
    navigate(`/leaderboards?${params.toString()}`)
  }

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

| 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 | | hideProfile | boolean | false | Skip the avatar/name profile card. Useful when the host site already shows the user's identity in a navbar — saves redundant page real estate. | | onComboSelect | (combo) => void | — | Called when a Recent Combos row is clicked. Wire to your router to take the driver to the partner-scoped leaderboard for that combo (see example above). When omitted, rows render as static. | | onGameRatingSelect | (entry) => void | — | Called when a Driver Rating chip is clicked, with {game, label, rating, rank, totalDrivers}. Wire to your rankings page if you have one. | | className | string | — | Additional class on root container |

The dashboard automatically includes:

  • Stats Cards — Total Laps, Cars Used (each with a click-to-toggle breakdown popover), and per-game Driver Rating chips (one chip per game where the driver has a rating, with rank info)
  • Upcoming Events — competition rounds the driver is registered for (CDN-based, always available)
  • Recent Combos — every (track, layout, car, game, version) the driver has touched in the partner scope, sorted by lastDrivenAt desc, with rank/gap and a leader-trophy icon for combos where the driver holds the record. Replaces the older Records table — held records surface as trophies on the relevant combo rows.
  • Notifications — only when onFetchNotifications is provided to the provider (see Notifications)

Layer components

import {
  DriverProfile,
  StatsCards,
  RecentCombosCard,
  RecordsTable,
  UpcomingEvents,
  NotificationsCard,
} from '@pitvox/partner-react'
  • <StatsCards> — Stats row. Pass gameRatings (from useDriverRatingsByGame) for the new chips behaviour, or rating (from useDriverRating) for the legacy single-number layout. Optional onGameRatingSelect.
  • <RecentCombosCard> — Recent combos list with rank/gap and trophy treatment. Accepts combos (from useDriverCombos) and optional onComboSelect(combo) for row navigation.
  • <RecordsTable> — Legacy "Current Records" list, still exported for consumers that prefer the explicit records UI. The composite DriverDashboard no longer renders it (records are surfaced as trophies on RecentCombosCard rows instead).
  • <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,
  useDriverCombos,
  useDriverRating,
  useDriverRatings,
  useDriverRatingsByGame,
  useUpcomingEvents,
} from '@pitvox/partner-react'

useDriverStats(steamId) — Driver stats, records, and ranking from CDN. Always fetches the global index (not partner-scoped) so stats reflect the driver's whole career, not just partner-affiliated activity.

useDriverCombos(steamId) — Per-(track, layout, car, game, version) combo list, partner-scoped via the provider's partnerSlug. Each entry: {trackId, trackLayout, carId, game, gameVersion, lapCount, validLapCount, lastDrivenAt, personalBestMs, rank, totalDrivers, gapToLeaderMs, gapToNextMs}. Sorted server-side by lastDrivenAt desc. Rank/gap fields refresh on the 5-min full pass on the partner CDN path; lapCount and lastDrivenAt are always fresh.

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

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

  • options.game — Game identifier ('evo', 'acc', 'lmu')
  • options.gameVersion — Version filter for versioned games
  • options.enabled — Whether to enable the query (default true)
  • Returns { data: { drivers: [...], driverCount }, isLoading, error }

useDriverRatingsByGame(steamId) — Per-game rating chips for one driver. Reads default versions from the leaderboard index's versions metadata (no hardcode), queries each game's ratings file in parallel, and returns [{game, label, rating, rank, totalDrivers}] for games where the driver appears. Order matches the leaderboards page tabs (EVO, ACC, LMU). Drives the chip layout in <StatsCards>.

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"
  formatFuel,                   // 27.822 → "27.8L", 50 → "50L", null → "-"
  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