feat: fix song view layout - align waveform top, scrollable comments, compose section always visible
This commit is contained in:
@@ -116,20 +116,22 @@ function Avatar({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
width: size,
|
{
|
||||||
height: size,
|
width: size,
|
||||||
borderRadius: "50%",
|
height: size,
|
||||||
background: mc.bg,
|
borderRadius: "50%",
|
||||||
border: `1.5px solid ${mc.border}`,
|
background: mc.bg,
|
||||||
color: mc.text,
|
border: `1.5px solid ${mc.border}`,
|
||||||
display: "flex",
|
color: mc.text,
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
fontSize: size * 0.38,
|
justifyContent: "center",
|
||||||
fontWeight: 700,
|
fontSize: size * 0.38,
|
||||||
flexShrink: 0,
|
fontWeight: 700,
|
||||||
}}
|
flexShrink: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{getInitials(name)}
|
{getInitials(name)}
|
||||||
</div>
|
</div>
|
||||||
@@ -200,19 +202,21 @@ function WaveformPins({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={c.id}
|
key={c.id}
|
||||||
style={{
|
style={
|
||||||
position: "absolute",
|
{
|
||||||
left,
|
position: "absolute",
|
||||||
top: 0,
|
left,
|
||||||
transform: "translateX(-50%)",
|
top: 0,
|
||||||
display: "flex",
|
transform: "translateX(-50%)",
|
||||||
flexDirection: "column",
|
display: "flex",
|
||||||
alignItems: "center",
|
flexDirection: "column",
|
||||||
cursor: "pointer",
|
alignItems: "center",
|
||||||
zIndex: 10,
|
cursor: "pointer",
|
||||||
transition: "transform 0.12s",
|
zIndex: 10,
|
||||||
...(isHovered ? { transform: "translateX(-50%) scale(1.15)" } : {}),
|
transition: "transform 0.12s",
|
||||||
}}
|
...(isHovered ? { transform: "translateX(-50%) scale(1.15)" } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
onMouseEnter={() => setHoveredId(c.id)}
|
onMouseEnter={() => setHoveredId(c.id)}
|
||||||
onMouseLeave={() => setHoveredId(null)}
|
onMouseLeave={() => setHoveredId(null)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -223,34 +227,38 @@ function WaveformPins({
|
|||||||
{/* Tooltip */}
|
{/* Tooltip */}
|
||||||
{isHovered && (
|
{isHovered && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
position: "absolute",
|
{
|
||||||
bottom: "calc(100% + 6px)",
|
position: "absolute",
|
||||||
left: "50%",
|
bottom: "calc(100% + 6px)",
|
||||||
transform: "translateX(-50%)",
|
left: "50%",
|
||||||
background: "#1c1c22",
|
transform: "translateX(-50%)",
|
||||||
border: "1px solid rgba(255,255,255,0.1)",
|
background: "#1c1c22",
|
||||||
borderRadius: 8,
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
padding: "8px 10px",
|
borderRadius: 8,
|
||||||
width: 180,
|
padding: "8px 10px",
|
||||||
zIndex: 50,
|
width: 180,
|
||||||
pointerEvents: "none",
|
zIndex: 50,
|
||||||
}}
|
pointerEvents: "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
width: 18,
|
{
|
||||||
height: 18,
|
width: 18,
|
||||||
borderRadius: "50%",
|
height: 18,
|
||||||
background: mc.bg,
|
borderRadius: "50%",
|
||||||
color: mc.text,
|
background: mc.bg,
|
||||||
display: "flex",
|
color: mc.text,
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
fontSize: 8,
|
justifyContent: "center",
|
||||||
fontWeight: 700,
|
fontSize: 8,
|
||||||
}}
|
fontWeight: 700,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{getInitials(c.author_name)}
|
{getInitials(c.author_name)}
|
||||||
</div>
|
</div>
|
||||||
@@ -266,20 +274,22 @@ function WaveformPins({
|
|||||||
)}
|
)}
|
||||||
{/* Avatar circle */}
|
{/* Avatar circle */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
width: 24,
|
{
|
||||||
height: 24,
|
width: 24,
|
||||||
borderRadius: "50%",
|
height: 24,
|
||||||
background: mc.bg,
|
borderRadius: "50%",
|
||||||
border: `2px solid ${mc.border}`,
|
background: mc.bg,
|
||||||
color: mc.text,
|
border: `2px solid ${mc.border}`,
|
||||||
display: "flex",
|
color: mc.text,
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
fontSize: 9,
|
justifyContent: "center",
|
||||||
fontWeight: 700,
|
fontSize: 9,
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.45)",
|
fontWeight: 700,
|
||||||
}}
|
boxShadow: "0 2px 8px rgba(0,0,0,0.45)",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{getInitials(c.author_name)}
|
{getInitials(c.author_name)}
|
||||||
</div>
|
</div>
|
||||||
@@ -307,7 +317,7 @@ export function SongPage() {
|
|||||||
const [composeFocused, setComposeFocused] = useState(false);
|
const [composeFocused, setComposeFocused] = useState(false);
|
||||||
const [waveformWidth, setWaveformWidth] = useState(0);
|
const [waveformWidth, setWaveformWidth] = useState(0);
|
||||||
|
|
||||||
// ── Data fetching ────────────────────────────────────────────────────────
|
// ── Data fetching ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const { data: me } = useQuery({
|
const { data: me } = useQuery({
|
||||||
queryKey: ["me"],
|
queryKey: ["me"],
|
||||||
@@ -338,7 +348,7 @@ export function SongPage() {
|
|||||||
enabled: !!songId,
|
enabled: !!songId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Version selection ────────────────────────────────────────────────────
|
// ── Version selection ──────────────────────────────────────────────────
|
||||||
|
|
||||||
const activeVersion = selectedVersionId ?? versions?.[0]?.id ?? null;
|
const activeVersion = selectedVersionId ?? versions?.[0]?.id ?? null;
|
||||||
|
|
||||||
@@ -361,7 +371,7 @@ export function SongPage() {
|
|||||||
return () => ro.disconnect();
|
return () => ro.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── Keyboard shortcut: Space ──────────────────────────────────────────────
|
// ── Keyboard shortcut: Space ────────────────────────────────────────────
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -401,26 +411,27 @@ export function SongPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// ── Styles ──────────────────────────────────────────────────────────────
|
||||||
// ── Styles ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const border = "rgba(255,255,255,0.055)";
|
const border = "rgba(255,255,255,0.055)";
|
||||||
|
|
||||||
// ── Render ────────────────────────────────────────────────────────────────
|
// ── Render ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden", background: "#0f0f12" }}>
|
<div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden", background: "#0f0f12" }}>
|
||||||
|
|
||||||
{/* ── Breadcrumb header ──────────────────────────────────────────── */}
|
{/* ── Breadcrumb header ──────────────────────────────────────────── */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
padding: "11px 20px",
|
{
|
||||||
borderBottom: `1px solid ${border}`,
|
padding: "11px 20px",
|
||||||
display: "flex",
|
borderBottom: `1px solid ${border}`,
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
gap: 8,
|
alignItems: "center",
|
||||||
flexShrink: 0,
|
gap: 8,
|
||||||
}}
|
flexShrink: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 5, flex: 1, minWidth: 0 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 5, flex: 1, minWidth: 0 }}>
|
||||||
<button
|
<button
|
||||||
@@ -446,14 +457,16 @@ export function SongPage() {
|
|||||||
)}
|
)}
|
||||||
<span style={{ color: "rgba(255,255,255,0.15)", fontSize: 11 }}>›</span>
|
<span style={{ color: "rgba(255,255,255,0.15)", fontSize: 11 }}>›</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={
|
||||||
fontSize: 12,
|
{
|
||||||
color: "rgba(255,255,255,0.7)",
|
fontSize: 12,
|
||||||
fontFamily: "'SF Mono', 'Fira Code', monospace",
|
color: "rgba(255,255,255,0.7)",
|
||||||
overflow: "hidden",
|
fontFamily: "'SF Mono', 'Fira Code', monospace",
|
||||||
textOverflow: "ellipsis",
|
overflow: "hidden",
|
||||||
whiteSpace: "nowrap",
|
textOverflow: "ellipsis",
|
||||||
}}
|
whiteSpace: "nowrap",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{song?.title ?? "…"}
|
{song?.title ?? "…"}
|
||||||
</span>
|
</span>
|
||||||
@@ -466,16 +479,18 @@ export function SongPage() {
|
|||||||
<button
|
<button
|
||||||
key={v.id}
|
key={v.id}
|
||||||
onClick={() => setSelectedVersionId(v.id)}
|
onClick={() => setSelectedVersionId(v.id)}
|
||||||
style={{
|
style={
|
||||||
background: v.id === activeVersion ? "rgba(232,162,42,0.14)" : "transparent",
|
{
|
||||||
border: `1px solid ${v.id === activeVersion ? "rgba(232,162,42,0.28)" : "rgba(255,255,255,0.09)"}`,
|
background: v.id === activeVersion ? "rgba(232,162,42,0.14)" : "transparent",
|
||||||
borderRadius: 6,
|
border: `1px solid ${v.id === activeVersion ? "rgba(232,162,42,0.28)" : "rgba(255,255,255,0.09)"}`,
|
||||||
padding: "4px 10px",
|
borderRadius: 6,
|
||||||
color: v.id === activeVersion ? "#e8a22a" : "rgba(255,255,255,0.38)",
|
padding: "4px 10px",
|
||||||
cursor: "pointer",
|
color: v.id === activeVersion ? "#e8a22a" : "rgba(255,255,255,0.38)",
|
||||||
fontSize: 11,
|
cursor: "pointer",
|
||||||
fontFamily: "monospace",
|
fontSize: 11,
|
||||||
}}
|
fontFamily: "monospace",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
v{v.version_number}{v.label ? ` · ${v.label}` : ""}
|
v{v.version_number}{v.label ? ` · ${v.label}` : ""}
|
||||||
</button>
|
</button>
|
||||||
@@ -484,38 +499,42 @@ export function SongPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
style={{
|
style={
|
||||||
background: "transparent",
|
{
|
||||||
border: "1px solid rgba(255,255,255,0.09)",
|
background: "transparent",
|
||||||
borderRadius: 6,
|
border: "1px solid rgba(255,255,255,0.09)",
|
||||||
color: "rgba(255,255,255,0.38)",
|
borderRadius: 6,
|
||||||
cursor: "pointer",
|
color: "rgba(255,255,255,0.38)",
|
||||||
fontSize: 12,
|
cursor: "pointer",
|
||||||
padding: "5px 12px",
|
fontSize: 12,
|
||||||
fontFamily: "inherit",
|
padding: "5px 12px",
|
||||||
flexShrink: 0,
|
fontFamily: "inherit",
|
||||||
}}
|
flexShrink: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Share
|
Share
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Body: waveform | comments ────────────────────────────────── */}
|
{/* ── Body: waveform | comments ────────────────────────────────── */}
|
||||||
<div className="song-page-body" style={{ display: "flex", flex: 1, overflow: "hidden" }}>
|
<div className="song-page-body" style={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden" }}>
|
||||||
|
|
||||||
{/* ── Left: waveform + transport ──────────────────────────────── */}
|
{/* ── Waveform section (top) ──────────────────────────────── */}
|
||||||
<div className="waveform-section" style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", padding: "16px 20px" }}>
|
<div className="waveform-section" style={{ display: "flex", flexDirection: "column", overflow: "hidden", padding: "16px 20px" }}>
|
||||||
|
|
||||||
{/* Waveform card */}
|
{/* Waveform card */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
background: "rgba(255,255,255,0.02)",
|
{
|
||||||
border: `1px solid ${border}`,
|
background: "rgba(255,255,255,0.02)",
|
||||||
borderRadius: 10,
|
border: `1px solid ${border}`,
|
||||||
padding: "14px 14px 10px",
|
borderRadius: 10,
|
||||||
marginBottom: 12,
|
padding: "14px 14px 10px",
|
||||||
flexShrink: 0,
|
marginBottom: 12,
|
||||||
}}
|
flexShrink: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 }}>
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 }}>
|
||||||
<span style={{ fontSize: 11, color: "rgba(255,255,255,0.45)", fontFamily: "monospace" }}>
|
<span style={{ fontSize: 11, color: "rgba(255,255,255,0.45)", fontFamily: "monospace" }}>
|
||||||
@@ -559,13 +578,15 @@ export function SongPage() {
|
|||||||
|
|
||||||
{/* Transport */}
|
{/* Transport */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
display: "flex",
|
{
|
||||||
justifyContent: "center",
|
display: "flex",
|
||||||
gap: 10,
|
justifyContent: "center",
|
||||||
padding: "4px 0 12px",
|
gap: 10,
|
||||||
flexShrink: 0,
|
padding: "4px 0 12px",
|
||||||
}}
|
flexShrink: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Skip back */}
|
{/* Skip back */}
|
||||||
<TransportButton onClick={() => seekTo(Math.max(0, currentTime - 30))} title="−30s">
|
<TransportButton onClick={() => seekTo(Math.max(0, currentTime - 30))} title="−30s">
|
||||||
@@ -576,20 +597,22 @@ export function SongPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={isPlaying ? pause : play}
|
onClick={isPlaying ? pause : play}
|
||||||
disabled={!activeVersion}
|
disabled={!activeVersion}
|
||||||
style={{
|
style={
|
||||||
width: 46,
|
{
|
||||||
height: 46,
|
width: 46,
|
||||||
background: "#e8a22a",
|
height: 46,
|
||||||
borderRadius: "50%",
|
background: "#e8a22a",
|
||||||
border: "none",
|
borderRadius: "50%",
|
||||||
display: "flex",
|
border: "none",
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
cursor: activeVersion ? "pointer" : "default",
|
justifyContent: "center",
|
||||||
opacity: activeVersion ? 1 : 0.4,
|
cursor: activeVersion ? "pointer" : "default",
|
||||||
flexShrink: 0,
|
opacity: activeVersion ? 1 : 0.4,
|
||||||
transition: "background 0.15s, transform 0.15s",
|
flexShrink: 0,
|
||||||
}}
|
transition: "background 0.15s, transform 0.15s",
|
||||||
|
}
|
||||||
|
}
|
||||||
onMouseEnter={(e) => { if (activeVersion) e.currentTarget.style.background = "#f0b740"; }}
|
onMouseEnter={(e) => { if (activeVersion) e.currentTarget.style.background = "#f0b740"; }}
|
||||||
onMouseLeave={(e) => { e.currentTarget.style.background = "#e8a22a"; }}
|
onMouseLeave={(e) => { e.currentTarget.style.background = "#e8a22a"; }}
|
||||||
>
|
>
|
||||||
@@ -606,49 +629,207 @@ export function SongPage() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Right: comment panel ──────────────────────────────────────── */}
|
{/* ── Comments section (bottom) ──────────────────────────────── */}
|
||||||
<div
|
<div
|
||||||
className="comment-panel"
|
className="comment-panel"
|
||||||
style={{
|
style={
|
||||||
width: 280,
|
{
|
||||||
minWidth: 280,
|
display: "flex",
|
||||||
borderLeft: `1px solid ${border}`,
|
flexDirection: "column",
|
||||||
display: "flex",
|
overflow: "hidden",
|
||||||
flexDirection: "column",
|
background: "rgba(0,0,0,0.12)",
|
||||||
overflow: "hidden",
|
borderTop: `1px solid ${border}`,
|
||||||
background: "rgba(0,0,0,0.12)",
|
flex: 1,
|
||||||
// Responsive: center on mobile
|
}
|
||||||
margin: "0 auto",
|
}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
padding: "12px 15px",
|
{
|
||||||
borderBottom: `1px solid ${border}`,
|
padding: "12px 15px",
|
||||||
display: "flex",
|
borderBottom: `1px solid ${border}`,
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
alignItems: "center",
|
||||||
flexShrink: 0,
|
justifyContent: "space-between",
|
||||||
}}
|
flexShrink: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 13, fontWeight: 500, color: "rgba(255,255,255,0.72)" }}>Comments</span>
|
<span style={{ fontSize: 13, fontWeight: 500, color: "rgba(255,255,255,0.72)" }}>Comments</span>
|
||||||
{comments && comments.length > 0 && (
|
{comments && comments.length > 0 && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={
|
||||||
fontSize: 11,
|
{
|
||||||
background: "rgba(232,162,42,0.14)",
|
fontSize: 11,
|
||||||
color: "#e8a22a",
|
background: "rgba(232,162,42,0.14)",
|
||||||
padding: "1px 8px",
|
color: "#e8a22a",
|
||||||
borderRadius: 10,
|
padding: "1px 8px",
|
||||||
}}
|
borderRadius: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{comments.length}
|
{comments.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comment list */}
|
{/* Compose section (moved to top) */}
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
padding: "11px 14px",
|
||||||
|
borderBottom: `1px solid ${border}`,
|
||||||
|
flexShrink: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", gap: 9, alignItems: "flex-start" }}>
|
||||||
|
{/* My avatar */}
|
||||||
|
{me ? (
|
||||||
|
<Avatar name={me.display_name} avatarUrl={me.avatar_url ?? null} authorId={me.id} size={26} />
|
||||||
|
) : (
|
||||||
|
<div style={{ width: 26, height: 26, borderRadius: "50%", background: "rgba(255,255,255,0.06)", flexShrink: 0 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{/* Timestamp pill */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 6 }}>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
background: "rgba(232,162,42,0.1)",
|
||||||
|
color: "#e8a22a",
|
||||||
|
border: "1px solid rgba(232,162,42,0.22)",
|
||||||
|
padding: "3px 9px",
|
||||||
|
borderRadius: 20,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isPlaying && (
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#e8a22a",
|
||||||
|
animation: "rh-blink 1.1s infinite",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{formatTime(currentTime)}
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: 11, color: "rgba(255,255,255,0.18)" }}>· pins to playhead</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Textarea */}
|
||||||
|
<textarea
|
||||||
|
value={commentBody}
|
||||||
|
onChange={(e) => setCommentBody(e.target.value)}
|
||||||
|
onFocus={() => setComposeFocused(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!commentBody.trim()) setComposeFocused(false);
|
||||||
|
}}
|
||||||
|
placeholder="What do you hear at this moment…"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
width: "100%",
|
||||||
|
background: "rgba(255,255,255,0.05)",
|
||||||
|
border: composeFocused
|
||||||
|
? "1px solid rgba(232,162,42,0.35)"
|
||||||
|
: "1px solid rgba(255,255,255,0.07)",
|
||||||
|
borderRadius: 7,
|
||||||
|
padding: "8px 10px",
|
||||||
|
color: "#e0e0e8",
|
||||||
|
fontSize: 12,
|
||||||
|
resize: "none",
|
||||||
|
outline: "none",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
height: composeFocused ? 68 : 42,
|
||||||
|
transition: "height 0.18s, border-color 0.15s",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tag buttons + Post (visible when focused) */}
|
||||||
|
{composeFocused && (
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 7 }}>
|
||||||
|
<div style={{ display: "flex", gap: 5 }}>
|
||||||
|
{(["suggestion", "issue", "keeper"] as const).map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
onClick={() => setSelectedTag((t) => (t === tag ? "" : tag))}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
fontSize: 11,
|
||||||
|
padding: "3px 8px",
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: "pointer",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
background:
|
||||||
|
selectedTag === tag
|
||||||
|
? TAG_STYLES[tag].bg
|
||||||
|
: "rgba(255,255,255,0.05)",
|
||||||
|
border:
|
||||||
|
selectedTag === tag
|
||||||
|
? `1px solid ${TAG_STYLES[tag].color}44`
|
||||||
|
: "1px solid rgba(255,255,255,0.07)",
|
||||||
|
color:
|
||||||
|
selectedTag === tag
|
||||||
|
? TAG_STYLES[tag].color
|
||||||
|
: "rgba(255,255,255,0.32)",
|
||||||
|
transition: "all 0.12s",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (commentBody.trim()) {
|
||||||
|
addCommentMutation.mutate({
|
||||||
|
body: commentBody.trim(),
|
||||||
|
timestamp: currentTime,
|
||||||
|
tag: selectedTag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!commentBody.trim() || addCommentMutation.isPending}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
padding: "5px 14px",
|
||||||
|
borderRadius: 6,
|
||||||
|
background: "#e8a22a",
|
||||||
|
border: "none",
|
||||||
|
color: "#0f0f12",
|
||||||
|
cursor: commentBody.trim() ? "pointer" : "default",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
opacity: commentBody.trim() ? 1 : 0.35,
|
||||||
|
transition: "opacity 0.12s",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Post
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable comment list */}
|
||||||
<div style={{ flex: 1, overflowY: "auto", padding: "12px 14px" }}>
|
<div style={{ flex: 1, overflowY: "auto", padding: "12px 14px" }}>
|
||||||
{comments?.map((c) => {
|
{comments?.map((c) => {
|
||||||
const tagStyle = c.tag ? TAG_STYLES[c.tag] : null;
|
const tagStyle = c.tag ? TAG_STYLES[c.tag] : null;
|
||||||
@@ -658,15 +839,17 @@ export function SongPage() {
|
|||||||
<div
|
<div
|
||||||
key={c.id}
|
key={c.id}
|
||||||
id={`comment-${c.id}`}
|
id={`comment-${c.id}`}
|
||||||
style={{
|
style={
|
||||||
marginBottom: 14,
|
{
|
||||||
paddingBottom: 14,
|
marginBottom: 14,
|
||||||
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
paddingBottom: 14,
|
||||||
borderRadius: isNearPlayhead ? 6 : undefined,
|
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
||||||
background: isNearPlayhead ? "rgba(232,162,42,0.04)" : undefined,
|
borderRadius: isNearPlayhead ? 6 : undefined,
|
||||||
border: isNearPlayhead ? "1px solid rgba(232,162,42,0.12)" : undefined,
|
background: isNearPlayhead ? "rgba(232,162,42,0.04)" : undefined,
|
||||||
padding: isNearPlayhead ? 8 : undefined,
|
border: isNearPlayhead ? "1px solid rgba(232,162,42,0.12)" : undefined,
|
||||||
}}
|
padding: isNearPlayhead ? 8 : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Author row */}
|
{/* Author row */}
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 5 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 5 }}>
|
||||||
@@ -679,17 +862,19 @@ export function SongPage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
seekTo(c.timestamp!);
|
seekTo(c.timestamp!);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={
|
||||||
marginLeft: "auto",
|
{
|
||||||
fontSize: 10,
|
marginLeft: "auto",
|
||||||
fontFamily: "monospace",
|
fontSize: 10,
|
||||||
color: "#e8a22a",
|
fontFamily: "monospace",
|
||||||
background: "rgba(232,162,42,0.1)",
|
color: "#e8a22a",
|
||||||
border: "none",
|
background: "rgba(232,162,42,0.1)",
|
||||||
borderRadius: 3,
|
border: "none",
|
||||||
padding: "1px 5px",
|
borderRadius: 3,
|
||||||
cursor: "pointer",
|
padding: "1px 5px",
|
||||||
}}
|
cursor: "pointer",
|
||||||
|
}
|
||||||
|
}
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(232,162,42,0.2)")}
|
onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(232,162,42,0.2)")}
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.background = "rgba(232,162,42,0.1)")}
|
onMouseLeave={(e) => (e.currentTarget.style.background = "rgba(232,162,42,0.1)")}
|
||||||
>
|
>
|
||||||
@@ -698,13 +883,15 @@ export function SongPage() {
|
|||||||
)}
|
)}
|
||||||
{tagStyle && (
|
{tagStyle && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={
|
||||||
fontSize: 10,
|
{
|
||||||
padding: "1px 5px",
|
fontSize: 10,
|
||||||
borderRadius: 3,
|
padding: "1px 5px",
|
||||||
background: tagStyle.bg,
|
borderRadius: 3,
|
||||||
color: tagStyle.color,
|
background: tagStyle.bg,
|
||||||
}}
|
color: tagStyle.color,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{c.tag}
|
{c.tag}
|
||||||
</span>
|
</span>
|
||||||
@@ -744,148 +931,6 @@ export function SongPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compose */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "11px 14px",
|
|
||||||
borderTop: `1px solid ${border}`,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", gap: 9, alignItems: "flex-start" }}>
|
|
||||||
{/* My avatar */}
|
|
||||||
{me ? (
|
|
||||||
<Avatar name={me.display_name} avatarUrl={me.avatar_url ?? null} authorId={me.id} size={26} />
|
|
||||||
) : (
|
|
||||||
<div style={{ width: 26, height: 26, borderRadius: "50%", background: "rgba(255,255,255,0.06)", flexShrink: 0 }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
{/* Timestamp pill */}
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 6 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 11,
|
|
||||||
fontFamily: "monospace",
|
|
||||||
background: "rgba(232,162,42,0.1)",
|
|
||||||
color: "#e8a22a",
|
|
||||||
border: "1px solid rgba(232,162,42,0.22)",
|
|
||||||
padding: "3px 9px",
|
|
||||||
borderRadius: 20,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPlaying && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: "50%",
|
|
||||||
background: "#e8a22a",
|
|
||||||
animation: "rh-blink 1.1s infinite",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{formatTime(currentTime)}
|
|
||||||
</div>
|
|
||||||
<span style={{ fontSize: 11, color: "rgba(255,255,255,0.18)" }}>· pins to playhead</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Textarea */}
|
|
||||||
<textarea
|
|
||||||
value={commentBody}
|
|
||||||
onChange={(e) => setCommentBody(e.target.value)}
|
|
||||||
onFocus={() => setComposeFocused(true)}
|
|
||||||
onBlur={() => {
|
|
||||||
if (!commentBody.trim()) setComposeFocused(false);
|
|
||||||
}}
|
|
||||||
placeholder="What do you hear at this moment…"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
background: "rgba(255,255,255,0.05)",
|
|
||||||
border: composeFocused
|
|
||||||
? "1px solid rgba(232,162,42,0.35)"
|
|
||||||
: "1px solid rgba(255,255,255,0.07)",
|
|
||||||
borderRadius: 7,
|
|
||||||
padding: "8px 10px",
|
|
||||||
color: "#e0e0e8",
|
|
||||||
fontSize: 12,
|
|
||||||
resize: "none",
|
|
||||||
outline: "none",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
height: composeFocused ? 68 : 42,
|
|
||||||
transition: "height 0.18s, border-color 0.15s",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Tag buttons + Post (visible when focused) */}
|
|
||||||
{composeFocused && (
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 7 }}>
|
|
||||||
<div style={{ display: "flex", gap: 5 }}>
|
|
||||||
{(["suggestion", "issue", "keeper"] as const).map((tag) => (
|
|
||||||
<button
|
|
||||||
key={tag}
|
|
||||||
onClick={() => setSelectedTag((t) => (t === tag ? "" : tag))}
|
|
||||||
style={{
|
|
||||||
fontSize: 11,
|
|
||||||
padding: "3px 8px",
|
|
||||||
borderRadius: 4,
|
|
||||||
cursor: "pointer",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
background:
|
|
||||||
selectedTag === tag
|
|
||||||
? TAG_STYLES[tag].bg
|
|
||||||
: "rgba(255,255,255,0.05)",
|
|
||||||
border:
|
|
||||||
selectedTag === tag
|
|
||||||
? `1px solid ${TAG_STYLES[tag].color}44`
|
|
||||||
: "1px solid rgba(255,255,255,0.07)",
|
|
||||||
color:
|
|
||||||
selectedTag === tag
|
|
||||||
? TAG_STYLES[tag].color
|
|
||||||
: "rgba(255,255,255,0.32)",
|
|
||||||
transition: "all 0.12s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (commentBody.trim()) {
|
|
||||||
addCommentMutation.mutate({
|
|
||||||
body: commentBody.trim(),
|
|
||||||
timestamp: currentTime,
|
|
||||||
tag: selectedTag,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!commentBody.trim() || addCommentMutation.isPending}
|
|
||||||
style={{
|
|
||||||
padding: "5px 14px",
|
|
||||||
borderRadius: 6,
|
|
||||||
background: "#e8a22a",
|
|
||||||
border: "none",
|
|
||||||
color: "#0f0f12",
|
|
||||||
cursor: commentBody.trim() ? "pointer" : "default",
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
fontFamily: "inherit",
|
|
||||||
opacity: commentBody.trim() ? 1 : 0.35,
|
|
||||||
transition: "opacity 0.12s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Post
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -928,24 +973,24 @@ function TransportButton({ onClick, title, children }: { onClick: () => void; ti
|
|||||||
title={title}
|
title={title}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
style={{
|
style={
|
||||||
width: 34,
|
{
|
||||||
height: 34,
|
width: 34,
|
||||||
borderRadius: "50%",
|
height: 34,
|
||||||
background: hovered ? "rgba(255,255,255,0.08)" : "rgba(255,255,255,0.04)",
|
borderRadius: "50%",
|
||||||
border: "1px solid rgba(255,255,255,0.07)",
|
background: hovered ? "rgba(255,255,255,0.08)" : "rgba(255,255,255,0.04)",
|
||||||
display: "flex",
|
border: "1px solid rgba(255,255,255,0.07)",
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
cursor: "pointer",
|
justifyContent: "center",
|
||||||
color: hovered ? "rgba(255,255,255,0.7)" : "rgba(255,255,255,0.35)",
|
cursor: "pointer",
|
||||||
flexShrink: 0,
|
color: hovered ? "rgba(255,255,255,0.7)" : "rgba(255,255,255,0.35)",
|
||||||
transition: "all 0.12s",
|
flexShrink: 0,
|
||||||
}}
|
transition: "all 0.12s",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user