160 lines
7.1 KiB
JavaScript
160 lines
7.1 KiB
JavaScript
// 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: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg>',
|
|
pause: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>',
|
|
};
|
|
|
|
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) => `<div class="cover" style="background:${s.color}">${s.coverUrl
|
|
? `<img src="${esc(s.coverUrl)}" alt="" loading="lazy" onerror="this.remove()">` : '<span>♪</span>'}</div>`;
|
|
|
|
// ---------- 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) => `
|
|
<li class="sample">
|
|
<button class="sample-btn" aria-label="Play ${esc(s.title)}">${icons.play}</button>
|
|
${cover(s)}
|
|
<div class="t"><b>${esc(s.title)}</b><span>${esc(s.credit)}</span></div>
|
|
<span class="tag" style="--c:${s.color}">${esc(s.label)}</span>
|
|
<span class="dur">${s.seconds ? fmt(s.seconds) : ''}</span>
|
|
</li>`).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 = '<li class="empty">The songs could not be loaded right now. Please refresh the page.</li>';
|
|
});
|
|
|
|
// ---------- Store links: config.js can override the links written in the HTML ----------
|
|
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';
|
|
}
|
|
});
|
|
$('#year').textContent = new Date().getFullYear();
|
|
})();
|