Files
clicktrack/components/TempoMapEditor.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

131 lines
4.2 KiB
TypeScript

"use client";
/**
* TempoMapEditor
*
* Renders a read-only or editable view of a CTP document's sections.
* The full interactive editor (drag-to-resize bars, BPM knob, etc.)
* is a future milestone — this version is a structured table view
* with inline editing stubs.
*/
import type { CTPDocument, CTPSection } from "@/lib/ctp/schema";
import { isRampSection, sectionStartBpm } from "@/lib/ctp/schema";
interface TempoMapEditorProps {
ctpDoc: CTPDocument;
/** When true, all inputs are disabled. */
readOnly?: boolean;
onChange?: (doc: CTPDocument) => void;
}
function timeSigLabel(ts: CTPSection["time_signature"]): string {
return `${ts.numerator}/${ts.denominator}`;
}
function bpmLabel(section: CTPSection): string {
if (isRampSection(section)) {
return `${section.bpm_start}${section.bpm_end}`;
}
return String(section.bpm);
}
export default function TempoMapEditor({
ctpDoc,
readOnly = false,
}: TempoMapEditorProps) {
const { metadata, count_in, sections } = ctpDoc;
return (
<div className="space-y-5">
{/* Metadata strip */}
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-zinc-500">
<span>
Contributed by{" "}
<span className="text-zinc-300 font-medium">{metadata.contributed_by}</span>
</span>
{metadata.verified && (
<span className="text-green-400 font-medium"> Verified</span>
)}
<span>CTP v{ctpDoc.version}</span>
{metadata.mbid && (
<a
href={`https://musicbrainz.org/recording/${metadata.mbid}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
MusicBrainz
</a>
)}
</div>
{/* Count-in */}
<div className="flex items-center gap-3 text-sm">
<span className="text-zinc-500">Count-in:</span>
<span className="text-zinc-300">
{count_in.enabled ? `${count_in.bars} bar${count_in.bars !== 1 ? "s" : ""}` : "Disabled"}
</span>
</div>
{/* Sections table */}
<div className="overflow-x-auto rounded-lg border border-zinc-800">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-800 text-left text-xs uppercase tracking-widest text-zinc-600">
<th className="px-4 py-3">Label</th>
<th className="px-4 py-3">Start Bar</th>
<th className="px-4 py-3">BPM</th>
<th className="px-4 py-3">Time Sig</th>
<th className="px-4 py-3">Transition</th>
</tr>
</thead>
<tbody>
{sections.map((section, i) => (
<tr
key={i}
className="border-b border-zinc-800/60 last:border-0 hover:bg-zinc-800/30"
>
<td className="px-4 py-3 font-medium text-zinc-200">
{section.label}
</td>
<td className="px-4 py-3 text-zinc-400">{section.start_bar}</td>
<td className="px-4 py-3 font-mono text-green-400">
{bpmLabel(section)}
</td>
<td className="px-4 py-3 text-zinc-400">
{timeSigLabel(section.time_signature)}
</td>
<td className="px-4 py-3">
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
section.transition === "ramp"
? "bg-amber-900/40 text-amber-400"
: "bg-zinc-800 text-zinc-400"
}`}
>
{section.transition}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{!readOnly && (
<p className="text-xs text-zinc-600">
Interactive editor coming soon contribute via the{" "}
<a
href="https://github.com/your-org/clicktrack-registry"
className="text-blue-500 hover:underline"
>
community registry
</a>{" "}
in the meantime.
</p>
)}
</div>
);
}