Cycle hire docks
Santander Cycles docks as a map and a detail list, from the same bike-point rows.
Preview
Usage
Installation
pnpm dlx shadcn@latest add https://tfl.manglekuo.com/r/cycle-hire-docks.jsonSantander Cycles docks as a map and a detail list, from the same bike-point rows.
pnpm dlx shadcn@latest add https://tfl.manglekuo.com/r/cycle-hire-docks.jsonMap
Detail
Data from Transport for London via tfl-ts. Pass normalised rows as data.
import TflClient from "tfl-ts"
const tfl = new TflClient({
appKey: process.env.TFL_APP_KEY!,
})
const data = await Promise.all([
tfl.bikePoint.getById("BikePoints_237"),
tfl.bikePoint.getById("BikePoints_490"),
tfl.bikePoint.getById("BikePoints_46"),
])data is the public tfl.bikePoint.getById() row type, aliased here as CycleHireDock. Nearby search:
const nearby = await tfl.bikePoint.getByRadius({
lat: 51.508418,
lon: -0.067048,
radius: 500,
})
// nearby.places, same shapeOn this docs site, getCachedBikePoints in @/lib/tfl/cycle-hire-data wraps the curated IDs with Cache Components.
Neither surface fetches. Layout chrome, full-screen map, Sheet, floating card, stays in the app.
data on each surface)CycleHireDocksMap and CycleHireDocksDetail are plain named exports —
render them straight from a Server Component page:
import {
CycleHireDocksMap,
CycleHireDocksDetail,
} from "@/components/tfl/cycle-hire/cycle-hire-docks"
export default async function Page() {
const data = await getBikePoints()
return (
<>
<CycleHireDocksMap data={data} className="h-dvh" />
<CycleHireDocksDetail data={data} hideHeader />
</>
)
}data via the compound)CycleHireDocks.Map / CycleHireDocks.Detail read data from a Provider
instead of a prop, so you don't have to pass it twice. The compound is a
static property on a Client Component ("use client") — resolve data in a
Server Component first, then hand it to a Client Component that renders the
compound. Reaching CycleHireDocks.Map from a Server Component module
throws (RSC client references don't keep static properties); use the named
exports above there instead.
"use client"
import { CycleHireDocks } from "@/components/tfl/cycle-hire/cycle-hire-docks"
export function DocksView({ data }) {
return (
<CycleHireDocks data={data}>
<CycleHireDocks.Map className="h-dvh" />
<CycleHireDocks.Detail hideHeader />
</CycleHireDocks>
)
}Mount one surface or both.
import {
CycleHireDocks,
DEFAULT_CYCLE_HIRE_DOCK_IDS,
} from "@/components/tfl/cycle-hire/cycle-hire-docks"
import { getCachedBikePoints } from "@/lib/tfl/cycle-hire-data"
export default async function Page() {
const data = await getCachedBikePoints(DEFAULT_CYCLE_HIRE_DOCK_IDS)
return (
<CycleHireDocks data={data}>
<CycleHireDocks.Map />
<CycleHireDocks.Detail hideHeader />
</CycleHireDocks>
)
}import { CycleHireDockRow } from "@/components/tfl/cycle-hire/cycle-hire-docks"
<CycleHireDockRow dock={data[0]} />Broken slots are hidden by default. Pass showBroken on Detail or a row:
<CycleHireDocks.Detail hideHeader showBroken />| Prop / API | Type | Description |
|---|---|---|
CycleHireDocks | { data, children } | Optional Provider root. Inject data once for Map / Detail children. |
CycleHireDocks.Map / CycleHireDocksMap | data?, className? | OSM MapLibre surface. Markers need lat / lon on each row. |
CycleHireDocks.Detail / CycleHireDocksDetail | data?, showBroken?, hideHeader? | Occupancy bars and counts. |
showBroken | boolean | Include broken docks in the detail bar. Default false. |
Map markers: bike, then e-bike, then empty ring. Broken omitted. Lucide Bike in the centre. Map labels use StationName. They hang below the pin, then flip above when they would cover another pin or leave the map.
Detail segment order: bike, e-bike, empty, then broken when showBroken. Accents on light UI: classic bikes red-500, e-bikes cyan-400, broken amber-300.
Map and Detail are not selection-linked. Compose that in the app if you need it.
Map
Detail
Data from Transport for London via tfl-ts. Pass normalised rows as data.
import TflClient from "tfl-ts"
const tfl = new TflClient({
appKey: process.env.TFL_APP_KEY!,
})
const data = await Promise.all([
tfl.bikePoint.getById("BikePoints_237"),
tfl.bikePoint.getById("BikePoints_490"),
tfl.bikePoint.getById("BikePoints_46"),
])data is the public tfl.bikePoint.getById() row type, aliased here as CycleHireDock. Nearby search:
const nearby = await tfl.bikePoint.getByRadius({
lat: 51.508418,
lon: -0.067048,
radius: 500,
})
// nearby.places, same shapeOn this docs site, getCachedBikePoints in @/lib/tfl/cycle-hire-data wraps the curated IDs with Cache Components.
Neither surface fetches. Layout chrome, full-screen map, Sheet, floating card, stays in the app.
data on each surface)CycleHireDocksMap and CycleHireDocksDetail are plain named exports —
render them straight from a Server Component page:
import {
CycleHireDocksMap,
CycleHireDocksDetail,
} from "@/components/tfl/cycle-hire/cycle-hire-docks"
export default async function Page() {
const data = await getBikePoints()
return (
<>
<CycleHireDocksMap data={data} className="h-dvh" />
<CycleHireDocksDetail data={data} hideHeader />
</>
)
}data via the compound)CycleHireDocks.Map / CycleHireDocks.Detail read data from a Provider
instead of a prop, so you don't have to pass it twice. The compound is a
static property on a Client Component ("use client") — resolve data in a
Server Component first, then hand it to a Client Component that renders the
compound. Reaching CycleHireDocks.Map from a Server Component module
throws (RSC client references don't keep static properties); use the named
exports above there instead.
"use client"
import { CycleHireDocks } from "@/components/tfl/cycle-hire/cycle-hire-docks"
export function DocksView({ data }) {
return (
<CycleHireDocks data={data}>
<CycleHireDocks.Map className="h-dvh" />
<CycleHireDocks.Detail hideHeader />
</CycleHireDocks>
)
}Mount one surface or both.
import {
CycleHireDocks,
DEFAULT_CYCLE_HIRE_DOCK_IDS,
} from "@/components/tfl/cycle-hire/cycle-hire-docks"
import { getCachedBikePoints } from "@/lib/tfl/cycle-hire-data"
export default async function Page() {
const data = await getCachedBikePoints(DEFAULT_CYCLE_HIRE_DOCK_IDS)
return (
<CycleHireDocks data={data}>
<CycleHireDocks.Map />
<CycleHireDocks.Detail hideHeader />
</CycleHireDocks>
)
}import { CycleHireDockRow } from "@/components/tfl/cycle-hire/cycle-hire-docks"
<CycleHireDockRow dock={data[0]} />Broken slots are hidden by default. Pass showBroken on Detail or a row:
<CycleHireDocks.Detail hideHeader showBroken />| Prop / API | Type | Description |
|---|---|---|
CycleHireDocks | { data, children } | Optional Provider root. Inject data once for Map / Detail children. |
CycleHireDocks.Map / CycleHireDocksMap | data?, className? | OSM MapLibre surface. Markers need lat / lon on each row. |
CycleHireDocks.Detail / CycleHireDocksDetail | data?, showBroken?, hideHeader? | Occupancy bars and counts. |
showBroken | boolean | Include broken docks in the detail bar. Default false. |
Map markers: bike, then e-bike, then empty ring. Broken omitted. Lucide Bike in the centre. Map labels use StationName. They hang below the pin, then flip above when they would cover another pin or leave the map.
Detail segment order: bike, e-bike, empty, then broken when showBroken. Accents on light UI: classic bikes red-500, e-bikes cyan-400, broken amber-300.
Map and Detail are not selection-linked. Compose that in the app if you need it.