nkbmusic.com marketing site with live song samples from the backend
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1
.gitignore
vendored
@ -53,3 +53,4 @@ env.json
|
||||
# Song collection and import manifest (large, private)
|
||||
/songs/
|
||||
/songs_manifest.csv
|
||||
site/config.js
|
||||
|
||||
186
site/app.css
Normal file
@ -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%; }
|
||||
161
site/app.js
Normal file
@ -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: '<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: "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();
|
||||
})();
|
||||
BIN
site/favicon.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
site/img/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
site/img/icon-256.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
site/img/nilgiris_tea.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
site/img/nilgiris_valley.jpg
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
site/img/nilgiris_village.jpg
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
site/img/og.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
253
site/index.html
Normal file
@ -0,0 +1,253 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nanga Kathai: Badaga Songs App, Free with No Ads</title>
|
||||
<meta name="description" content="Nanga Kathai is the free Badaga songs app: folk, wedding, festival and devotional music from the Nilgiris hattis of Ooty, Kotagiri and Coonoor. No ads, listen offline, for Android and iPhone.">
|
||||
<link rel="canonical" href="https://nkbmusic.com/">
|
||||
<meta name="theme-color" content="#0B1A14">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Nanga Kathai">
|
||||
<meta property="og:title" content="Nanga Kathai: Badaga songs from the Nilgiris">
|
||||
<meta property="og:description" content="The free Badaga songs app. Folk, wedding, festival and devotional music. No ads, listen offline.">
|
||||
<meta property="og:url" content="https://nkbmusic.com/">
|
||||
<meta property="og:image" content="https://nkbmusic.com/img/og.jpg">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preconnect" href="https://app.nkbmusic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Sora:wght@600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"name": "Nanga Kathai",
|
||||
"url": "https://nkbmusic.com/",
|
||||
"inLanguage": "en"
|
||||
},
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "Metatroncube Software Solutions",
|
||||
"url": "https://nkbmusic.com/",
|
||||
"logo": "https://nkbmusic.com/img/icon-256.png",
|
||||
"email": "support@nkbmusic.com"
|
||||
},
|
||||
{
|
||||
"@type": "MobileApplication",
|
||||
"name": "Nanga Kathai: Badaga Songs",
|
||||
"operatingSystem": "Android, iOS",
|
||||
"applicationCategory": "MusicApplication",
|
||||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
|
||||
"description": "Badaga songs and Nilgiris folk music. Free, no ads, listen offline."
|
||||
},
|
||||
{
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{ "@type": "Question", "name": "Is Nanga Kathai free?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Yes. Every song is free to listen to and there are no ads. You can choose to send a one-time gift in the app to support its development, but it unlocks nothing extra." } },
|
||||
{ "@type": "Question", "name": "Can I listen to Badaga songs offline?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Yes. Download songs in the app and play them without an internet connection." } },
|
||||
{ "@type": "Question", "name": "Can I use Nanga Kathai outside India?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Yes. Nanga Kathai works worldwide, including the Gulf countries, Canada, the US, the UK and Australia." } },
|
||||
{ "@type": "Question", "name": "One of the songs is mine or my family's. What can I do?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Tap 'Is this your song?' in the app, or email support@nkbmusic.com. We will credit you properly, or remove the song if you prefer." } },
|
||||
{ "@type": "Question", "name": "I am a Badaga singer. Can I publish my songs?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Creator accounts are coming in a future update, so singers can publish their own songs and receive support from listeners. Email support@nkbmusic.com to be told when it opens." } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="top">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="/"><img src="/img/icon-256.png" alt="" width="36" height="36">Nanga Kathai</a>
|
||||
<nav aria-label="Main">
|
||||
<a href="#listen" class="opt">Listen</a>
|
||||
<a href="#features" class="opt">Features</a>
|
||||
<a href="#story" class="opt">Our story</a>
|
||||
<a href="#faq" class="opt">FAQ</a>
|
||||
<a href="#download" class="btn primary sm">Get the app</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<img class="hero-bg" src="/img/nilgiris_valley.jpg" alt="" fetchpriority="high">
|
||||
<div class="wrap hero-grid">
|
||||
<div>
|
||||
<p class="eyebrow">The Badaga music app</p>
|
||||
<h1>Songs of the Nilgiris, <em>in your pocket</em></h1>
|
||||
<p class="lead">Nanga Kathai brings Badaga folk, wedding, festival and devotional songs together in one free app. No ads. Listen online or offline, wherever you are.</p>
|
||||
<div class="actions">
|
||||
<a class="store-badge soon" data-store="playUrl">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 2.8v18.4c0 .4.4.7.8.5l10.6-9.2a.6.6 0 0 0 0-.9L4.8 2.3c-.4-.2-.8.1-.8.5zm12.6 7.6 2.9 1.6c.6.3.6 1.1 0 1.4l-2.9 1.6-2.4-2.3z"/></svg>
|
||||
<span><small>Coming soon to</small>Google Play</span></a>
|
||||
<a class="store-badge soon" data-store="appStoreUrl">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.4 12.6c0-2.4 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9s-2-.9-3.3-.9c-1.7 0-3.3 1-4.2 2.5-1.8 3.1-.5 7.7 1.3 10.2.9 1.2 1.9 2.6 3.2 2.5 1.3-.1 1.8-.8 3.3-.8s2 .8 3.3.8c1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9-.1 0-2.8-1-2.8-4.1zM14 5.3c.7-.8 1.2-2 1-3.1-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.6 2.9-1.4z"/></svg>
|
||||
<span><small>Coming soon to the</small>App Store</span></a>
|
||||
</div>
|
||||
<div class="facts"><span><b id="count-songs">180+</b> songs</span><span><b id="count-albums">18</b> albums</span><span><b>7</b> occasions</span><span><b>0</b> ads</span></div>
|
||||
</div>
|
||||
|
||||
<div class="phone" aria-label="Song player preview">
|
||||
<div class="phone-screen">
|
||||
<div class="phone-photo"><img src="/img/nilgiris_tea.jpg" alt=""></div>
|
||||
<p class="phone-label">Now playing</p>
|
||||
<div id="phone-art" class="phone-art"></div>
|
||||
<b id="phone-title">Badaga songs</b>
|
||||
<span id="phone-credit">Nanga Kathai</span>
|
||||
<div class="phone-progress"><i id="phone-bar"></i></div>
|
||||
<div class="phone-times"><span id="phone-time">0:00</span><span id="phone-total">0:00</span></div>
|
||||
<div class="phone-controls">
|
||||
<button id="phone-prev" aria-label="Previous"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 5h2v14H6zM20 6.2v11.6a1 1 0 0 1-1.5.86L10 13.2a1.4 1.4 0 0 1 0-2.4l8.5-5.46A1 1 0 0 1 20 6.2z"/></svg></button>
|
||||
<button id="phone-play" class="big" aria-label="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></button>
|
||||
<button id="phone-next" aria-label="Next"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M16 5h2v14h-2zM4 6.2v11.6a1 1 0 0 0 1.5.86L14 13.2a1.4 1.4 0 0 0 0-2.4L5.5 5.34A1 1 0 0 0 4 6.2z"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="listen">
|
||||
<div class="wrap listen-grid">
|
||||
<div>
|
||||
<p class="eyebrow">Hear it now</p>
|
||||
<h2>A taste of what's inside</h2>
|
||||
<p class="muted">A few songs from the collection. Press play right here, then get the app for every song, offline downloads and your own playlists.</p>
|
||||
<a class="link" href="#download">Get the full collection →</a>
|
||||
</div>
|
||||
<ul class="samples" id="samples">
|
||||
<li class="skeleton"></li><li class="skeleton"></li><li class="skeleton"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="occasions">
|
||||
<div class="wrap">
|
||||
<div class="center-head">
|
||||
<p class="eyebrow">For every moment</p>
|
||||
<h2>The songs that mark a Badaga life</h2>
|
||||
</div>
|
||||
<div class="tiles">
|
||||
<div class="tile" style="--c:#3B6E8F"><b>Huttu</b><span>Songs for a birth and the naming day.</span></div>
|
||||
<div class="tile" style="--c:#B5541E"><b>Habba</b><span>Festival songs, from the Hethai Habba to harvest.</span></div>
|
||||
<div class="tile" style="--c:#9C2F4A"><b>Madhuvae</b><span>Wedding songs sung by family and friends.</span></div>
|
||||
<div class="tile" style="--c:#6B4FA0"><b>Devotional</b><span>Songs for Hethai and the temples of the hills.</span></div>
|
||||
<div class="tile" style="--c:#2E7D5B"><b>Folk</b><span>Traditional melodies from the hattis and tea fields.</span></div>
|
||||
<div class="tile" style="--c:#4A4F57"><b>Saavu</b><span>Songs of remembrance for those we have lost.</span></div>
|
||||
<div class="tile" style="--c:#8A5A2B"><b>Instrumental</b><span>Flute, drums and the sounds of celebration.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features">
|
||||
<div class="wrap">
|
||||
<div class="center-head">
|
||||
<p class="eyebrow">Why Nanga Kathai</p>
|
||||
<h2>Made for Badaga listeners</h2>
|
||||
</div>
|
||||
<div class="features">
|
||||
<div class="feature"><i>✦</i><b>Free, with no ads</b><p>Every song is free. No ads interrupting a wedding song, now or ever.</p></div>
|
||||
<div class="feature"><i>↧</i><b>Listen offline</b><p>Download songs and play them anywhere, even with no signal on the ghat road or a flight to Dubai.</p></div>
|
||||
<div class="feature"><i>⚲</i><b>Find any song</b><p>Search in English or Tamil script. Spelling is forgiving, because Badaga words are written many ways.</p></div>
|
||||
<div class="feature"><i>♫</i><b>Your own playlists</b><p>Like songs and build playlists for the next function, festival or long drive.</p></div>
|
||||
<div class="feature"><i>◉</i><b>Plays in the background</b><p>Lock-screen controls, Bluetooth speakers and car audio all work.</p></div>
|
||||
<div class="feature"><i>❤</i><b>Respecting singers</b><p>Every song can be claimed by its singer or family, for credit or removal.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="story" class="story-band">
|
||||
<div class="wrap story">
|
||||
<img src="/img/nilgiris_village.jpg" alt="A Badaga village in the Nilgiris hills" loading="lazy">
|
||||
<div>
|
||||
<p class="eyebrow">Our story</p>
|
||||
<h2>Nanga Kathai means "our story"</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>It is built by Metatroncube Software Solutions, founded by a Badaga, as a way of giving back to the community.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="world">
|
||||
<div class="wrap">
|
||||
<div class="center-head">
|
||||
<p class="eyebrow">Wherever you are</p>
|
||||
<h2>From the Nilgiris to the world</h2>
|
||||
<p class="muted">Badaga families now live far from the hills. Nanga Kathai keeps home a tap away.</p>
|
||||
</div>
|
||||
<div class="places">
|
||||
<span>Ooty</span><span>Kotagiri</span><span>Coonoor</span><span>Gudalur</span><span>Coimbatore</span><span>Bengaluru</span><span>Chennai</span>
|
||||
<span>Dubai</span><span>Abu Dhabi</span><span>Doha</span><span>Riyadh</span><span>Kuwait</span><span>Muscat</span>
|
||||
<span>Toronto</span><span>Vancouver</span><span>New Jersey</span><span>California</span><span>London</span><span>Singapore</span><span>Sydney</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="creators">
|
||||
<div class="wrap">
|
||||
<div class="creator-band">
|
||||
<div>
|
||||
<p class="eyebrow">Coming soon</p>
|
||||
<h2>Are you a Badaga singer?</h2>
|
||||
<p>Creator accounts are on the way. You'll be able to publish your own songs and receive support directly from listeners around the world.</p>
|
||||
</div>
|
||||
<a class="btn ghost" href="mailto:support@nkbmusic.com?subject=Tell%20me%20when%20creator%20accounts%20open">Tell me when it opens</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="faq">
|
||||
<div class="wrap faq">
|
||||
<div class="center-head"><p class="eyebrow">Questions</p><h2>Frequently asked</h2></div>
|
||||
<details><summary>Is Nanga Kathai free?</summary><p>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.</p></details>
|
||||
<details><summary>Can I listen to Badaga songs offline?</summary><p>Yes. Download songs in the app and play them without an internet connection.</p></details>
|
||||
<details><summary>Can I use it outside India?</summary><p>Yes. Nanga Kathai works worldwide, including the Gulf countries, Canada, the US, the UK and Australia.</p></details>
|
||||
<details><summary>One of the songs is mine or my family's. What can I do?</summary><p>Tap "Is this your song?" in the app, or email <a href="mailto:support@nkbmusic.com">support@nkbmusic.com</a>. We'll credit you properly, or remove the song if you prefer.</p></details>
|
||||
<details><summary>I am a Badaga singer. Can I publish my songs?</summary><p>Creator accounts are coming in a future update. <a href="mailto:support@nkbmusic.com?subject=Tell%20me%20when%20creator%20accounts%20open">Email us</a> and we'll tell you when it opens.</p></details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="download">
|
||||
<div class="wrap">
|
||||
<div class="app-band">
|
||||
<img class="icon" src="/img/icon-256.png" alt="Nanga Kathai app icon" loading="lazy">
|
||||
<div>
|
||||
<h2>Take Badaga music with you</h2>
|
||||
<p>Free on Android and iPhone. No ads, ever.</p>
|
||||
<div class="actions">
|
||||
<a class="store-badge soon" data-store="playUrl"><span><small>Coming soon to</small>Google Play</span></a>
|
||||
<a class="store-badge soon" data-store="appStoreUrl"><span><small>Coming soon to the</small>App Store</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<a class="brand" href="/"><img src="/img/icon-256.png" alt="" width="28" height="28">Nanga Kathai</a>
|
||||
<nav aria-label="Footer">
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
<a href="/terms">Terms</a>
|
||||
<a href="/delete-account">Delete account</a>
|
||||
<a href="mailto:support@nkbmusic.com">support@nkbmusic.com</a>
|
||||
</nav>
|
||||
<span class="copy">© <span id="year">2026</span> Metatroncube Software Solutions</span>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="/config.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
4
site/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://nkbmusic.com/sitemap.xml
|
||||
7
site/sitemap.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url><loc>https://nkbmusic.com/</loc><lastmod>2026-10-31</lastmod><priority>1.0</priority></url>
|
||||
<url><loc>https://nkbmusic.com/privacy</loc><lastmod>2026-10-31</lastmod><priority>0.3</priority></url>
|
||||
<url><loc>https://nkbmusic.com/terms</loc><lastmod>2026-10-31</lastmod><priority>0.3</priority></url>
|
||||
<url><loc>https://nkbmusic.com/delete-account</loc><lastmod>2026-10-31</lastmod><priority>0.3</priority></url>
|
||||
</urlset>
|
||||
48
tools/build_site.py
Normal file
@ -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' <url><loc>{u}</loc><lastmod>{today}</lastmod><priority>{p}</priority></url>'
|
||||
for u, p in urls)
|
||||
(SITE / 'sitemap.xml').write_text(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
f'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n{body}\n</urlset>\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()
|
||||