feat: band NC folder config, fix watcher event filter, add light/dark theme

- Add PATCH /bands/{id} endpoint for admins to update nc_folder_path
- Add band NC scan folder UI panel with inline edit
- Fix watcher: use activity type field (not human-readable subject) for upload detection
- Reorder watcher filters: audio extension check first, then band path, then type
- Add dark/light theme toggle using GitHub Primer-inspired CSS custom properties
- All inline styles migrated to CSS variables for theme-awareness

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Steffen Schuhmann
2026-03-29 00:29:58 +01:00
parent 5536bf4394
commit fbac62a0ea
13 changed files with 419 additions and 211 deletions

View File

@@ -74,8 +74,8 @@ export function SongPage() {
});
return (
<div style={{ background: "#080A0E", minHeight: "100vh", color: "#E2E6F0", padding: 24 }}>
<Link to={`/bands/${bandId}`} style={{ color: "#5A6480", fontSize: 12, textDecoration: "none", display: "inline-block", marginBottom: 16 }}>
<div style={{ background: "var(--bg)", minHeight: "100vh", color: "var(--text)", padding: 24 }}>
<Link to={`/bands/${bandId}`} style={{ color: "var(--text-muted)", fontSize: 12, textDecoration: "none", display: "inline-block", marginBottom: 16 }}>
Back to Band
</Link>
@@ -86,9 +86,10 @@ export function SongPage() {
key={v.id}
onClick={() => setSelectedVersionId(v.id)}
style={{
background: v.id === activeVersion ? "#2A1E08" : "#131720",
border: `1px solid ${v.id === activeVersion ? "#F0A840" : "#1C2235"}`,
borderRadius: 6, padding: "6px 14px", color: v.id === activeVersion ? "#F0A840" : "#5A6480",
background: v.id === activeVersion ? "var(--accent-bg)" : "var(--bg-inset)",
border: `1px solid ${v.id === activeVersion ? "var(--accent)" : "var(--border)"}`,
borderRadius: 6, padding: "6px 14px",
color: v.id === activeVersion ? "var(--accent)" : "var(--text-muted)",
cursor: "pointer", fontSize: 12, fontFamily: "monospace",
}}
>
@@ -98,21 +99,16 @@ export function SongPage() {
</div>
{/* Waveform */}
<div
style={{ background: "#0E1118", border: "1px solid #1C2235", borderRadius: 8, padding: "16px 16px 8px", marginBottom: 16 }}
onClick={(_e) => {
// TODO: seek on click (needs duration from wavesurfer)
}}
>
<div style={{ background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 8, padding: "16px 16px 8px", marginBottom: 16 }}>
<div ref={waveformRef} />
<div style={{ display: "flex", gap: 12, marginTop: 8 }}>
<button
onClick={isPlaying ? pause : play}
style={{ background: "#F0A840", border: "none", borderRadius: 6, padding: "6px 18px", cursor: "pointer", fontWeight: 600, color: "#080A0E" }}
style={{ background: "var(--accent)", border: "none", borderRadius: 6, padding: "6px 18px", cursor: "pointer", fontWeight: 600, color: "var(--accent-fg)" }}
>
{isPlaying ? "⏸ Pause" : "▶ Play"}
</button>
<span style={{ color: "#5A6480", fontSize: 12, alignSelf: "center" }}>
<span style={{ color: "var(--text-muted)", fontSize: 12, alignSelf: "center" }}>
{formatTime(currentTime)}
</span>
</div>
@@ -127,28 +123,28 @@ export function SongPage() {
{/* Comments */}
<div>
<h2 style={{ fontSize: 14, color: "#5A6480", fontFamily: "monospace", letterSpacing: 1, marginBottom: 14 }}>COMMENTS</h2>
<h2 style={{ fontSize: 14, color: "var(--text-muted)", fontFamily: "monospace", letterSpacing: 1, marginBottom: 14 }}>COMMENTS</h2>
<div style={{ display: "grid", gap: 8, marginBottom: 16 }}>
{comments?.map((c) => (
<div key={c.id} style={{ background: "#0E1118", border: "1px solid #1C2235", borderRadius: 8, padding: "12px 16px" }}>
<div key={c.id} style={{ background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 8, padding: "12px 16px" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
<span style={{ fontWeight: 600, fontSize: 13, color: "#E2E6F0" }}>{c.author_name}</span>
<span style={{ fontWeight: 600, fontSize: 13, color: "var(--text)" }}>{c.author_name}</span>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ color: "#38496A", fontSize: 11 }}>{new Date(c.created_at).toLocaleString()}</span>
<span style={{ color: "var(--text-subtle)", fontSize: 11 }}>{new Date(c.created_at).toLocaleString()}</span>
<button
onClick={() => deleteCommentMutation.mutate(c.id)}
style={{ background: "none", border: "none", color: "#38496A", cursor: "pointer", fontSize: 11, padding: 0 }}
style={{ background: "none", border: "none", color: "var(--text-subtle)", cursor: "pointer", fontSize: 11, padding: 0 }}
>
Delete
</button>
</div>
</div>
<p style={{ margin: 0, fontSize: 13, color: "#C8CDD8", lineHeight: 1.5 }}>{c.body}</p>
<p style={{ margin: 0, fontSize: 13, color: "var(--text)", lineHeight: 1.5 }}>{c.body}</p>
</div>
))}
{comments?.length === 0 && (
<p style={{ color: "#38496A", fontSize: 13 }}>No comments yet. Be the first.</p>
<p style={{ color: "var(--text-subtle)", fontSize: 13 }}>No comments yet. Be the first.</p>
)}
</div>
@@ -158,12 +154,12 @@ export function SongPage() {
onChange={(e) => setCommentBody(e.target.value)}
placeholder="Add a comment…"
rows={2}
style={{ flex: 1, padding: "8px 12px", background: "#131720", border: "1px solid #1C2235", borderRadius: 6, color: "#E2E6F0", fontSize: 13, resize: "vertical", fontFamily: "inherit" }}
style={{ flex: 1, padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, resize: "vertical", fontFamily: "inherit" }}
/>
<button
onClick={() => commentBody.trim() && addCommentMutation.mutate(commentBody.trim())}
disabled={!commentBody.trim() || addCommentMutation.isPending}
style={{ background: "#F0A840", border: "none", borderRadius: 6, color: "#080A0E", cursor: "pointer", padding: "0 18px", fontWeight: 600, fontSize: 13, alignSelf: "stretch" }}
style={{ background: "var(--accent)", border: "none", borderRadius: 6, color: "var(--accent-fg)", cursor: "pointer", padding: "0 18px", fontWeight: 600, fontSize: 13, alignSelf: "stretch" }}
>
Post
</button>
@@ -181,24 +177,24 @@ function AnnotationCard({ annotation: a, onSeek, versionId }: { annotation: Anno
});
return (
<div style={{ background: "#131720", border: "1px solid #1C2235", borderRadius: 8, padding: 14 }}>
<div style={{ background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 8, padding: 14 }}>
<div style={{ display: "flex", gap: 8, marginBottom: 6 }}>
<button
onClick={() => onSeek(a.timestamp_ms / 1000)}
style={{ background: "#2A1E08", border: "1px solid #F0A840", borderRadius: 4, color: "#F0A840", cursor: "pointer", fontSize: 10, padding: "2px 8px", fontFamily: "monospace" }}
style={{ background: "var(--accent-bg)", border: "1px solid var(--accent)", borderRadius: 4, color: "var(--accent)", cursor: "pointer", fontSize: 10, padding: "2px 8px", fontFamily: "monospace" }}
>
{formatTime(a.timestamp_ms / 1000)}
{a.range_end_ms != null && `${formatTime(a.range_end_ms / 1000)}`}
</button>
<span style={{ color: "#5A6480", fontSize: 11 }}>{a.type}</span>
{a.label && <span style={{ color: "#38C9A8", fontSize: 11 }}>{a.label}</span>}
<span style={{ color: "var(--text-muted)", fontSize: 11 }}>{a.type}</span>
{a.label && <span style={{ color: "var(--teal)", fontSize: 11 }}>{a.label}</span>}
{a.tags.map((t) => (
<span key={t} style={{ background: "#0A2820", color: "#38C9A8", fontSize: 9, padding: "1px 6px", borderRadius: 3, fontFamily: "monospace" }}>{t}</span>
<span key={t} style={{ background: "var(--teal-bg)", color: "var(--teal)", fontSize: 9, padding: "1px 6px", borderRadius: 3, fontFamily: "monospace" }}>{t}</span>
))}
</div>
{a.body && <p style={{ color: "#E2E6F0", margin: 0, fontSize: 13 }}>{a.body}</p>}
{a.body && <p style={{ color: "var(--text)", margin: 0, fontSize: 13 }}>{a.body}</p>}
{a.range_analysis && (
<div style={{ marginTop: 8, display: "flex", gap: 12, fontSize: 11, color: "#5A6480" }}>
<div style={{ marginTop: 8, display: "flex", gap: 12, fontSize: 11, color: "var(--text-muted)" }}>
{a.range_analysis.bpm && <span> {a.range_analysis.bpm.toFixed(1)} BPM</span>}
{a.range_analysis.key && <span>🎵 {a.range_analysis.key}</span>}
{a.range_analysis.avg_loudness_lufs && <span>{a.range_analysis.avg_loudness_lufs.toFixed(1)} LUFS</span>}
@@ -209,10 +205,10 @@ function AnnotationCard({ annotation: a, onSeek, versionId }: { annotation: Anno
<button
key={emoji}
onClick={() => reactionMutation.mutate(emoji)}
style={{ background: "none", border: "1px solid #1C2235", borderRadius: 4, cursor: "pointer", padding: "2px 6px", fontSize: 14 }}
style={{ background: "none", border: "1px solid var(--border)", borderRadius: 4, cursor: "pointer", padding: "2px 6px", fontSize: 14 }}
>
{emoji}{" "}
<span style={{ fontSize: 10, color: "#5A6480" }}>
<span style={{ fontSize: 10, color: "var(--text-muted)" }}>
{a.reactions.filter((r) => r.emoji === emoji).length || ""}
</span>
</button>