// Nanga Kathai marketing site. Reads a few songs and the catalogue counts from the live Supabase // backend (same tables as lib/data/catalog_repository.dart; row-level security only returns // approved songs) and plays them in the browser. Settings come from config.js (tools/build_site.py). (() => { 'use strict'; const cfg = window.NK_CONFIG || {}; const API = `${cfg.supabaseUrl}/rest/v1`; const headers = { apikey: cfg.supabaseKey, Authorization: `Bearer ${cfg.supabaseKey}` }; const FEATURED = 6; // Same occasion labels and colours as lib/core/models.dart. const OCCASIONS = { huttu: ['Huttu', '#3B6E8F'], habba: ['Habba', '#B5541E'], madhuvae: ['Madhuvae', '#9C2F4A'], devotional: ['Devotional', '#6B4FA0'], folk: ['Folk songs', '#2E7D5B'], saavu: ['Saavu', '#4A4F57'], instrumental: ['Instrumental', '#8A5A2B'], }; const $ = (sel, el = document) => el.querySelector(sel); const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const fmt = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, '0')}`; const icons = { play: '', pause: '', }; async function get(path, extra = {}) { const res = await fetch(`${API}/${path}`, { headers: { ...headers, ...extra } }); if (!res.ok) throw new Error(`${res.status} ${path}`); return res; } // One song from each of the biggest albums, so the sample shows some variety. async function loadSongs() { const [artists, albums, tracks] = await Promise.all([ get('artists?select=id,name_latin').then((r) => r.json()), get('albums?select=id,title_latin,cover_url').then((r) => r.json()), get('tracks?select=id,title_latin,album_id,occasion,duration_ms,audio_key_128,cover_url,' + 'track_artists(artist_id,position,role)&status=eq.approved&order=created_at.asc').then((r) => r.json()), ]); const artistName = new Map(artists.map((a) => [a.id, a.name_latin])); const albumById = new Map(albums.map((a) => [a.id, a])); const byAlbum = new Map(); for (const t of tracks) { if (!byAlbum.has(t.album_id)) byAlbum.set(t.album_id, []); byAlbum.get(t.album_id).push(t); } const picks = [...byAlbum.values()].sort((a, b) => b.length - a.length).slice(0, FEATURED).map((list) => list[0]); const songs = picks.map((r) => { const album = albumById.get(r.album_id); const singers = (r.track_artists || []).filter((t) => t.role === 'singer') .sort((a, b) => a.position - b.position).map((t) => artistName.get(t.artist_id)).filter(Boolean); const [label, color] = OCCASIONS[r.occasion] || OCCASIONS.folk; return { id: r.id, title: r.title_latin, credit: singers.join(', ') || album?.title_latin || 'Badaga song', album: album?.title_latin, label, color, coverUrl: r.cover_url || album?.cover_url || null, audioUrl: `${cfg.mediaBaseUrl}/${r.audio_key_128}`, seconds: (r.duration_ms || 0) / 1000, }; }); return { songs, songCount: tracks.length, albumCount: byAlbum.size }; } const cover = (s) => `
${s.coverUrl ? `` : '♪'}
`; // ---------- Player ---------- const audio = new Audio(); audio.preload = 'none'; let songs = []; let current = -1; function play(i) { if (i === current) { audio.paused ? audio.play() : audio.pause(); return; } current = i; const s = songs[i]; audio.src = s.audioUrl; audio.play().catch(() => {}); $('#phone-title').textContent = s.title; $('#phone-credit').textContent = s.credit; $('#phone-art').innerHTML = cover(s); if ('mediaSession' in navigator) { navigator.mediaSession.metadata = new MediaMetadata({ title: s.title, artist: s.credit, album: s.album || 'Nanga Kathai', artwork: [{ src: s.coverUrl || '/img/icon-256.png', sizes: '256x256' }], }); } sync(); } function sync() { document.querySelectorAll('.sample').forEach((el, i) => { const on = i === current && !audio.paused; el.classList.toggle('playing', i === current); el.querySelector('.sample-btn').innerHTML = on ? icons.pause : icons.play; el.querySelector('.sample-btn').setAttribute('aria-label', `${on ? 'Pause' : 'Play'} ${songs[i].title}`); }); const btn = $('#phone-play'); btn.innerHTML = audio.paused ? icons.play : icons.pause; btn.setAttribute('aria-label', audio.paused ? 'Play' : 'Pause'); } audio.addEventListener('play', sync); audio.addEventListener('pause', sync); audio.addEventListener('ended', () => (current < songs.length - 1 ? play(current + 1) : sync())); audio.addEventListener('timeupdate', () => { if (!audio.duration) return; $('#phone-bar').style.width = `${(audio.currentTime / audio.duration) * 100}%`; $('#phone-time').textContent = fmt(audio.currentTime); $('#phone-total').textContent = fmt(audio.duration); }); $('#phone-play').onclick = () => play(current < 0 ? 0 : current); $('#phone-next').onclick = () => songs.length && play((current + 1) % songs.length); $('#phone-prev').onclick = () => songs.length && play(current <= 0 ? 0 : current - 1); loadSongs().then((data) => { songs = data.songs; $('#count-songs').textContent = data.songCount; $('#count-albums').textContent = data.albumCount; $('#samples').innerHTML = songs.map((s, i) => `
  • ${cover(s)}
    ${esc(s.title)}${esc(s.credit)}
    ${esc(s.label)} ${s.seconds ? fmt(s.seconds) : ''}
  • `).join(''); $('#samples').querySelectorAll('.sample').forEach((el, i) => { el.onclick = () => play(i); }); if (songs[0]) { $('#phone-title').textContent = songs[0].title; $('#phone-credit').textContent = songs[0].credit; $('#phone-art').innerHTML = cover(songs[0]); $('#phone-total').textContent = fmt(songs[0].seconds); } }).catch((err) => { console.error(err); $('#samples').innerHTML = '
  • The songs could not be loaded right now. Please refresh the page.
  • '; }); // ---------- Store links: "coming soon" until the URLs are set in config.js ---------- document.querySelectorAll('[data-store]').forEach((a) => { const url = cfg[a.dataset.store]; if (url) { a.href = url; a.classList.remove('soon'); a.querySelector('small').textContent = a.dataset.store === 'playUrl' ? 'Get it on' : 'Download on the'; } else { a.removeAttribute('href'); } }); $('#year').textContent = new Date().getFullYear(); })();