Skip to content

About

Editorial-style data dashboard for all 30 NBA franchises. Built with Next.js 16, TypeScript, and Tailwind CSS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

NBA Teams Dashboard

A responsive data dashboard showcasing all 30 NBA franchises — built as a front-end take-home project in Next.js 16 with a focus on aesthetic polish, accessibility, and thoughtful architecture.

Live demo: nba-teams-dashboard.vercel.app

Next.js TypeScript Tailwind CSS Framer Motion


Dark Light
Dark mode list view Light mode list view
Dark mode detail view Light mode detail view

Highlights

  • Editorial visual design — per-team accent colors pulled from the API, Space Grotesk display type, and a dark mode that's the default (not an afterthought)
  • Zero-flash theme toggle — a blocking inline script sets the theme before first paint, so reloading never shows a white flash in dark mode
  • Real React 19 concurrent features — search uses useDeferredValue so typing never blocks rendering, even on a throttled CPU
  • Server Components by default — data fetching lives in async server components, client components only where interactivity demands them

Features

  • All 30 NBA teams in a responsive grid (1 → 2 → 3 → 4 columns across breakpoints)
  • Live client-side search by name, city, or abbreviation
  • Dedicated team detail pages with badge, arena, capacity, founding year, description, and roster
  • Skeleton loading, empty, and error states at both the list and detail levels
  • Error boundaries with a working "Try again" button that refetches
  • Dark / light mode toggle persisted to localStorage, respects prefers-color-scheme on first visit
  • Staggered card reveal animations and hover-lift via Framer Motion
  • Fully keyboard navigable with visible focus rings, ARIA live regions on search, and semantic landmarks
  • Responsive from 375px up, tested across mobile, tablet, and desktop breakpoints

Quick start

Requires Node.js 20 or higher.

git clone https://github.com/akallam04/nba-dashboard.git
cd nba-dashboard
npm install
npm run dev

Open http://localhost:3000. No environment variables or API keys required — the project uses TheSportsDB's free public tier.

For a production build:

npm run build
npm start

Architecture

Why App Router over Pages Router

The App Router (stable since Next.js 13.4) co-locates data fetching with the components that consume it via async Server Components. That eliminates the prop-drilling and loading-waterfall problems inherent to getServerSideProps. Each route segment owns its own loading.tsx and error.tsx, which Next wraps in <Suspense> and <ErrorBoundary> automatically — so route-level loading and error UI cost almost no code.

Server vs. Client Components

Component Rendering Why
app/page.tsx Server Reads all 30 teams from local snapshot at request time, no client JS needed for initial render
app/teams/[id]/page.tsx Server Reads team from local snapshot, fetches roster server-side
TeamGrid Client Owns search state, needs useState and useDeferredValue
TeamCard Client Framer Motion animations require browser APIs
ThemeToggle Client Reads and writes localStorage, mutates the DOM

State management

Deliberately minimal — no Redux, no Zustand, no Context. The only state in the app is:

  1. Search query (useState inside TeamGrid, filtered with useDeferredValue)
  2. Theme preference (localStorage plus a data-theme attribute on the <html> element)

Adding a favorites feature would be the natural trigger for introducing persisted local state, and a team comparison view would justify a lightweight store.

Zero-flash theme toggle

The standard pitfall with dark mode is the brief white flash on page load before JavaScript runs and applies the user's saved preference. The fix is a blocking synchronous <script> in <head> that runs before paint — it reads localStorage, falls back to prefers-color-scheme, and sets data-theme on <html> before any styles apply. CSS custom properties keyed on [data-theme] handle the actual color switching from there, so navigation never re-flashes.

Error boundaries

Three error entry points are wired up:

  • app/error.tsx catches list page fetch failures
  • app/teams/[id]/error.tsx catches detail page fetch failures
  • app/teams/[id]/not-found.tsx renders when notFound() is thrown for an invalid team id

Resilient data layer with self-hosted assets

The dashboard reads all team data from a committed JSON snapshot at src/data/nba-teams.json, and serves all team badges from public/team-badges/ — no third-party CDN dependencies for the core experience. This is a deliberate resilience decision rather than a workaround.

The original implementation used TheSportsDB's free public API for both list (search_all_teams.php) and detail (lookupteam.php) data. Both endpoints have since become unreliable on the free tier — the list endpoint silently caps at 10 results, and individual team lookups have started returning unrelated data. Rather than gate the project behind a paid API key, I snapshotted the full 30-team dataset, downloaded the badge images into the repo, and made the dashboard self-contained for the parts users see most.

A live API call still exists for the optional roster section on team detail pages. When the API returns players, they render under "Featured Players." When it returns nothing, the section is hidden — the rest of the page is unaffected.

The trade-off is that team metadata (arena, founding year, badge image) is point-in-time. Refreshing the snapshot is a small script away. For a portfolio project that should never serve a degraded experience to a recruiter, the trade is worth it.

Project structure

src/
├── app/
│   ├── layout.tsx              # Root layout: fonts, theme script, header
│   ├── page.tsx                # List page (Server Component)
│   ├── loading.tsx             # Route-level skeleton
│   ├── error.tsx               # Route-level error boundary
│   ├── globals.css             # Tailwind + theme CSS variables
│   └── teams/[id]/
│       ├── page.tsx            # Detail page (Server Component)
│       ├── loading.tsx
│       └── error.tsx
├── components/
│   ├── Button.tsx              # Reusable — variants + sizes
│   ├── LoadingIndicator.tsx    # Spinner + skeleton variants
│   ├── TeamCard.tsx            # Per-team accent color + motion
│   ├── TeamGrid.tsx            # Client: search state + filtered render
│   ├── PlayerCard.tsx
│   ├── SearchInput.tsx         # Controlled input + ARIA live region
│   ├── EmptyState.tsx
│   ├── ErrorState.tsx
│   ├── ThemeToggle.tsx
│   └── Header.tsx
├── lib/
│   ├── api.ts                  # Typed fetch wrappers
│   └── utils.ts                # cn() + color/URL helpers
└── types/
    └── index.ts                # Team, Player, API response types

Known limitations

  • Team metadata is a committed snapshot. Arena names, founding years, and badge images are point-in-time as of the last snapshot refresh. They do not auto-update.
  • Roster data depends on an external free API tier. The "Featured Players" section on detail pages calls TheSportsDB's lookup_all_players.php endpoint, which has uneven availability across teams on the free tier. Where a team returns no players, the section is hidden gracefully rather than rendering empty.
  • No automated tests yet. Unit and end-to-end tests are listed in the roadmap.
  • No pagination. With 30 NBA franchises the list fits on one screen; pagination would only matter at league-wide player scale.

Roadmap

Things I'd tackle next with more time:

  • Unit tests with Vitest + React Testing Library, targeting TeamGrid search logic and the theme toggle's localStorage handling
  • End-to-end tests with Playwright for the list → search → detail → back flow
  • Favorites feature persisted to localStorage, with starred teams pinned to the top of the grid
  • Team comparison view for side-by-side stats
  • Conference / division filter chips alongside search
  • Dynamic OG images per team via opengraph-image.tsx
  • GitHub Actions CI: lint + typecheck + build on every PR
  • Full WCAG 2.1 AA audit with axe-core
  • Lighthouse 90+ across Performance, Accessibility, Best Practices, and SEO

License

MIT

About

Editorial-style data dashboard for all 30 NBA franchises. Built with Next.js 16, TypeScript, and Tailwind CSS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages