1- import { useState , useCallback } from 'react' ;
1+ import { useState , useCallback , useRef , useEffect } from 'react' ;
22import { useSongStore } from '../../stores/useSongStore' ;
33import { useModeStore } from '../../stores/useModeStore' ;
44import { MarkerList } from '../Markers/MarkerList' ;
55import { exportSong , importSong , exportSetlist , importSetlist } from '../../services/exportService' ;
6- import type { Setlist } from '../../types' ;
76import { useTabStore } from '../../stores/useTabStore' ;
87import { useToastStore } from '../../stores/useToastStore' ;
98
@@ -21,11 +20,26 @@ export function Sidebar({ onSeekTo, duration, currentTime, isViewer = false, col
2120 const [ sectionsOpen , setSectionsOpen ] = useState ( true ) ;
2221 const [ setlistOpen , setSetlistOpen ] = useState ( true ) ;
2322 const [ setlistName , setSetlistName ] = useState ( '' ) ;
24- const [ importedSetlist ] = useState < Setlist | null > ( null ) ;
2523 const [ editingPauseId , setEditingPauseId ] = useState < string | null > ( null ) ;
2624 const [ editingPauseValue , setEditingPauseValue ] = useState ( '' ) ;
2725 const [ dragIndex , setDragIndex ] = useState < number | null > ( null ) ;
2826 const [ dropIndex , setDropIndex ] = useState < number | null > ( null ) ;
27+ const [ showImportExport , setShowImportExport ] = useState ( false ) ;
28+ const [ setlistExportMode , setSetlistExportMode ] = useState ( false ) ;
29+ const importExportRef = useRef < HTMLDivElement > ( null ) ;
30+
31+ // Close dropdown on outside click
32+ useEffect ( ( ) => {
33+ if ( ! showImportExport ) return ;
34+ const handleClick = ( e : MouseEvent ) => {
35+ if ( importExportRef . current && ! importExportRef . current . contains ( e . target as Node ) ) {
36+ setShowImportExport ( false ) ;
37+ setSetlistExportMode ( false ) ;
38+ }
39+ } ;
40+ document . addEventListener ( 'mousedown' , handleClick ) ;
41+ return ( ) => document . removeEventListener ( 'mousedown' , handleClick ) ;
42+ } , [ showImportExport ] ) ;
2943
3044 const songs = useSongStore ( ( state ) => state . songs ) ;
3145 const songOrder = useSongStore ( ( state ) => state . songOrder ) ;
@@ -249,31 +263,6 @@ export function Sidebar({ onSeekTo, duration, currentTime, isViewer = false, col
249263 </ div >
250264 ) }
251265
252- { /* Song import/export */ }
253- < div className = 'border-t border-slate-700 p-3 flex flex-col gap-2' >
254- < p className = 'text-xs font-mono text-slate-500 uppercase tracking-widest' >
255- Song Data
256- </ p >
257- < div className = 'flex gap-2' >
258- { activeSong && (
259- < button
260- onClick = { handleExportSong }
261- className = 'flex-1 px-2 py-1 text-xs font-mono bg-slate-700
262- hover:bg-slate-600 text-slate-300 rounded transition-colors'
263- >
264- ↓ export
265- </ button >
266- ) }
267- < button
268- onClick = { handleImportSong }
269- className = 'flex-1 px-2 py-1 text-xs font-mono bg-slate-700
270- hover:bg-slate-600 text-slate-300 rounded transition-colors'
271- >
272- ↑ import
273- </ button >
274- </ div >
275- </ div >
276-
277266 </ div >
278267 ) }
279268 </ div >
@@ -546,60 +535,111 @@ export function Sidebar({ onSeekTo, duration, currentTime, isViewer = false, col
546535 </ button >
547536 ) }
548537 </ div >
538+ </ div >
539+ ) }
540+ </ div >
549541
550- { ! isBand && (
551- < >
552- { /* Export setlist */ }
553- < div className = 'flex flex-col gap-2 border-t border-slate-700 pt-3' >
554- < p className = 'text-xs font-mono text-slate-500 uppercase tracking-widest' >
555- Export Setlist
556- </ p >
557- < input
558- type = 'text'
559- placeholder = 'Setlist name...'
560- value = { setlistName }
561- onChange = { ( e ) => setSetlistName ( e . target . value ) }
562- className = 'bg-slate-800 text-slate-200 text-xs rounded px-2 py-1.5
563- border border-slate-600 focus:border-indigo-500 outline-none font-mono'
564- />
565- < button
566- onClick = { handleExportSetlist }
567- disabled = { ! setlistName . trim ( ) || songs . length === 0 }
568- className = 'px-2 py-1 text-xs font-mono bg-slate-700 hover:bg-slate-600
569- text-slate-300 rounded transition-colors
570- disabled:opacity-30 disabled:cursor-not-allowed'
571- >
572- ↓ export setlist
573- </ button >
574- </ div >
575-
576- { /* Import setlist */ }
577- < div className = 'flex flex-col gap-2 border-t border-slate-700 pt-3' >
578- < p className = 'text-xs font-mono text-slate-500 uppercase tracking-widest' >
579- Import Setlist
580- </ p >
542+ { /* ── Import / Export (pinned to bottom) ── */ }
543+ < div className = 'mt-auto border-t border-slate-700 p-3' ref = { importExportRef } >
544+ < div className = 'relative' >
545+ < button
546+ onClick = { ( ) => {
547+ setShowImportExport ( ( v ) => ! v ) ;
548+ setSetlistExportMode ( false ) ;
549+ } }
550+ className = 'w-full px-2 py-1.5 text-xs font-mono rounded transition-colors
551+ bg-slate-700 hover:bg-slate-600 text-slate-300'
552+ >
553+ Import / Export
554+ </ button >
555+ { showImportExport && (
556+ < div className = 'absolute left-0 right-0 bottom-full mb-1 bg-slate-800 border
557+ border-slate-600 rounded-lg shadow-xl py-1 z-50' >
558+ { activeSong && (
559+ < button
560+ onClick = { ( ) => {
561+ handleExportSong ( ) ;
562+ setShowImportExport ( false ) ;
563+ } }
564+ className = 'w-full text-left px-3 py-1.5 text-xs font-mono
565+ text-slate-300 hover:bg-slate-700 transition-colors'
566+ >
567+ ↓ Export Song
568+ </ button >
569+ ) }
570+ < button
571+ onClick = { ( ) => {
572+ handleImportSong ( ) ;
573+ setShowImportExport ( false ) ;
574+ } }
575+ className = 'w-full text-left px-3 py-1.5 text-xs font-mono
576+ text-slate-300 hover:bg-slate-700 transition-colors'
577+ >
578+ ↑ Import Song
579+ </ button >
580+ { ! isBand && (
581+ < >
582+ < div className = 'border-t border-slate-700 my-1' />
583+ { ! setlistExportMode ? (
584+ < button
585+ onClick = { ( ) => setSetlistExportMode ( true ) }
586+ disabled = { songs . length === 0 }
587+ className = 'w-full text-left px-3 py-1.5 text-xs font-mono
588+ text-slate-300 hover:bg-slate-700 transition-colors
589+ disabled:opacity-30 disabled:cursor-not-allowed'
590+ >
591+ ↓ Export Setlist
592+ </ button >
593+ ) : (
594+ < div className = 'px-3 py-1.5 flex flex-col gap-1.5' >
595+ < input
596+ type = 'text'
597+ placeholder = 'Setlist name...'
598+ value = { setlistName }
599+ onChange = { ( e ) => setSetlistName ( e . target . value ) }
600+ onKeyDown = { ( e ) => {
601+ if ( e . key === 'Enter' && setlistName . trim ( ) ) {
602+ handleExportSetlist ( ) ;
603+ setShowImportExport ( false ) ;
604+ setSetlistExportMode ( false ) ;
605+ }
606+ } }
607+ autoFocus
608+ className = 'bg-slate-900 text-slate-200 text-xs rounded px-2
609+ py-1 border border-slate-600 focus:border-indigo-500
610+ outline-none font-mono w-full'
611+ />
612+ < button
613+ onClick = { ( ) => {
614+ handleExportSetlist ( ) ;
615+ setShowImportExport ( false ) ;
616+ setSetlistExportMode ( false ) ;
617+ } }
618+ disabled = { ! setlistName . trim ( ) }
619+ className = 'px-2 py-1 text-xs font-mono bg-indigo-600
620+ hover:bg-indigo-500 text-white rounded
621+ transition-colors disabled:opacity-30
622+ disabled:cursor-not-allowed'
623+ >
624+ ↓ Export
625+ </ button >
626+ </ div >
627+ ) }
581628 < button
582- onClick = { handleImportSetlist }
583- className = 'px-2 py-1 text-xs font-mono bg-slate-700 hover:bg-slate-600
584- text-slate-300 rounded transition-colors'
629+ onClick = { ( ) => {
630+ handleImportSetlist ( ) ;
631+ setShowImportExport ( false ) ;
632+ } }
633+ className = 'w-full text-left px-3 py-1.5 text-xs font-mono
634+ text-slate-300 hover:bg-slate-700 transition-colors'
585635 >
586- ↑ import setlist
636+ ↑ Import Setlist
587637 </ button >
588- { importedSetlist && (
589- < div className = 'flex flex-col gap-1' >
590- < p className = 'text-xs font-mono text-slate-400' > { importedSetlist . name } </ p >
591- { importedSetlist . entries . map ( ( entry ) => (
592- < div key = { entry . songId } className = 'text-xs font-mono text-slate-500 px-2' >
593- • { entry . title }
594- </ div >
595- ) ) }
596- </ div >
597- ) }
598- </ div >
599- </ >
600- ) }
601- </ div >
602- ) }
638+ </ >
639+ ) }
640+ </ div >
641+ ) }
642+ </ div >
603643 </ div >
604644 </ aside >
605645 ) ;
0 commit comments