Skip to content

Species card-grid page, real audio playback, and visual polish - #2

Merged
CalebLovell merged 3 commits into
mainfrom
feat/seed-data-nature-redesign
Jul 22, 2026
Merged

Species card-grid page, real audio playback, and visual polish#2
CalebLovell merged 3 commits into
mainfrom
feat/seed-data-nature-redesign

Conversation

@CalebLovell

Copy link
Copy Markdown
Owner

Summary

PR #1 merged before these follow-up commits were pushed to the same branch, so they never made it into main. This PR picks them up.

  • Visual polish: removes the hill/tree/sun hero graphic and the dotted paper-grain background (flat solid background now), reduces the shared corner-radius token for a less rounded look site-wide, and adds scrollbar-gutter: stable so filtering doesn't shift the page horizontally.
  • Species page rebuild: replaces the old table with a card grid — search box, sort (Most recordings / Recent / A–Z with icons and pagination), each card showing a Wikipedia thumbnail, this-hour/all-time counts, and Play/Wiki/eBird actions.
  • Real audio playback: adds BIRDNET_EXTRACTED_DIR config and a dynamic server route that streams clips from BirdNET-Pi's actual extraction path (BirdSongs/Extracted/By_Date/..., a sibling of the repo, matching production layout). Worked around a few real bugs in this bleeding-edge TanStack Start/Nitro version along the way (details in the commit message).
  • Seed script: now generates one placeholder audio clip per species so the play button has something real to play locally.

Test plan

  • tsc --noEmit and biome check clean
  • npm run build succeeds
  • Verified in both a dev server and a real production build: search/sort/pagination work, images load, and the play button streams and plays real audio end-to-end

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

CalebLovell and others added 3 commits July 22, 2026 17:58
Remove the hill/tree/sun hero graphic and the dotted paper-grain
background texture per feedback -- flat solid background everywhere.
Also reduce the shared --radius token so cards, buttons, inputs, and
badges read with tighter, less rounded corners site-wide, and add
scrollbar-gutter: stable so pages that change height (e.g. filtering
a grid) don't shift horizontally when the scrollbar appears/disappears.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…udio playback

Rebuilds the species listing as a card grid (search box; sort by most
recordings / recent / A-Z with icons and pagination), each card showing
a Wikipedia thumbnail (with graceful fallback), this-hour/all-time
counts, and Play/Wiki/eBird actions.

Audio playback is fully wired, not a placeholder: adds
BIRDNET_EXTRACTED_DIR config and a dynamic server route that streams
clips from BirdNET-Pi's real extraction path (BirdSongs/Extracted/
By_Date/..., a sibling of the repo, matching production layout).
Wikipedia images come from their public summary API (image + canonical
page URL, no key needed); eBird links go through a scoped search since
their species pages require login for anonymous requests.

Worked around three real bugs in this bleeding-edge TanStack Start/
Nitro version along the way: a Node-only import leaking into the
client bundle (fixed by marking db/index.ts server-only and splitting
audio.ts's filesystem code into audio.server.ts), a 3-segment dynamic
route silently failing to register (collapsed to 2 segments), and
native <audio src> requests 404ing while identical fetch() calls
succeeded (worked around by fetching the clip and playing it from a
Blob URL). Verified in both dev and a production build.

Card layout also fixes two bugs found after building it: the image
wrapper's height varied across cards despite aspect-square (a flex/
aspect-ratio interaction quirk) -- replaced with an explicit fixed
height. Reordered content to name/sciname, then image, then stats,
then actions, top to bottom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
For each species, synthesizes a short distinct tone at its most recent
detection's extraction path (BirdSongs/Extracted/By_Date/...), matching
the same BIRDNET_EXTRACTED_DIR default web-ui uses, so the new
play-button feature has real audio to play during local dev.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 23:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR brings back follow-up commits for the web UI: it refines the site’s visual styling, rebuilds the /species route as a searchable/sortable card grid, and adds an API route to play real extracted audio clips (plus seed-data support for local playback).

Changes:

  • Visual updates: flatter background, smaller global radius, and stable scrollbar gutter to prevent layout shift.
  • /species redesign: card grid with search, sort modes, pagination, thumbnails, counts, and Play/Wiki/eBird actions.
  • Real audio playback: /api/audio/... route + helpers/config to stream extracted clips; seed script now generates placeholder wavs per species for local dev.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
web-ui/src/styles.css Tweaks global radius, removes paper grain overlay, adds scrollbar-gutter: stable.
web-ui/src/routeTree.gen.ts Registers the new dynamic audio API route.
web-ui/src/routes/species.tsx Replaces the species table with a searchable/sortable/paginated card grid + audio play UI.
web-ui/src/routes/index.tsx Removes the HeroBand from the home page and aligns updated rounding.
web-ui/src/routes/detections.tsx Adjusts card rounding to match new radius styling.
web-ui/src/routes/api/audio/$date/$speciesAndFile.ts Adds an API handler that serves extracted audio clips by date/species/file.
web-ui/src/lib/wikipedia.ts Adds Wikipedia thumbnail + link lookup with in-memory caching, plus eBird search URL helper.
web-ui/src/lib/detections.ts Adds getLifeListCards() to power the new species page (counts + latest clip + external links).
web-ui/src/lib/audio.ts Adds URL building/encoding helpers for the new audio route.
web-ui/src/lib/audio.server.ts Adds server-only extracted-clip resolution and MIME-type detection.
web-ui/src/db/index.ts Marks DB access as server-only.
web-ui/src/components/ui/toggle.tsx Adds a Toggle UI primitive used by the sort controls.
web-ui/src/components/ui/toggle-group.tsx Adds ToggleGroup/ToggleGroupItem for sort controls.
web-ui/src/components/ui/pagination.tsx Adds pagination UI components used on /species.
web-ui/src/components/ui/input.tsx Adds an Input UI component used for species search.
web-ui/src/components/HeroBand.tsx Removes the HeroBand SVG component.
scripts/seed_test_data.py Adds placeholder wav generation so local species cards can play real audio.
Comments suppressed due to low confidence (1)

web-ui/src/lib/audio.ts:49

  • splitSpeciesAndFile() will currently throw if encoded isn’t valid base64url (via atob()), which would turn malformed requests into 500s. Returning an empty tuple on decode/format failure lets the API route reliably return 404 instead.
export function splitSpeciesAndFile(
	encoded: string,
): [species: string, file: string] {
	const [species, file] = base64UrlDecode(encoded).split(SEGMENT_SEPARATOR);
	return [species ?? "", file ?? ""];
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

MoreHorizontalIcon,
} from "lucide-react";
import type * as React from "react";
import { type Button, buttonVariants } from "#/components/ui/button.tsx";
Comment on lines +33 to +40
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className,
)}
Comment thread web-ui/src/lib/audio.ts
Comment on lines +20 to +32
function base64UrlEncode(value: string): string {
return btoa(value)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
}

function base64UrlDecode(value: string): string {
const padded = value.replaceAll("-", "+").replaceAll("_", "/");
const padding =
padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4));
return atob(padded + padding);
}
Comment on lines +17 to +21
try {
const data = await readFile(resolved);
return new Response(data, {
headers: {
"Content-Type": mimeTypeFor(resolved),
Comment on lines 103 to +107
.from(detections)
.groupBy(detections.Com_Name, detections.Sci_Name)
.orderBy(sql`count(*) desc`);
.orderBy(desc(detections.Date), desc(detections.Time));
const latestByName = new Map<
string,
{ date: string; time: string; fileName: string }
@CalebLovell
CalebLovell merged commit 8f6cd2d into main Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants