diff --git a/index.html b/index.html index 6773f6f..2b9ccf0 100644 --- a/index.html +++ b/index.html @@ -936,6 +936,17 @@ + + +
diff --git a/src/main.js b/src/main.js index 7841461..f4d5c0f 100644 --- a/src/main.js +++ b/src/main.js @@ -14,7 +14,7 @@ import { import { navigate, _navGoBack, _navLoad, onHeaderSearch, closeHeaderSearch, onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum, - searchSimilarArtist, searchAndPreview, loadItunesAlbum, + searchSimilarArtist, searchAndPreview, loadItunesAlbum, closeEventsModal, loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres, } from './views/views.js'; @@ -300,7 +300,7 @@ Object.assign(window, { // Navigation navigate, onHeaderSearch, closeHeaderSearch, onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum, searchSimilarArtist, searchAndPreview, - loadItunesAlbum, + loadItunesAlbum, closeEventsModal, // Player playQueue, togglePlay, prevSong, nextSong, toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo, starCurrent, downloadCurrent, playPreview, diff --git a/src/views/detail.js b/src/views/detail.js index 89590c2..32810e2 100644 --- a/src/views/detail.js +++ b/src/views/detail.js @@ -7,6 +7,7 @@ import { showCtxMenu, openDlModal } from '../ui/ui.js'; // Circular — safe (called inside event handlers / function bodies) import { navigate, _navGoBack } from './navigation.js'; import { getArtistImage } from './home.js'; +import { fetchEvents, openEventsModal } from './events.js'; // ─── QUICK PLAY HELPERS ─── export async function quickPlayAlbum(a) { @@ -85,7 +86,7 @@ export async function loadArtistDetail(artist) { const hasND = !!artist.id; const knownItunes = artist.itunesArtistId || null; - const [imgRes, albumRes, infoRes, topRes, iArtistRes] = await Promise.allSettled([ + const [imgRes, albumRes, infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([ hasND ? getArtistImage(artist.id) : Promise.resolve(null), hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null), hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null), @@ -93,6 +94,7 @@ export async function loadArtistDetail(artist) { knownItunes ? Promise.resolve([{ artistId: knownItunes }]) : itunesSearch(artist.name, 'musicArtist', 5), + fetchEvents(artist.name), ]); const _normWords = s => new Set((s || '').toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(Boolean)); @@ -226,6 +228,14 @@ export async function loadArtistDetail(artist) { actionBar.appendChild(playTopBtn); actionBar.appendChild(shuffleBtn); } + const events = eventsRes.status === 'fulfilled' ? (eventsRes.value || []) : []; + if(events.length) { + const evBtn = document.createElement('button'); + evBtn.className = 'artist-action-btn'; + evBtn.innerHTML = ` Konzerte (${events.length})`; + evBtn.onclick = () => openEventsModal(events, artist.name); + actionBar.appendChild(evBtn); + } } if(albEl) { albEl.innerHTML = ''; _renderArtistAlbums(albEl, ndAlbums, iAlbums, artist); } diff --git a/src/views/events.js b/src/views/events.js new file mode 100644 index 0000000..b06a4ad --- /dev/null +++ b/src/views/events.js @@ -0,0 +1,43 @@ +import { esc } from '../utils.js'; + +export async function fetchEvents(artistName) { + try { + const r = await fetch(`https://rest.bandsintown.com/artists/${encodeURIComponent(artistName)}/events?app_id=novasonic`); + if(!r.ok) return []; + const data = await r.json(); + if(!Array.isArray(data)) return []; + return data.filter(e => new Date(e.datetime) > new Date()); + } catch { return []; } +} + +export function openEventsModal(events, artistName) { + const modal = document.getElementById('events-modal'); + const title = document.getElementById('events-modal-title'); + const list = document.getElementById('events-modal-list'); + if(!modal || !list) return; + + title.textContent = `Konzerte · ${artistName}`; + list.innerHTML = events.map(e => { + const d = new Date(e.datetime); + const date = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' }); + const time = d.getHours() === 0 && d.getMinutes() === 0 ? '' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); + const venue = e.venue?.name || ''; + const city = [e.venue?.city, e.venue?.country].filter(Boolean).join(', '); + const offer = e.offers?.find(o => o.url) || null; + const soldOut = offer?.status === 'unavailable'; + return `
+
${esc(date)}${time ? `${esc(time)}` : ''}
+
+
${esc(venue)}
+
${esc(city)}
+
+ ${offer ? `${soldOut ? 'Ausverkauft' : 'Buchen →'}` : ''} +
`; + }).join(''); + + modal.classList.add('open'); +} + +export function closeEventsModal() { + document.getElementById('events-modal')?.classList.remove('open'); +} diff --git a/src/views/views.js b/src/views/views.js index e51fb65..45544b0 100644 --- a/src/views/views.js +++ b/src/views/views.js @@ -6,3 +6,4 @@ export * from './detail.js'; export * from './playlists.js'; export * from './search.js'; export * from './discover.js'; +export * from './events.js'; diff --git a/styles.css b/styles.css index 5dce645..149cb17 100644 --- a/styles.css +++ b/styles.css @@ -841,6 +841,24 @@ button:focus{outline:none} .artist-hero-name{font-size:44px;letter-spacing:-2px} } +/* ── EVENTS MODAL ── */ +.events-modal{padding:0;overflow:hidden;width:min(520px,calc(100vw - 32px))} +.events-modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--border)} +.events-modal-title{font-size:16px;font-weight:700} +.events-modal-close{background:none;border:none;color:var(--text2);font-size:18px;cursor:pointer;padding:4px 8px;line-height:1} +.events-modal-close:hover{color:var(--text)} +.events-modal-list{overflow-y:auto;max-height:60vh;padding:8px 0} +.event-row{display:flex;align-items:center;gap:12px;padding:12px 24px;border-bottom:1px solid var(--border)} +.event-row:last-child{border-bottom:none} +.event-date{font-size:13px;font-weight:700;color:var(--text);min-width:110px;flex-shrink:0;line-height:1.4} +.event-time{display:block;font-size:11px;font-weight:400;color:var(--text2);margin-top:2px} +.event-venue{flex:1;min-width:0} +.event-venue-name{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.event-venue-city{font-size:11px;color:var(--text2);margin-top:2px} +.event-book-btn{flex-shrink:0;padding:7px 14px;border-radius:20px;background:var(--accent);color:#fff;font-family:var(--font);font-size:12px;font-weight:700;text-decoration:none;transition:opacity .15s} +.event-book-btn:hover{opacity:.85} +.event-book-btn.sold-out{background:var(--surface2);color:var(--text3)} + /* ── MISC ── */ .spinner{width:28px;height:28px;border:3px solid var(--surface2);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;margin:50px auto;display:block} .empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:70px 20px;color:var(--text3);text-align:center}