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>
This commit is contained in:
42
components/SongResult.tsx
Normal file
42
components/SongResult.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user