2026-07-08 11:05:21 +02:00
let me = null ;
let allSongs = [ ] ;
let setlist = null ;
2026-07-08 11:50:12 +02:00
let mainRows = [ ] ;
let encoreRows = [ ] ;
let addSongId = null ;
2026-07-08 11:05:21 +02:00
function formatDate ( dateStr ) {
const d = new Date ( dateStr ) ;
return d . toLocaleDateString ( 'fr-FR' , { weekday : 'long' , year : 'numeric' , month : 'long' , day : 'numeric' } ) ;
}
function readOnlyView ( ) {
const main = setlist . songs . filter ( ( s ) => ! s . is _encore ) ;
const encore = setlist . songs . filter ( ( s ) => s . is _encore ) ;
return `
< div class = "card" >
< div class = "card-title" > $ { escapeHtml ( setlist . name || 'Concert' ) } < / d i v >
< div class = "card-subtitle" > $ { escapeHtml ( setlist . venue || '' ) } · $ { formatDate ( setlist . concert _date ) } < / d i v >
< / d i v >
< div class = "setlist-section" >
< h3 > Setlist < / h 3 >
$ { main . length
2026-07-08 11:50:12 +02:00
? ` <ol class="setlist"> ${ main . map ( ( s ) => ` <li><span class="row-title"> ${ escapeHtml ( s . title ) } </span> <span class="row-artist">— ${ escapeHtml ( s . artist ) } </span> ${ s . note ? ` <span class="note"> ${ escapeHtml ( s . note ) } </span> ` : '' } </li> ` ) . join ( '' ) } </ol> `
2026-07-08 11:05:21 +02:00
: '<p class="empty">Aucun morceau pour le moment.</p>' }
< / d i v >
< div class = "setlist-section" >
< h3 > Rappel < / h 3 >
$ { encore . length
2026-07-08 11:50:12 +02:00
? ` <ol class="setlist"> ${ encore . map ( ( s ) => ` <li><span class="row-title"> ${ escapeHtml ( s . title ) } </span> <span class="row-artist">— ${ escapeHtml ( s . artist ) } </span> ${ s . note ? ` <span class="note"> ${ escapeHtml ( s . note ) } </span> ` : '' } </li> ` ) . join ( '' ) } </ol> `
2026-07-08 11:05:21 +02:00
: '<p class="empty">Aucun morceau de rappel prévu.</p>' }
< / d i v >
2026-07-08 11:50:12 +02:00
$ { me . isAdmin ? '<button id="edit-btn" class="secondary" style="margin-top:1rem">Modifier la setlist</button>' : '' }
2026-07-08 11:05:21 +02:00
` ;
}
2026-07-08 11:50:12 +02:00
function renderReadOnly ( ) {
const container = document . getElementById ( 'content' ) ;
if ( ! setlist ) {
container . innerHTML = me . isAdmin
? ` <p class="empty">Aucun concert à venir.</p> ${ metaFormTemplate ( ) } `
: '<p class="empty">Aucun concert à venir pour le moment.</p>' ;
if ( me . isAdmin ) attachMetaFormHandler ( ) ;
return ;
}
container . innerHTML = readOnlyView ( ) ;
const editBtn = document . getElementById ( 'edit-btn' ) ;
if ( editBtn ) editBtn . addEventListener ( 'click' , enterEditMode ) ;
2026-07-08 11:05:21 +02:00
}
2026-07-08 11:50:12 +02:00
function metaFormTemplate ( ) {
2026-07-08 11:05:21 +02:00
return `
2026-07-08 11:50:12 +02:00
< div class = "panel" >
< h3 > $ { setlist ? 'Détails du concert' : 'Créer le prochain concert' } < / h 3 >
2026-07-08 11:05:21 +02:00
< form id = "meta-form" class = "inline-form" >
< label > Nom < input name = "name" value = "${escapeHtml(setlist?.name || '')}" > < / l a b e l >
< label > Lieu < input name = "venue" value = "${escapeHtml(setlist?.venue || '')}" > < / l a b e l >
< label > Date < input type = "date" name = "concertDate" value = "${setlist ? setlist.concert_date.slice(0, 10) : ''}" required > < / l a b e l >
< button type = "submit" > $ { setlist ? 'Enregistrer' : 'Créer le concert' } < / b u t t o n >
< / f o r m >
< / d i v >
2026-07-08 11:50:12 +02:00
` ;
}
function usedSongIds ( ) {
return new Set ( [ ... mainRows , ... encoreRows ] . map ( ( r ) => r . songId ) ) ;
}
function availableSongOptions ( ) {
const used = usedSongIds ( ) ;
const available = allSongs . filter ( ( s ) => ! used . has ( s . id ) ) ;
if ( ! available . length ) return '<option value="">(tous les morceaux sont déjà dans la setlist)</option>' ;
return available . map ( ( s ) => ` <option value=" ${ s . id } "> ${ escapeHtml ( s . title ) } — ${ escapeHtml ( s . artist ) } </option> ` ) . join ( '' ) ;
}
function rowTemplate ( row , index , section , total ) {
return `
< div class = "setlist-row" data - section = "${section}" data - index = "${index}" >
< div class = "row-index" > $ { index + 1 } < / d i v >
< div class = "row-main" >
< div class = "row-title" > $ { escapeHtml ( row . title ) } < / d i v >
< div class = "row-artist" > $ { escapeHtml ( row . artist ) } < / d i v >
< / d i v >
< input class = "row-note-input" data - field = "note" placeholder = "Note (optionnel)" value = "${escapeHtml(row.note || '')}" >
< div class = "row-actions" >
< button type = "button" class = "secondary icon-btn move-up" $ { index === 0 ? 'disabled' : '' } title = "Monter" > & # 8593 ; < / b u t t o n >
< button type = "button" class = "secondary icon-btn move-down" $ { index === total - 1 ? 'disabled' : '' } title = "Descendre" > & # 8595 ; < / b u t t o n >
< button type = "button" class = "secondary icon-btn move-section" title = "${section === 'main' ? 'Déplacer en rappel' : 'Déplacer au programme'}" > $ { section === 'main' ? '⇥ Rappel' : '⇤ Programme' } < / b u t t o n >
< button type = "button" class = "danger icon-btn remove-row" title = "Retirer" > & times ; < / b u t t o n >
< / d i v >
< / d i v >
` ;
}
function editView ( ) {
return `
$ { metaFormTemplate ( ) }
2026-07-08 11:05:21 +02:00
$ { setlist ? `
2026-07-08 11:50:12 +02:00
< div class = "panel" >
< h3 > Ajouter un morceau du répertoire < / h 3 >
< form id = "add-song-form" class = "inline-form" >
< label > Morceau
< select id = "add-song-select" > $ { availableSongOptions ( ) } < / s e l e c t >
< / l a b e l >
< button type = "button" id = "add-to-main-btn" class = "secondary" > Ajouter au programme < / b u t t o n >
< button type = "button" id = "add-to-encore-btn" class = "secondary" > Ajouter au rappel < / b u t t o n >
< / f o r m >
< / d i v >
< div class = "setlist-section" >
< h3 > Programme principal < / h 3 >
< div class = "setlist-rows" id = "main-rows" >
$ { mainRows . length ? mainRows . map ( ( r , i ) => rowTemplate ( r , i , 'main' , mainRows . length ) ) . join ( '' ) : '<p class="empty">Aucun morceau. Ajoutez-en depuis le répertoire ci-dessus.</p>' }
< / d i v >
< / d i v >
< div class = "setlist-section" >
< h3 > Rappel < / h 3 >
< div class = "setlist-rows" id = "encore-rows" >
$ { encoreRows . length ? encoreRows . map ( ( r , i ) => rowTemplate ( r , i , 'encore' , encoreRows . length ) ) . join ( '' ) : '<p class="empty">Aucun morceau de rappel prévu.</p>' }
< / d i v >
2026-07-08 11:05:21 +02:00
< / d i v >
2026-07-08 11:50:12 +02:00
< div class = "inline-form" style = "margin-top:1.25rem" >
2026-07-08 11:05:21 +02:00
< button type = "button" id = "save-songs-btn" > Enregistrer la setlist < / b u t t o n >
2026-07-08 11:50:12 +02:00
< button type = "button" id = "cancel-edit-btn" class = "secondary" > Retour < / b u t t o n >
2026-07-08 11:05:21 +02:00
< / d i v > ` : ' ' }
` ;
}
function enterEditMode ( ) {
2026-07-08 11:50:12 +02:00
mainRows = setlist . songs
. filter ( ( s ) => ! s . is _encore )
. map ( ( s ) => ( { setlistSongId : s . id , songId : s . song _id , title : s . title , artist : s . artist , note : s . note } ) ) ;
encoreRows = setlist . songs
. filter ( ( s ) => s . is _encore )
. map ( ( s ) => ( { setlistSongId : s . id , songId : s . song _id , title : s . title , artist : s . artist , note : s . note } ) ) ;
2026-07-08 11:05:21 +02:00
renderEdit ( ) ;
}
function renderEdit ( ) {
const container = document . getElementById ( 'content' ) ;
container . innerHTML = editView ( ) ;
attachMetaFormHandler ( ) ;
attachEditHandlers ( ) ;
}
function attachMetaFormHandler ( ) {
const form = document . getElementById ( 'meta-form' ) ;
if ( ! form ) return ;
form . addEventListener ( 'submit' , async ( e ) => {
e . preventDefault ( ) ;
const name = form . name . value . trim ( ) ;
const venue = form . venue . value . trim ( ) ;
const concertDate = form . concertDate . value ;
try {
if ( setlist ) {
setlist = await api . patch ( ` /api/setlists/ ${ setlist . id } ` , { name , venue , concertDate } ) ;
setlist . songs = ( await api . get ( ` /api/setlists/ ${ setlist . id } ` ) ) . songs ;
} else {
setlist = await api . post ( '/api/setlists' , { name , venue , concertDate } ) ;
setlist . songs = [ ] ;
}
enterEditMode ( ) ;
} catch ( err ) {
showError ( err . message ) ;
}
} ) ;
}
2026-07-08 11:50:12 +02:00
function rowsForSection ( section ) {
return section === 'main' ? mainRows : encoreRows ;
}
2026-07-08 11:05:21 +02:00
function attachEditHandlers ( ) {
2026-07-08 11:50:12 +02:00
const addSelect = document . getElementById ( 'add-song-select' ) ;
const addToMain = document . getElementById ( 'add-to-main-btn' ) ;
const addToEncore = document . getElementById ( 'add-to-encore-btn' ) ;
if ( addSelect ) {
if ( ! addSelect . value ) addSongId = null ;
addSelect . addEventListener ( 'change' , ( ) => { addSongId = addSelect . value ? parseInt ( addSelect . value , 10 ) : null ; } ) ;
}
if ( addToMain ) addToMain . addEventListener ( 'click' , ( ) => addSongToSection ( 'main' , addSelect ) ) ;
if ( addToEncore ) addToEncore . addEventListener ( 'click' , ( ) => addSongToSection ( 'encore' , addSelect ) ) ;
document . querySelectorAll ( '.setlist-row' ) . forEach ( ( rowEl ) => {
const section = rowEl . dataset . section ;
const index = parseInt ( rowEl . dataset . index , 10 ) ;
const rows = rowsForSection ( section ) ;
rowEl . querySelector ( '.row-note-input' ) . addEventListener ( 'change' , ( e ) => {
rows [ index ] . note = e . target . value ;
} ) ;
rowEl . querySelector ( '.move-up' ) ? . addEventListener ( 'click' , ( ) => {
if ( index === 0 ) return ;
[ rows [ index - 1 ] , rows [ index ] ] = [ rows [ index ] , rows [ index - 1 ] ] ;
2026-07-08 11:05:21 +02:00
renderEdit ( ) ;
} ) ;
2026-07-08 11:50:12 +02:00
rowEl . querySelector ( '.move-down' ) ? . addEventListener ( 'click' , ( ) => {
if ( index === rows . length - 1 ) return ;
[ rows [ index + 1 ] , rows [ index ] ] = [ rows [ index ] , rows [ index + 1 ] ] ;
renderEdit ( ) ;
2026-07-08 11:05:21 +02:00
} ) ;
2026-07-08 11:50:12 +02:00
rowEl . querySelector ( '.move-section' ) . addEventListener ( 'click' , ( ) => {
const [ row ] = rows . splice ( index , 1 ) ;
if ( section === 'main' ) encoreRows . push ( row ) ;
else mainRows . push ( row ) ;
2026-07-08 11:05:21 +02:00
renderEdit ( ) ;
} ) ;
2026-07-08 11:50:12 +02:00
rowEl . querySelector ( '.remove-row' ) . addEventListener ( 'click' , ( ) => {
rows . splice ( index , 1 ) ;
renderEdit ( ) ;
2026-07-08 11:05:21 +02:00
} ) ;
2026-07-08 11:50:12 +02:00
} ) ;
const saveBtn = document . getElementById ( 'save-songs-btn' ) ;
if ( saveBtn ) saveBtn . addEventListener ( 'click' , onSaveSongs ) ;
2026-07-08 11:05:21 +02:00
const cancelBtn = document . getElementById ( 'cancel-edit-btn' ) ;
if ( cancelBtn ) cancelBtn . addEventListener ( 'click' , renderReadOnly ) ;
}
2026-07-08 11:50:12 +02:00
function addSongToSection ( section , selectEl ) {
const songId = selectEl . value ? parseInt ( selectEl . value , 10 ) : null ;
if ( ! songId ) return ;
const song = allSongs . find ( ( s ) => s . id === songId ) ;
if ( ! song ) return ;
const row = { setlistSongId : null , songId : song . id , title : song . title , artist : song . artist , note : '' } ;
if ( section === 'main' ) mainRows . push ( row ) ;
else encoreRows . push ( row ) ;
renderEdit ( ) ;
}
async function onSaveSongs ( ) {
try {
const payload = [
... mainRows . map ( ( r , i ) => ( { songId : r . songId , position : i + 1 , note : r . note , isEncore : false } ) ) ,
... encoreRows . map ( ( r , i ) => ( { songId : r . songId , position : i + 1 , note : r . note , isEncore : true } ) ) ,
] ;
setlist = await api . put ( ` /api/setlists/ ${ setlist . id } /songs ` , { songs : payload } ) ;
renderReadOnly ( ) ;
} catch ( err ) {
showError ( err . message ) ;
}
}
2026-07-08 11:05:21 +02:00
function showError ( message ) {
document . getElementById ( 'error' ) . innerHTML = ` <div class="error-banner"> ${ escapeHtml ( message ) } </div> ` ;
}
( async function init ( ) {
me = await initNav ( 'setlist' ) ;
if ( me . isAdmin ) {
allSongs = await api . get ( '/api/songs' ) ;
}
setlist = await api . get ( '/api/setlists/next' ) ;
renderReadOnly ( ) ;
} ) ( ) ;