diff --git a/.gitignore b/.gitignore index d3e45bb..6f0e289 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ env.json # Song collection and import manifest (large, private) /songs/ /songs_manifest.csv +site/config.js diff --git a/site/app.css b/site/app.css new file mode 100644 index 0000000..54d948c --- /dev/null +++ b/site/app.css @@ -0,0 +1,186 @@ +/* Nanga Kathai marketing site. Colours match lib/app/theme.dart (NkColors). */ +:root { + --ground: #0B1A14; + --surface: #14281F; + --raised: #1F3A2B; + --outline: #2A4537; + --emblem: #1F4A2E; + --tea: #7FA857; + --sun: #F5B335; + --cream: #FBF7EE; + --muted: #9FB3A6; + --on-photo: #DCE5DF; +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { + margin: 0; background: var(--ground); color: var(--on-photo); + font: 17px/1.65 Manrope, system-ui, -apple-system, "Segoe UI", sans-serif; + -webkit-font-smoothing: antialiased; overflow-x: hidden; +} +h1, h2, h3 { font-family: Sora, Manrope, sans-serif; color: var(--cream); line-height: 1.12; margin: 0; letter-spacing: -.015em; } +h2 { font-size: clamp(28px, 4vw, 44px); } +a { color: var(--sun); } +img { display: block; max-width: 100%; } +button { font: inherit; color: inherit; cursor: pointer; } +.wrap { max-width: 1160px; margin: 0 auto; padding: 0 20px; } +.muted { color: var(--muted); } +.eyebrow { color: var(--sun); font-weight: 800; letter-spacing: .16em; text-transform: uppercase; font-size: 13px; margin: 0 0 14px; } +section { padding: 96px 0 0; scroll-margin-top: 40px; } +.center-head { text-align: center; max-width: 680px; margin: 0 auto 44px; } +.center-head p.muted { margin: 14px 0 0; } + +/* Header */ +.top { position: sticky; top: 0; z-index: 20; background: rgba(11, 26, 20, .82); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(42, 69, 55, .5); } +.top .wrap { display: flex; align-items: center; gap: 20px; height: 68px; } +.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--cream); font: 700 18px Sora, sans-serif; } +.brand img { width: 36px; height: 36px; border-radius: 9px; } +.top nav { display: flex; align-items: center; gap: 26px; margin-left: auto; } +.top nav a:not(.btn) { color: var(--on-photo); text-decoration: none; font-weight: 600; font-size: 15px; } +.top nav a:not(.btn):hover { color: var(--sun); } +@media (max-width: 760px) { .top nav a.opt { display: none; } } + +/* Buttons */ +.btn { display: inline-flex; align-items: center; gap: 10px; border: 0; border-radius: 999px; padding: 14px 26px; font-weight: 700; font-size: 16px; text-decoration: none; } +.btn.sm { padding: 9px 18px; font-size: 14px; } +.btn.primary { background: var(--sun); color: var(--ground); } +.btn.primary:hover { background: #ffc552; } +.btn.ghost { background: rgba(251,247,238,.08); color: var(--cream); border: 1px solid rgba(251,247,238,.25); } +.btn.ghost:hover { background: rgba(251,247,238,.16); } +.link { color: var(--sun); font-weight: 700; text-decoration: none; } +.actions { display: flex; flex-wrap: wrap; gap: 12px; } +.store-badge { + display: inline-flex; align-items: center; gap: 12px; padding: 10px 22px 10px 18px; border-radius: 14px; + background: var(--cream); color: var(--ground); text-decoration: none; font: 700 18px/1.15 Sora, sans-serif; min-width: 190px; +} +.store-badge svg { width: 26px; height: 26px; flex: none; } +.store-badge small { display: block; font: 600 11px Manrope, sans-serif; opacity: .7; } +.store-badge:hover { background: #fff; } +.store-badge.soon { cursor: default; } +.store-badge.soon:hover { background: var(--cream); } + +/* Hero */ +.hero { position: relative; padding: 72px 0 96px; overflow: hidden; } +.hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .55; } +.hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(100deg, var(--ground) 20%, rgba(11,26,20,.6) 60%, rgba(11,26,20,.35)), linear-gradient(180deg, transparent 70%, var(--ground)); } +.hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: 1.15fr .85fr; gap: 48px; align-items: center; } +.hero h1 { font-size: clamp(40px, 6vw, 72px); } +.hero h1 em { font-style: normal; color: var(--sun); } +.lead { font-size: clamp(18px, 2vw, 21px); max-width: 580px; margin: 22px 0 32px; } +.facts { display: flex; flex-wrap: wrap; gap: 10px 28px; margin-top: 36px; color: var(--muted); font-size: 15px; } +.facts b { color: var(--cream); font: 700 24px Sora, sans-serif; margin-right: 4px; } +@media (max-width: 860px) { .hero-grid { grid-template-columns: 1fr; } .phone { margin: 0 auto; } } + +/* Phone preview */ +.phone { + width: 300px; border-radius: 44px; padding: 12px; background: #050d09; + box-shadow: 0 40px 80px rgba(0,0,0,.55), inset 0 0 0 2px #2a3a31; justify-self: center; transform: rotate(2deg); +} +.phone-screen { position: relative; border-radius: 34px; overflow: hidden; background: var(--ground); padding: 28px 22px 26px; text-align: center; } +.phone-photo { position: absolute; inset: 0 0 45% 0; } +.phone-photo img { width: 100%; height: 100%; object-fit: cover; opacity: .5; } +.phone-photo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(11,26,20,.2), var(--ground)); } +.phone-screen > *:not(.phone-photo) { position: relative; } +.phone-label { margin: 0 0 18px; font-size: 11px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; color: var(--on-photo); } +.phone-art { width: 196px; margin: 0 auto 20px; } +.phone-art .cover { border-radius: 16px; box-shadow: 0 16px 40px rgba(0,0,0,.5); } +.phone-art .cover span { font-size: 64px; } +.phone-screen > b { display: block; color: var(--cream); font: 700 19px Sora, sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.phone-screen > span { display: block; color: var(--muted); font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.phone-progress { height: 4px; border-radius: 4px; background: rgba(251,247,238,.18); margin: 20px 0 6px; overflow: hidden; } +.phone-progress i { display: block; height: 100%; width: 0; background: var(--sun); } +.phone-times { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; } +.phone-controls { display: flex; justify-content: center; align-items: center; gap: 22px; margin-top: 10px; } +.phone-controls button { background: none; border: 0; color: var(--cream); padding: 6px; display: grid; place-items: center; border-radius: 999px; } +.phone-controls button svg { width: 26px; height: 26px; } +.phone-controls .big { width: 60px; height: 60px; background: var(--sun); color: var(--ground); } +.phone-controls .big:hover { background: #ffc552; } + +/* Covers */ +.cover { position: relative; aspect-ratio: 1; border-radius: 10px; overflow: hidden; display: grid; place-items: center; } +.cover::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 6%; min-height: 3px; background: var(--cream); opacity: .9; } +.cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } +.cover span { color: rgba(251,247,238,.75); font-size: 22px; } + +/* Song samples */ +.listen-grid { display: grid; grid-template-columns: .8fr 1.2fr; gap: 56px; align-items: center; } +.listen-grid h2 { margin-bottom: 16px; } +.listen-grid p { margin: 0 0 20px; } +@media (max-width: 860px) { .listen-grid { grid-template-columns: 1fr; gap: 28px; } } +.samples { list-style: none; margin: 0; padding: 10px; background: var(--surface); border: 1px solid var(--outline); border-radius: 22px; } +.sample { display: grid; grid-template-columns: 40px 48px minmax(0, 1fr) auto auto; gap: 14px; align-items: center; padding: 10px; border-radius: 14px; cursor: pointer; } +.sample:hover, .sample.playing { background: var(--raised); } +.sample-btn { width: 40px; height: 40px; border-radius: 999px; border: 0; background: rgba(251,247,238,.1); color: var(--cream); display: grid; place-items: center; } +.sample:hover .sample-btn, .sample.playing .sample-btn { background: var(--sun); color: var(--ground); } +.sample-btn svg { width: 18px; height: 18px; } +.sample .t { min-width: 0; } +.sample b { display: block; color: var(--cream); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.sample.playing b { color: var(--sun); } +.sample .t span { display: block; font-size: 14px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.tag { font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 999px; background: color-mix(in srgb, var(--c) 35%, transparent); color: var(--cream); white-space: nowrap; } +.dur { font-size: 14px; color: var(--muted); font-variant-numeric: tabular-nums; } +@media (max-width: 560px) { .sample { grid-template-columns: 40px 44px minmax(0, 1fr) auto; } .sample .tag { display: none; } } +.skeleton { list-style: none; height: 64px; margin: 6px 0; border-radius: 14px; background: linear-gradient(90deg, var(--surface), var(--raised), var(--surface)); background-size: 200% 100%; animation: sk 1.4s infinite; } +@keyframes sk { to { background-position: -200% 0; } } +.empty { color: var(--muted); padding: 20px; } + +/* Occasions */ +.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; } +.tile { position: relative; overflow: hidden; border-radius: 18px; padding: 24px 22px 30px; background: var(--c); min-height: 160px; } +.tile:first-child { grid-row: span 2; display: flex; flex-direction: column; justify-content: flex-end; } +.tile::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 6px; background: var(--cream); opacity: .9; } +.tile b { display: block; font: 700 22px Sora, sans-serif; color: var(--cream); margin-bottom: 6px; } +.tile span { font-size: 15px; color: rgba(251,247,238,.85); } +@media (max-width: 860px) { .tiles { grid-template-columns: repeat(2, 1fr); } .tile:first-child { grid-row: auto; } } + +/* Features */ +.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; } +.feature { background: var(--surface); border: 1px solid var(--outline); border-radius: 20px; padding: 28px; } +.feature i { display: grid; place-items: center; width: 46px; height: 46px; border-radius: 14px; background: rgba(245,179,53,.14); color: var(--sun); font-style: normal; font-size: 22px; margin-bottom: 18px; } +.feature b { display: block; color: var(--cream); font: 700 19px Sora, sans-serif; margin-bottom: 8px; } +.feature p { margin: 0; color: var(--muted); font-size: 16px; } +@media (max-width: 860px) { .features { grid-template-columns: 1fr 1fr; } } +@media (max-width: 560px) { .features { grid-template-columns: 1fr; } } + +/* Story */ +.story { display: grid; grid-template-columns: 1fr 1.1fr; gap: 56px; align-items: center; } +.story img { border-radius: 24px; width: 100%; aspect-ratio: 4 / 5; object-fit: cover; } +.story h2 { margin-bottom: 20px; } +.story p { margin: 0 0 16px; } +@media (max-width: 860px) { .story { grid-template-columns: 1fr; } .story img { aspect-ratio: 16 / 10; } } + +/* Places */ +.places { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; max-width: 860px; margin: 0 auto; } +.places span { padding: 8px 18px; border-radius: 999px; border: 1px solid var(--outline); background: var(--surface); color: var(--cream); font-weight: 600; font-size: 15px; } +.places span:nth-child(-n+4) { border-color: rgba(127,168,87,.6); color: var(--tea); } + +/* Creators */ +.creator-band { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 24px; padding: 40px 44px; border-radius: 24px; border: 1px dashed rgba(245,179,53,.5); background: rgba(245,179,53,.05); } +.creator-band > div { max-width: 640px; } +.creator-band h2 { font-size: clamp(24px, 3vw, 34px); margin-bottom: 10px; } +.creator-band p { margin: 0; } + +/* FAQ */ +.faq { max-width: 820px; } +.faq details { border-bottom: 1px solid var(--outline); } +.faq summary { cursor: pointer; list-style: none; padding: 22px 40px 22px 0; position: relative; color: var(--cream); font: 600 19px Sora, sans-serif; } +.faq summary::-webkit-details-marker { display: none; } +.faq summary::after { content: "+"; position: absolute; right: 4px; top: 50%; transform: translateY(-50%); color: var(--sun); font-size: 26px; font-weight: 400; } +.faq details[open] summary::after { content: "\2212"; } +.faq details p { margin: 0 0 22px; color: var(--on-photo); } + +/* Download */ +.app-band { border-radius: 28px; padding: 56px; background: radial-gradient(circle at 85% 20%, rgba(245,179,53,.18), transparent 50%), linear-gradient(120deg, var(--emblem), var(--surface)); display: grid; grid-template-columns: auto 1fr; gap: 36px; align-items: center; } +.app-band .icon { width: 128px; height: 128px; border-radius: 30px; box-shadow: 0 14px 40px rgba(0,0,0,.4); } +.app-band p { margin: 10px 0 24px; font-size: 19px; } +@media (max-width: 700px) { .app-band { grid-template-columns: 1fr; padding: 32px; } .app-band .icon { width: 96px; height: 96px; } } + +/* Footer */ +footer { margin-top: 96px; border-top: 1px solid var(--outline); padding: 36px 0 44px; font-size: 14px; color: var(--muted); } +footer .wrap { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px 28px; } +footer .brand { font-size: 16px; } +footer .brand img { width: 28px; height: 28px; border-radius: 7px; } +footer nav { display: flex; flex-wrap: wrap; gap: 20px; } +footer nav a { color: var(--on-photo); text-decoration: none; } +footer nav a:hover { color: var(--sun); } +footer .copy { width: 100%; } diff --git a/site/app.js b/site/app.js new file mode 100644 index 0000000..8a1098b --- /dev/null +++ b/site/app.js @@ -0,0 +1,161 @@ +// 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(); +})(); diff --git a/site/favicon.png b/site/favicon.png new file mode 100644 index 0000000..e73e349 Binary files /dev/null and b/site/favicon.png differ diff --git a/site/img/apple-touch-icon.png b/site/img/apple-touch-icon.png new file mode 100644 index 0000000..46373f8 Binary files /dev/null and b/site/img/apple-touch-icon.png differ diff --git a/site/img/icon-256.png b/site/img/icon-256.png new file mode 100644 index 0000000..b2036e1 Binary files /dev/null and b/site/img/icon-256.png differ diff --git a/site/img/nilgiris_tea.jpg b/site/img/nilgiris_tea.jpg new file mode 100644 index 0000000..76341c2 Binary files /dev/null and b/site/img/nilgiris_tea.jpg differ diff --git a/site/img/nilgiris_valley.jpg b/site/img/nilgiris_valley.jpg new file mode 100644 index 0000000..7d5e89b Binary files /dev/null and b/site/img/nilgiris_valley.jpg differ diff --git a/site/img/nilgiris_village.jpg b/site/img/nilgiris_village.jpg new file mode 100644 index 0000000..f77aefe Binary files /dev/null and b/site/img/nilgiris_village.jpg differ diff --git a/site/img/og.jpg b/site/img/og.jpg new file mode 100644 index 0000000..36d78c8 Binary files /dev/null and b/site/img/og.jpg differ diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..c2ccee1 --- /dev/null +++ b/site/index.html @@ -0,0 +1,253 @@ + + + + + +Nanga Kathai: Badaga Songs App, Free with No Ads + + + + + + + + + + + + + + + + + + + + +
    +
    + Nanga Kathai + +
    +
    + +
    +
    + +
    +
    +

    The Badaga music app

    +

    Songs of the Nilgiris, in your pocket

    +

    Nanga Kathai brings Badaga folk, wedding, festival and devotional songs together in one free app. No ads. Listen online or offline, wherever you are.

    + +
    180+ songs18 albums7 occasions0 ads
    +
    + +
    +
    +
    +

    Now playing

    +
    + Badaga songs + Nanga Kathai +
    +
    0:000:00
    +
    + + + +
    +
    +
    +
    +
    + +
    +
    +
    +

    Hear it now

    +

    A taste of what's inside

    +

    A few songs from the collection. Press play right here, then get the app for every song, offline downloads and your own playlists.

    + Get the full collection → +
    +
      +
    • +
    +
    +
    + +
    +
    +
    +

    For every moment

    +

    The songs that mark a Badaga life

    +
    +
    +
    HuttuSongs for a birth and the naming day.
    +
    HabbaFestival songs, from the Hethai Habba to harvest.
    +
    MadhuvaeWedding songs sung by family and friends.
    +
    DevotionalSongs for Hethai and the temples of the hills.
    +
    FolkTraditional melodies from the hattis and tea fields.
    +
    SaavuSongs of remembrance for those we have lost.
    +
    InstrumentalFlute, drums and the sounds of celebration.
    +
    +
    +
    + +
    +
    +
    +

    Why Nanga Kathai

    +

    Made for Badaga listeners

    +
    +
    +
    ✦Free, with no ads

    Every song is free. No ads interrupting a wedding song, now or ever.

    +
    ↧Listen offline

    Download songs and play them anywhere, even with no signal on the ghat road or a flight to Dubai.

    +
    ⚲Find any song

    Search in English or Tamil script. Spelling is forgiving, because Badaga words are written many ways.

    +
    ♫Your own playlists

    Like songs and build playlists for the next function, festival or long drive.

    +
    ◉Plays in the background

    Lock-screen controls, Bluetooth speakers and car audio all work.

    +
    ❤Respecting singers

    Every song can be claimed by its singer or family, for credit or removal.

    +
    +
    +
    + +
    +
    + A Badaga village in the Nilgiris hills +
    +

    Our story

    +

    Nanga Kathai means "our story"

    +

    For generations the Badagas of the Nilgiris have sung their stories: at weddings and naming days, at the Hethai Habba, in the tea fields, and in remembrance. Many of those songs survive only on old cassettes in family homes.

    +

    Nanga Kathai gathers them in one place, so Badaga families in the hattis, in Coimbatore and Bengaluru, and across the Gulf, Canada and the US can hear them any time and pass them on to their children.

    +

    It is built by Metatroncube Software Solutions, founded by a Badaga, as a way of giving back to the community.

    +
    +
    +
    + +
    +
    +
    +

    Wherever you are

    +

    From the Nilgiris to the world

    +

    Badaga families now live far from the hills. Nanga Kathai keeps home a tap away.

    +
    +
    + OotyKotagiriCoonoorGudalurCoimbatoreBengaluruChennai + DubaiAbu DhabiDohaRiyadhKuwaitMuscat + TorontoVancouverNew JerseyCaliforniaLondonSingaporeSydney +
    +
    +
    + +
    +
    +
    +
    +

    Coming soon

    +

    Are you a Badaga singer?

    +

    Creator accounts are on the way. You'll be able to publish your own songs and receive support directly from listeners around the world.

    +
    + Tell me when it opens +
    +
    +
    + +
    +
    +

    Questions

    Frequently asked

    +
    Is Nanga Kathai free?

    Yes. Every song is free to listen to and there are no ads. If you'd like to, you can send a one-time gift in the app to support its development, but it unlocks nothing extra.

    +
    Can I listen to Badaga songs offline?

    Yes. Download songs in the app and play them without an internet connection.

    +
    Can I use it outside India?

    Yes. Nanga Kathai works worldwide, including the Gulf countries, Canada, the US, the UK and Australia.

    +
    One of the songs is mine or my family's. What can I do?

    Tap "Is this your song?" in the app, or email support@nkbmusic.com. We'll credit you properly, or remove the song if you prefer.

    +
    I am a Badaga singer. Can I publish my songs?

    Creator accounts are coming in a future update. Email us and we'll tell you when it opens.

    +
    +
    + +
    +
    +
    + Nanga Kathai app icon +
    +

    Take Badaga music with you

    +

    Free on Android and iPhone. No ads, ever.

    + +
    +
    +
    +
    +
    + + + + + + diff --git a/site/robots.txt b/site/robots.txt new file mode 100644 index 0000000..5a213cc --- /dev/null +++ b/site/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://nkbmusic.com/sitemap.xml diff --git a/site/sitemap.xml b/site/sitemap.xml new file mode 100644 index 0000000..8124fb7 --- /dev/null +++ b/site/sitemap.xml @@ -0,0 +1,7 @@ + + + https://nkbmusic.com/2026-10-311.0 + https://nkbmusic.com/privacy2026-10-310.3 + https://nkbmusic.com/terms2026-10-310.3 + https://nkbmusic.com/delete-account2026-10-310.3 + diff --git a/tools/build_site.py b/tools/build_site.py new file mode 100644 index 0000000..0fe97d1 --- /dev/null +++ b/tools/build_site.py @@ -0,0 +1,48 @@ +"""Prepare the nkbmusic.com website in site/ for upload. + +Writes site/config.js (backend URL and public key, from env.json) and site/sitemap.xml +(home and legal pages). + + python tools/build_site.py [--play-url URL] [--app-store-url URL] +""" +import argparse +import datetime +import json +import pathlib + +ROOT = pathlib.Path(__file__).resolve().parent.parent +SITE = ROOT / 'site' +BASE = 'https://nkbmusic.com' + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--play-url', default='', help='Google Play link, once the app is live') + ap.add_argument('--app-store-url', default='', help='App Store link, once the app is live') + args = ap.parse_args() + + env = json.loads((ROOT / 'env.json').read_text(encoding='utf-8')) + config = { + 'supabaseUrl': env['SUPABASE_URL'], + # The publishable (anon) key is public by design; row-level security protects the data. + 'supabaseKey': env['SUPABASE_PUBLISHABLE_KEY'], + 'mediaBaseUrl': env['MEDIA_BASE_URL'], + 'playUrl': args.play_url, + 'appStoreUrl': args.app_store_url, + } + (SITE / 'config.js').write_text(f'window.NK_CONFIG = {json.dumps(config, indent=2)};\n', encoding='utf-8') + + today = datetime.date.today().isoformat() + urls = [(f'{BASE}/', '1.0'), (f'{BASE}/privacy', '0.3'), (f'{BASE}/terms', '0.3'), + (f'{BASE}/delete-account', '0.3')] + body = '\n'.join(f' {u}{today}{p}' + for u, p in urls) + (SITE / 'sitemap.xml').write_text( + '\n' + f'\n{body}\n\n', encoding='utf-8') + (SITE / 'robots.txt').write_text(f'User-agent: *\nAllow: /\n\nSitemap: {BASE}/sitemap.xml\n', encoding='utf-8') + print(f'config.js, sitemap.xml ({len(urls)} URLs) and robots.txt written to {SITE}') + + +if __name__ == '__main__': + main()