Files
clicktrack/components/SongResult.tsx
AJ Avezzano 5b772655c6 feat: initial scaffold for ClickTrack monorepo
Full self-hosted click track generator for cover bands.

Core technical pieces implemented:
- CTP (Click Track Protocol) TypeScript schema, Zod validator, and WAV
  renderer (44.1 kHz, 16-bit PCM, accented downbeats, ramp sections)
- MusicBrainz API client with 1 req/s rate limiting
- PostgreSQL schema (songs, tempo_maps, registry_sync_log) with triggers
- Git registry sync logic (clone/pull → validate CTP → upsert DB)
- Next.js 14 App Router: search page, track page, API routes
  (/api/songs, /api/tracks, /api/generate)
- UI components: SearchBar, SongResult, TempoMapEditor, ClickTrackPlayer
  (Web Audio API in-browser playback + WAV download)
- Docker Compose stack: app + postgres + redis + nginx + registry-sync
- Multi-stage Dockerfile with standalone Next.js output
- .env.example documenting all configuration variables
- README with setup instructions, CTP format spec, and API reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 11:14:46 -04:00

43 lines
1.4 KiB
TypeScript

import Link from "next/link";
import type { SongRow } from "@/lib/db/client";
interface SongResultProps {
song: SongRow;
}
function formatDuration(seconds: number | null): string {
if (!seconds) return "";
const m = Math.floor(seconds / 60);
const s = Math.round(seconds % 60);
return `${m}:${String(s).padStart(2, "0")}`;
}
export default function SongResult({ song }: SongResultProps) {
const duration = formatDuration(song.duration_seconds);
return (
<li>
<Link
href={`/track/${song.mbid}`}
className="flex items-center justify-between rounded-lg border border-zinc-800 bg-zinc-900 px-5 py-4 hover:border-green-600 hover:bg-zinc-800/60 transition-colors group"
>
<div className="min-w-0">
<p className="font-medium text-zinc-100 truncate group-hover:text-green-400 transition-colors">
{song.title}
</p>
<p className="text-sm text-zinc-500 truncate">{song.artist}</p>
</div>
<div className="ml-4 flex shrink-0 items-center gap-4 text-sm text-zinc-600">
{duration && <span>{duration}</span>}
{song.acousticbrainz_bpm && (
<span className="rounded bg-zinc-800 px-2 py-0.5 text-xs">
~{Math.round(Number(song.acousticbrainz_bpm))} BPM
</span>
)}
<span className="text-green-600 font-medium"></span>
</div>
</Link>
</li>
);
}