1import { r as reactExports, j as jsxRuntimeExports, R as React } from "./vendor-react-Bay9Y7Lb.js"; 2import { m as getOptimizedImageUrl, a as generateTrackUrl } from "./utils-BYCVoSTN.js"; 3import { u as usePlayer, a as useAdmin } from "./main-CznOH3wZ.js"; 4import { g as getBilingualTitle } from "./bilingual-B8ftIVs5.js"; 5import { a as api } from "./index-D0Bmh-UX.js"; 6import { u as useTranslation } from "./vendor-i18n-R-bMoYp_.js"; 7import { cI as Flag, aW as X, aY as TriangleAlert, bZ as Pause, bU as Play, a_ as CircleCheck, cj as Layers, ce as Plus, b2 as Upload } from "./vendor-dVNaLJx_.js"; 8const ReportBoundMidiModal = ({ 9 isOpen, 10 onClose, 11 songId, 12 songTitle, 13 track, 14 currentUser, 15 onReportSubmitted 16}) => { 17 var _a; 18 const { t } = useTranslation(); 19 const [reason, setReason] = reactExports.useState("wrong_song"); 20 const [details, setDetails] = reactExports.useState(""); 21 const [isSubmitting, setIsSubmitting] = reactExports.useState(false); 22 const [isSuccess, setIsSuccess] = reactExports.useState(false); 23 const [error, setError] = reactExports.useState(null); 24 const { activeTrack, isPlaying, setIsPlaying, playTrack } = usePlayer(); 25 const isPlayingThisTrack = (activeTrack == null ? void 0 : activeTrack.id) === (track == null ? void 0 : track.id) && isPlaying; 26 if (!isOpen || !track) return null; 27 const handlePlayPreview = () => { 28 if (isPlayingThisTrack) { 29 setIsPlaying(false); 30 } else { 31 playTrack(track, [track]); 32 } 33 }; 34 const handleSubmit = async (e) => { 35 e.preventDefault(); 36 if (!currentUser) { 37 setError(t("report.loginRequired", "You must be logged in to file a report.")); 38 return; 39 } 40 if (!reason) { 41 setError(t("report.selectReason", "Please select a reason for reporting.")); 42 return; 43 } 44 setIsSubmitting(true); 45 setError(null); 46 try { 47 await api.post("/reports", { 48 targetId: songId, 49 targetType: "song_midi_binding", 50 midiId: track.id, 51 reason, 52 details: details.trim() || void 0, 53 reporterId: currentUser.id 54 }); 55 setIsSuccess(true); 56 onReportSubmitted == null ? void 0 : onReportSubmitted(); 57 } catch (err) { 58 setError(err.message || "Failed to submit report. Please try again."); 59 } finally { 60 setIsSubmitting(false); 61 } 62 }; 63 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md animate-in fade-in duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-3xl shadow-2xl w-full max-w-lg overflow-hidden border border-zinc-200/50 dark:border-zinc-800/50 animate-in zoom-in-95 duration-200", children: [ 64 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-5 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50", children: [
65 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white flex items-center gap-2.5", children: [ 66 /* @__PURE__ */ jsxRuntimeExports.jsx(Flag, { size: 20, className: "text-red-500 animate-pulse" }), 67 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("report.boundMidiTitle", "Report Irrelevant MIDI Binding") }) 68 ] }), 69 /* @__PURE__ */ jsxRuntimeExports.jsx( 70 "button", 71 { 72 onClick: onClose, 73 className: "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors focus:outline-none", 74 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) 75 } 76 ) 77 ] }), 78 !isSuccess ? /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleSubmit, className: "p-6 space-y-5", children: [ 79 error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200/50 dark:border-red-800/30 rounded-2xl text-sm flex gap-2", children: [
80 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 18, className: "shrink-0 mt-0.5" }), 81 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: error }) 82 ] }), 83 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 p-4 rounded-2xl border border-zinc-200/70 dark:border-zinc-800/70 space-y-3", children: [ 84 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between", children: [ 85 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 86 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] font-black uppercase tracking-wider text-zinc-400 dark:text-zinc-500", children: "Collection Song" }), 87 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-bold text-zinc-900 dark:text-white truncate max-w-[280px]", children: songTitle }) 88 ] }), 89 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] px-2 py-0.5 rounded font-black uppercase tracking-wider bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-500/20", children: [ 90 "Bound Track #", 91 track.id 92 ] }) 93 ] }),
94 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 pt-2 border-t border-zinc-200/50 dark:border-zinc-800/50", children: [ 95 /* @__PURE__ */ jsxRuntimeExports.jsx( 96 "button", 97 { 98 type: "button", 99 onClick: handlePlayPreview, 100 className: `w-9 h-9 rounded-full flex items-center justify-center shrink-0 transition-colors shadow-sm ${isPlayingThisTrack ? "bg-indigo-600 text-white animate-pulse" : "bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-200"}`, 101 children: isPlayingThisTrack ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 16, className: "fill-current" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 16, className: "fill-current ml-0.5" }) 102 } 103 ), 104 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [ 105 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-bold text-zinc-800 dark:text-zinc-200 truncate", children: track.title }),
106 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[11px] text-zinc-500 dark:text-zinc-400 truncate", children: [ 107 track.artist || ((_a = track.uploader) == null ? void 0 : _a.username) || "Hamie Library", 108 " ", 109 track.duration && track.duration !== "0:00" ? `· ${track.duration}` : track.duration_str && track.duration_str !== "0:00" ? `· ${track.duration_str}` : "" 110 ] }) 111 ] }) 112 ] }) 113 ] }), 114 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 115 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-black text-zinc-400 dark:text-zinc-500 uppercase tracking-widest", children: t("report.selectReasonLabel", "Why is this MIDI incorrect?") }), 116 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 117 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3 rounded-xl border cursor-pointer transition-all ${reason === "wrong_song" ? "bg-indigo-50/50 dark:bg-indigo-950/20 border-indigo-500 text-indigo-950 dark:text-indigo-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
118 /* @__PURE__ */ jsxRuntimeExports.jsx( 119 "input", 120 { 121 type: "radio", 122 name: "boundMidiReason", 123 value: "wrong_song", 124 checked: reason === "wrong_song", 125 onChange: () => setReason("wrong_song"), 126 className: "mt-0.5 accent-indigo-600" 127 } 128 ), 129 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs", children: [ 130 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold", children: t("report.reasonWrongSong", "Wrong or Unrelated Song") }), 131 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 mt-0.5", children: t("report.reasonWrongSongDesc", "This MIDI file is an entirely different piece or melody from the collection song.") }) 132 ] }) 133 ] }), 134 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3 rounded-xl border cursor-pointer transition-all ${reason === "poor_quality" ? "bg-indigo-50/50 dark:bg-indigo-950/20 border-indigo-500 text-indigo-950 dark:text-indigo-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
135 /* @__PURE__ */ jsxRuntimeExports.jsx( 136 "input", 137 { 138 type: "radio", 139 name: "boundMidiReason", 140 value: "poor_quality", 141 checked: reason === "poor_quality", 142 onChange: () => setReason("poor_quality"), 143 className: "mt-0.5 accent-indigo-600" 144 } 145 ), 146 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs", children: [ 147 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold", children: t("report.reasonPoorQuality", "Corrupted / Inaccurate Transcription") }), 148 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 mt-0.5", children: t("report.reasonPoorQualityDesc", "The MIDI is severely out of key, corrupted, or contains unusable chaotic notes.") }) 149 ] }) 150 ] }), 151 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3 rounded-xl border cursor-pointer transition-all ${reason === "incomplete" ? "bg-indigo-50/50 dark:bg-indigo-950/20 border-indigo-500 text-indigo-950 dark:text-indigo-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
152 /* @__PURE__ */ jsxRuntimeExports.jsx( 153 "input", 154 { 155 type: "radio", 156 name: "boundMidiReason", 157 value: "incomplete", 158 checked: reason === "incomplete", 159 onChange: () => setReason("incomplete"), 160 className: "mt-0.5 accent-indigo-600" 161 } 162 ), 163 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs", children: [ 164 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold", children: t("report.reasonIncomplete", "Incomplete / Missing Arrangement") }), 165 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 mt-0.5", children: t("report.reasonIncompleteDesc", "Only a few seconds long, missing main melody, or cut off prematurely.") }) 166 ] }) 167 ] }), 168 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3 rounded-xl border cursor-pointer transition-all ${reason === "other" ? "bg-indigo-50/50 dark:bg-indigo-950/20 border-indigo-500 text-indigo-950 dark:text-indigo-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
169 /* @__PURE__ */ jsxRuntimeExports.jsx( 170 "input", 171 { 172 type: "radio", 173 name: "boundMidiReason", 174 value: "other", 175 checked: reason === "other", 176 onChange: () => setReason("other"), 177 className: "mt-0.5 accent-indigo-600" 178 } 179 ), 180 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold", children: t("report.reasonOther", "Other Discrepancy") }) }) 181 ] }) 182 ] }) 183 ] }), 184 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 185 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-black text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5", children: t("report.additionalDetails", "Additional Details (Optional)") }), 186 /* @__PURE__ */ jsxRuntimeExports.jsx( 187 "textarea", 188 { 189 value: details, 190 onChange: (e) => setDetails(e.target.value), 191 placeholder: t("report.boundDetailsPlaceholder", "Explain why this MIDI is not the correct match..."), 192 rows: 2, 193 className: "w-full text-xs p-3 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 text-zinc-900 dark:text-white focus:outline-none focus:border-indigo-500 resize-none" 194 } 195 ) 196 ] }),
197 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 pt-2", children: [ 198 /* @__PURE__ */ jsxRuntimeExports.jsx( 199 "button", 200 { 201 type: "button", 202 onClick: onClose, 203 className: "flex-1 py-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 text-xs font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition", 204 children: t("common.cancel", "Cancel") 205 } 206 ), 207 /* @__PURE__ */ jsxRuntimeExports.jsxs( 208 "button", 209 { 210 type: "submit", 211 disabled: isSubmitting || !reason, 212 className: "flex-1 py-2.5 rounded-xl bg-red-600 hover:bg-red-700 text-white text-xs font-bold shadow-sm transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2", 213 children: [ 214 /* @__PURE__ */ jsxRuntimeExports.jsx(Flag, { size: 14 }), 215 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: isSubmitting ? t("report.submitting", "Submitting...") : t("report.submitButton", "Submit Report") }) 216 ] 217 } 218 ) 219 ] }) 220 ] }) : ( 221 /* Success State */
222 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-8 text-center space-y-4", children: [ 223 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 rounded-full flex items-center justify-center mx-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 24 }) }), 224 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 225 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-base font-bold text-zinc-900 dark:text-white", children: t("report.thankYou", "Report Submitted") }), 226 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-1 max-w-sm mx-auto", children: t("report.boundSuccessDesc", "Thank you for helping keep HamieNET collections accurate! Your report has been recorded and submitted to the Hamie Tribunal moderation system.") }) 227 ] }), 228 /* @__PURE__ */ jsxRuntimeExports.jsx( 229 "button", 230 { 231 onClick: onClose, 232 className: "px-6 py-2 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-xl text-xs font-bold transition hover:opacity-90", 233 children: t("common.close", "Close") 234 } 235 ) 236 ] }) 237 ) 238 ] }) }); 239}; 240const SongItemRow = ({ 241 song, 242 allContextTracks, 243 activeTrack, 244 isPlaying, 245 setIsPlaying, 246 playTrack, 247 setPlaylistLabel, 248 playlistTitle, 249 showUploadButton, 250 onUploadClick, 251 onReportClick, 252 i18n 253}) => { 254 const { t } = useTranslation(); 255 const hasTracks = song.tracks && song.tracks.length > 0; 256 const { mainTitle: rawSongTitle, subTitle: songSubTitle, hoverTooltip: songTooltip, nativeTitle: songNativeTitle, nativeBadge: songNativeBadge } = getBilingualTitle(song.title, song.details, i18n.language); 257 const songMainTitle = rawSongTitle.replace(/^\d+\.\s*/, ""); 258 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50/60 dark:bg-zinc-900/40 border border-zinc-100 dark:border-zinc-800/80 rounded-xl p-2.5 mb-2 hover:border-zinc-200 dark:hover:border-zinc-700/60 transition-all group/song", children: [ 259 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2 mb-1 px-1", children: [
260 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", title: songTooltip || songMainTitle, children: [ 261 songNativeBadge && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black uppercase tracking-wider bg-zinc-200/80 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 px-1 py-0.5 rounded shrink-0 select-none", title: `Native Script: ${songNativeBadge}`, children: songNativeBadge }), 262 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-900 dark:text-zinc-100 truncate", children: songMainTitle }), 263 songNativeTitle && songNativeTitle !== songMainTitle && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[11px] text-zinc-500 dark:text-zinc-400 truncate hidden sm:inline font-sans", children: [ 264 "· ", 265 songNativeTitle 266 ] }), 267 songSubTitle && songSubTitle !== songNativeTitle && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[11px] text-zinc-400 dark:text-zinc-500 truncate hidden sm:inline", children: [ 268 "· ", 269 songSubTitle 270 ] }),
271 !hasTracks && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-black uppercase tracking-wider bg-amber-500/15 text-amber-600 dark:text-amber-400 px-1.5 py-0.5 rounded border border-amber-500/30 shrink-0", children: "Unbound" }), 272 hasTracks && song.tracks.length > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 px-1.5 py-0.5 rounded border border-indigo-500/20 shrink-0", children: [ 273 song.tracks.length, 274 " versions" 275 ] }) 276 ] }), 277 showUploadButton && /* @__PURE__ */ jsxRuntimeExports.jsxs( 278 "button", 279 { 280 type: "button", 281 onClick: (e) => { 282 e.stopPropagation(); 283 onUploadClick(song); 284 }, 285 title: t("collection.uploadMidiTooltip", "Upload & bind a MIDI to this song"), 286 className: "inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-bold rounded-lg bg-zinc-100 hover:bg-indigo-50 dark:bg-zinc-800 dark:hover:bg-indigo-950/40 text-zinc-600 hover:text-indigo-600 dark:text-zinc-300 dark:hover:text-indigo-400 border border-zinc-200/60 dark:border-zinc-700/60 hover:border-indigo-200 dark:hover:border-indigo-800 transition shrink-0", 287 children: [
288 /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 12 }), 289 /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 11 }), 290 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Upload MIDI" }) 291 ] 292 } 293 ) 294 ] }), 295 hasTracks ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1 mt-1.5", children: song.tracks.map((track, idx) => { 296 const isPlayingThisTrack = (activeTrack == null ? void 0 : activeTrack._playlistItemId) && track._playlistItemId ? activeTrack._playlistItemId === track._playlistItemId && isPlaying : (activeTrack == null ? void 0 : activeTrack.id) === track.id && isPlaying; 297 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 298 "div", 299 { 300 className: `flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-xs transition-colors group/track ${isPlayingThisTrack ? "bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800/60" : "bg-white/80 dark:bg-zinc-800/40 hover:bg-zinc-100 dark:hover:bg-zinc-800 border border-transparent"}`, 301 children: [
302 /* @__PURE__ */ jsxRuntimeExports.jsx( 303 "button", 304 { 305 type: "button", 306 onClick: () => { 307 if (isPlayingThisTrack) { 308 setIsPlaying(false); 309 } else { 310 playTrack(track, allContextTracks); 311 setPlaylistLabel == null ? void 0 : setPlaylistLabel(playlistTitle); 312 } 313 }, 314 className: `w-6 h-6 rounded-full flex items-center justify-center shrink-0 transition shadow-xs ${isPlayingThisTrack ? "bg-indigo-600 text-white" : "bg-zinc-200 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-indigo-600 hover:text-white"}`, 315 children: isPlayingThisTrack ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 10, className: "fill-current" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 10, className: "fill-current ml-0.5" }) 316 } 317 ), 318 /* @__PURE__ */ jsxRuntimeExports.jsxs( 319 "a", 320 { 321 href: generateTrackUrl(track), 322 className: "flex-1 min-w-0 flex items-center gap-2 cursor-pointer", 323 children: [
324 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `font-medium truncate ${isPlayingThisTrack ? "text-indigo-700 dark:text-indigo-300 font-bold" : "text-zinc-800 dark:text-zinc-200 hover:text-indigo-600 dark:hover:text-indigo-400"}`, children: track.title || songMainTitle }), 325 track.artist && track.artist !== songMainTitle && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-400 truncate hidden md:inline", children: [ 326 "(", 327 track.artist, 328 ")" 329 ] }) 330 ] 331 } 332 ), 333 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-mono text-zinc-400 dark:text-zinc-500 shrink-0", children: track.duration && track.duration !== "0:00" ? track.duration : track.duration_str && track.duration_str !== "0:00" ? track.duration_str : typeof track.durationSec === "number" && track.durationSec > 0 ? `${Math.floor(track.durationSec / 60)}:${Math.round(track.durationSec % 60).toString().padStart(2, "0")}` : "--:--" }),
334 /* @__PURE__ */ jsxRuntimeExports.jsx( 335 "button", 336 { 337 type: "button", 338 onClick: (e) => { 339 e.stopPropagation(); 340 onReportClick(song, track); 341 }, 342 title: t("collection.reportMidiTooltip", "Report irrelevant or mismatched MIDI"), 343 className: "p-1 text-zinc-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition shrink-0 opacity-0 group-hover/track:opacity-100 focus:opacity-100", 344 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Flag, { size: 12 }) 345 } 346 ) 347 ] 348 }, 349 track.id || idx 350 ); 351 }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] text-zinc-400 dark:text-zinc-500 italic px-2 py-1 flex items-center justify-between", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("collection.noMidiBound", "No MIDI files bound yet.") }) }) 352 ] }); 353}; 354const SectionContent = ({ 355 section, 356 activeSlug, 357 onClose, 358 currentUser, 359 onUploadRequested 360}) => { 361 const [songs, setSongs] = reactExports.useState([]); 362 const [isLoading, setIsLoading] = reactExports.useState(true); 363 const [imageError, setImageError] = reactExports.useState(false); 364 const [showAllSongs, setShowAllSongs] = reactExports.useState(false); 365 const [breadcrumbs, setBreadcrumbs] = reactExports.useState([]); 366 const [reportingItem, setReportingItem] = reactExports.useState(null); 367 const { i18n, t } = useTranslation(); 368 const { staffModeEnabled } = useAdmin(); 369 const { mainTitle: rawSecTitle, subTitle: secSubTitle } = getBilingualTitle(section.title, section.subtitle, i18n.language); 370 const secMainTitle = rawSecTitle.replace(/^\d+\.\s*/, ""); 371 const { 372 activeTrack, 373 isPlaying, 374 setIsPlaying, 375 playTrack, 376 activePlaylist, 377 setPlaylistLabel 378 } = usePlayer(); 379 const loadSectionSongs = reactExports.useCallback(() => { 380 let mounted = true; 381 fetch(`/api/collections/${activeSlug}/sections/${section.id}/songs`).then((res) => res.json()).then((data) => { 382 if (mounted && Array.isArray(data)) { 383 let gIdx = 0; 384 setSongs(data.map((s) => ({ ...s, tracks: (s.tracks || []).map((t2) => ({ ...t2, _playlistItemId: `col-${section.id}-${s.id}-${t2.id}-${gIdx++}` })) }))); 385 } 386 }).catch((err) => { 387 console.error("Failed to load section songs", err); 388 }); 389 return () => { 390 mounted = false; 391 }; 392 }, [activeSlug, section.id]); 393 reactExports.useEffect(() => { 394 let mounted = true; 395 setIsLoading(true); 396 const songsPromise = fetch(`/api/collections/${activeSlug}/sections/${section.id}/songs`).then((res) => res.json()).then((data) => { 397 if (mounted && Array.isArray(data)) { 398 let gIdx = 0; 399 setSongs(data.map((s) => ({ ...s, tracks: (s.tracks || []).map((t2) => ({ ...t2, _playlistItemId: `col-${section.id}-${s.id}-${t2.id}-${gIdx++}` })) }))); 400 } 401 }).catch((err) => { 402 console.error("Failed to load section songs", err); 403 }); 404 const breadcrumbsPromise = fetch(`/api/collections/${activeSlug}/sections/${section.id}/breadcrumbs`).then((res) => res.json()).then((data) => { 405 if (mounted && Array.isArray(data)) { 406 setBreadcrumbs(data); 407 } 408 }).catch((err) => console.error("Failed to load breadcrumbs", err)); 409 Promise.allSettled([songsPromise, breadcrumbsPromise]).finally(() => { 410 if (mounted) setIsLoading(false); 411 }); 412 return () => { 413 mounted = false; 414 }; 415 }, [activeSlug, section.id]); 416 reactExports.useEffect(() => { 417 const handleMidiUploaded = () => { 418 loadSectionSongs(); 419 }; 420 window.addEventListener("hamienet:midi_uploaded", handleMidiUploaded); 421 return () => window.removeEventListener("hamienet:midi_uploaded", handleMidiUploaded); 422 }, [loadSectionSongs]); 423 const allSectionTracks = React.useMemo(() => { 424 return songs.flatMap((s) => s.tracks); 425 }, [songs]); 426 const handlePlayAll = () => { 427 if (allSectionTracks.length > 0) { 428 playTrack(allSectionTracks[0], allSectionTracks); 429 setPlaylistLabel == null ? void 0 : setPlaylistLabel(secMainTitle); 430 } 431 }; 432 const handleUploadClick = (song) => { 433 const initialData = { 434 title: song.title, 435 artist: song.details || section.composer || "", 436 canonicalWorkId: song.id 437 }; 438 if (onUploadRequested) { 439 onUploadRequested(initialData); 440 } else { 441 window.dispatchEvent(new CustomEvent("hamienet:request_upload", { detail: initialData })); 442 } 443 }; 444 const handleReportClick = (song, track) => { 445 setReportingItem({ 446 songId: song.id, 447 songTitle: song.title, 448 track 449 }); 450 }; 451 const groupedSongs = React.useMemo(() => { 452 const groups = {};
453 songs.forEach((song) => { 454 const group = song.groupName || "Other Works"; 455 if (!groups[group]) groups[group] = []; 456 groups[group].push(song); 457 }); 458 return groups; 459 }, [songs]); 460 const isSectionPlaying = activePlaylist && activePlaylist.length > 0 && activePlaylist.some((t2) => allSectionTracks.some((st) => st.id === t2.id)); 461 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col bg-white dark:bg-[#0f0f0f] w-full min-h-[50vh]", children: [ 462 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-48 md:h-64 lg:h-80 bg-zinc-100 dark:bg-zinc-900 shrink-0 border-b border-zinc-200 dark:border-zinc-800", children: [ 463 section.bannerImage && !imageError ? /* @__PURE__ */ jsxRuntimeExports.jsx( 464 "img", 465 { 466 src: getOptimizedImageUrl(section.bannerImage, "banner"), 467 className: "w-full h-full object-cover", 468 alt: secMainTitle, 469 title: secSubTitle ? `${secMainTitle} (${secSubTitle})` : secMainTitle, 470 onError: () => setImageError(true) 471 } 472 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-full bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex flex-col items-center justify-center text-indigo-700/50 dark:text-indigo-300/50", children: [
473 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 48, className: "mb-2" }), 474 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold opacity-80", children: "No Banner Image" }) 475 ] }), 476 onClose && /* @__PURE__ */ jsxRuntimeExports.jsx( 477 "button", 478 { 479 onClick: onClose, 480 className: "absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition z-10", 481 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) 482 } 483 ) 484 ] }), 485 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 md:p-8 space-y-6", children: [ 486 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 487 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-3", children: [ 488 section.composer && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex items-center bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300 font-bold text-[10px] uppercase tracking-wider px-2 py-1 rounded", children: section.composer }),
489 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "inline-flex items-center bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 font-bold text-[10px] uppercase tracking-wider px-2 py-1 rounded", children: [ 490 section.trackCount || allSectionTracks.length, 491 " Tracks" 492 ] }) 493 ] }), 494 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-2xl md:text-3xl font-black text-zinc-900 dark:text-white tracking-tight flex flex-wrap items-center gap-2", children: breadcrumbs.length > 0 ? breadcrumbs.map((crumb, index) => { 495 const { mainTitle: crumbMainTitle, hoverTooltip: crumbTooltip } = getBilingualTitle(crumb.title, crumb.subtitle, i18n.language); 496 const isLast = index === breadcrumbs.length - 1; 497 const href = crumb.isCollection ? `/collections/${crumb.slug}` : `/collections/${activeSlug}#section-${crumb.id}`; 498 return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [ 499 index > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 dark:text-zinc-600 font-normal", children: "/" }),
500 /* @__PURE__ */ jsxRuntimeExports.jsx( 501 "a", 502 { 503 href, 504 title: crumbTooltip || crumbMainTitle, 505 onClick: (e) => { 506 if (onClose) { 507 setTimeout(() => onClose(), 10); 508 } 509 }, 510 className: `hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors ${isLast ? "text-zinc-900 dark:text-white" : "text-zinc-500 dark:text-zinc-400 font-bold"}`, 511 children: crumbMainTitle 512 } 513 ) 514 ] }, crumb.id); 515 }) : secMainTitle }), 516 secSubTitle && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400 mt-1", children: secSubTitle }), 517 section.description && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-600 dark:text-zinc-300 mt-4 leading-relaxed max-w-4xl", children: section.description }) 518 ] }), 519 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-3 pt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 520 "button", 521 { 522 onClick: handlePlayAll, 523 disabled: allSectionTracks.length === 0, 524 className: `flex items-center gap-2 px-5 py-2.5 rounded-full font-bold text-sm shadow-sm transition ${isSectionPlaying ? "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800" : "bg-indigo-600 hover:bg-indigo-700 text-white disabled:opacity-50 disabled:cursor-not-allowed"}`, 525 children: [
526 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 16, className: isSectionPlaying ? "text-indigo-500 fill-current" : "fill-current" }), 527 isSectionPlaying ? "Now Playing â" : "Play All" 528 ] 529 } 530 ) }), 531 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-8", children: isLoading ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center py-16 opacity-70 animate-in fade-in duration-300", children: [ 532 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-10 h-10 border-4 border-indigo-200 dark:border-indigo-900 border-t-indigo-600 dark:border-t-indigo-400 rounded-full animate-spin mb-4 shadow-sm" }), 533 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-semibold text-zinc-500 dark:text-zinc-400 tracking-wide uppercase", children: "Loading Tracks..." }), 534 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-8 w-full max-w-md space-y-3 px-4", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-14 bg-zinc-100/50 dark:bg-zinc-800/30 rounded-xl w-full animate-pulse" }, i)) }) 535 ] }) : songs.length === 0 || Object.keys(groupedSongs).length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center py-10 flex flex-col items-center justify-center text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm italic", children: "No tracks available for this section yet." }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-8", children: songs.length > 0 && Object.keys(groupedSongs).length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-8", children: [ 536 Object.entries(groupedSongs).map(([group, groupSongs]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
537 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider mb-3 px-2 border-b border-zinc-100 dark:border-zinc-800 pb-1", children: group }), 538 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1", children: groupSongs.filter((s) => showAllSongs || s.tracks && s.tracks.length > 0).map((song) => /* @__PURE__ */ jsxRuntimeExports.jsx( 539 SongItemRow, 540 { 541 song, 542 allContextTracks: allSectionTracks, 543 activeTrack, 544 isPlaying, 545 setIsPlaying, 546 playTrack, 547 setPlaylistLabel, 548 playlistTitle: secMainTitle, 549 showUploadButton: showAllSongs, 550 onUploadClick: handleUploadClick, 551 onReportClick: handleReportClick, 552 i18n 553 }, 554 song.id 555 )) }) 556 ] }, group)), 557 songs.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pt-4 border-t border-zinc-100 dark:border-zinc-800", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 558 "button", 559 { 560 onClick: () => setShowAllSongs(!showAllSongs), 561 className: "w-full py-3 text-sm font-bold text-zinc-500 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 rounded-lg transition-colors border-2 border-dashed border-zinc-200 dark:border-zinc-800", 562 children: showAllSongs ? songs.filter((s) => !(s.tracks && s.tracks.length > 0)).length > 0 ? "Hide unbound tracks & upload options" : "Hide upload options" : songs.filter((s) => !(s.tracks && s.tracks.length > 0)).length > 0 ? `Show all tracks & upload options (${songs.filter((s) => !(s.tracks && s.tracks.length > 0)).length} unbound tracks hidden)` : "Show upload options" 563 } 564 ) }) 565 ] }) }) }) 566 ] }), 567 reportingItem && /* @__PURE__ */ jsxRuntimeExports.jsx( 568 ReportBoundMidiModal, 569 { 570 isOpen: Boolean(reportingItem), 571 onClose: () => setReportingItem(null), 572 songId: reportingItem.songId, 573 songTitle: reportingItem.songTitle, 574 track: reportingItem.track, 575 currentUser: currentUser || null, 576 onReportSubmitted: () => { 577 loadSectionSongs(); 578 } 579 } 580 ) 581 ] }); 582}; 583export { 584 SectionContent as S 585};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.