Static reader: single-file browser reader over the divan-data static API
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5063608d5d
commit
0ce63b2bc6
11
README.md
11
README.md
@ -16,6 +16,17 @@ Divan is a standalone project built on a fork of [GanjoorService](https://github
|
|||||||
- **Links:** `ganjoor.net` links to the site's own pages are now relative. Links to Divan's other services (blog, audio, etc.) are left as they are.
|
- **Links:** `ganjoor.net` links to the site's own pages are now relative. Links to Divan's other services (blog, audio, etc.) are left as they are.
|
||||||
- **Locale:** `ur-PK`.
|
- **Locale:** `ur-PK`.
|
||||||
|
|
||||||
|
## Static reader (no server)
|
||||||
|
|
||||||
|
`reader/index.html` is a single-file reader: poets, intros, books and poems, with Nastaliq/Naskh switching. It reads the [divan-data](https://github.com/anas-rashid/divan-data) static API directly in the browser, so it needs no API, database or build step.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# against the public CDN: just open reader/index.html in a browser, or host it anywhere (e.g. GitHub Pages)
|
||||||
|
# against a local divan-data checkout:
|
||||||
|
mkdir -p www && ln -s "$PWD/reader/index.html" www/ && ln -s /path/to/divan-data www/data
|
||||||
|
python3 -m http.server 5300 -d www # open http://localhost:5300/?data=data/
|
||||||
|
```
|
||||||
|
|
||||||
## Deploy (Ubuntu/Debian x86-64, e.g. Vultr)
|
## Deploy (Ubuntu/Debian x86-64, e.g. Vultr)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
151
reader/index.html
Normal file
151
reader/index.html
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ur-PK" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>دیوان · قاری</title>
|
||||||
|
<meta name="description" content="دیوان کا سادہ قاری: اردو کی کلاسیکی شاعری اور نثر، براہِ راست divan-data سے۔">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400..700&family=Noto+Naskh+Arabic:wght@400..700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Divan static reader: no server, reads the divan-data static API (ganjoor-data layout). */
|
||||||
|
:root {
|
||||||
|
--bg: #faf7f0; --card: #ffffff; --ink: #2b2620; --muted: #7a6f60; --line: #e6dfd2; --accent: #8a5a19;
|
||||||
|
--font: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', serif; --lh: 2.5;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root { --bg: #17140f; --card: #211d17; --ink: #ece4d6; --muted: #a89c88; --line: #3a3328; --accent: #d9a55a; }
|
||||||
|
}
|
||||||
|
html.naskh { --font: 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', serif; --lh: 2; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--font); line-height: var(--lh); }
|
||||||
|
a { color: var(--accent); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
header { display: flex; align-items: center; gap: 16px; padding: 8px 16px; border-bottom: 1px solid var(--line); background: var(--card); position: sticky; top: 0; z-index: 1; }
|
||||||
|
header h1 { margin: 0; font-size: 1.8rem; font-weight: 700; }
|
||||||
|
header .grow { flex: 1; }
|
||||||
|
button { font: inherit; background: none; color: var(--ink); border: 1px solid var(--line); border-radius: 20px; padding: 0 14px; cursor: pointer; }
|
||||||
|
main { max-width: 860px; margin: 0 auto; padding: 16px; }
|
||||||
|
.crumbs { color: var(--muted); font-size: .95rem; }
|
||||||
|
.crumbs a + a::before, .crumbs a + span::before { content: " « "; color: var(--muted); }
|
||||||
|
input[type=search] { width: 100%; font: inherit; padding: 4px 14px; border: 1px solid var(--line); border-radius: 22px; background: var(--card); color: var(--ink); }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; margin-top: 16px; }
|
||||||
|
.grid a, .list a { display: block; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 4px 14px; }
|
||||||
|
.list { display: grid; gap: 6px; margin-top: 12px; }
|
||||||
|
.years { color: var(--muted); font-size: .85rem; }
|
||||||
|
.intro { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 8px 16px; white-space: pre-line; }
|
||||||
|
.poem { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin-top: 12px; }
|
||||||
|
.b { display: grid; grid-template-columns: 1fr 1fr; gap: 0 32px; margin: 4px 0 12px; }
|
||||||
|
.b p { margin: 0; }
|
||||||
|
.b p:last-child { text-align: left; }
|
||||||
|
.s { text-align: center; margin: 0; }
|
||||||
|
.para { text-align: justify; margin: 0 0 12px; }
|
||||||
|
.source { color: var(--muted); font-size: .85rem; margin-top: 16px; }
|
||||||
|
@media (max-width: 600px) { .b { grid-template-columns: 1fr; } .b p:last-child { text-align: right; padding-right: 32px; } }
|
||||||
|
.msg { color: var(--muted); text-align: center; padding: 32px; }
|
||||||
|
</style>
|
||||||
|
<script>try { if (localStorage.getItem('divan-font') === 'naskh') document.documentElement.classList.add('naskh'); } catch (e) {}</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1><a href="#/">دیوان</a></h1>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<button id="font" type="button" title="خط بدلیں">نستعلیق</button>
|
||||||
|
</header>
|
||||||
|
<main id="app"><p class="msg">لوڈ ہو رہا ہے…</p></main>
|
||||||
|
<script>
|
||||||
|
// Data source: ?data=<base url> (e.g. ?data=data/ when served next to a divan-data checkout), else the public CDN.
|
||||||
|
var DATA = new URLSearchParams(location.search).get('data') || 'https://cdn.jsdelivr.net/gh/anas-rashid/divan-data@main/';
|
||||||
|
var app = document.getElementById('app');
|
||||||
|
var cache = {};
|
||||||
|
|
||||||
|
function get(path) {
|
||||||
|
if (!cache[path]) cache[path] = fetch(DATA + path).then(function (r) { if (!r.ok) throw new Error(r.status + ' ' + path); return r.json(); });
|
||||||
|
return cache[path];
|
||||||
|
}
|
||||||
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
||||||
|
function ud(n) { return String(n).replace(/[0-9]/g, function (d) { return '۰۱۲۳۴۵۶۷۸۹'[d]; }); }
|
||||||
|
function years(p) {
|
||||||
|
var b = p.ValidBirthDate ? ud(p.BirthYearInLHijri) : '', d = p.ValidDeathDate ? ud(p.DeathYearInLHijri) : '';
|
||||||
|
return b || d ? (b || '؟') + ' – ' + (d || '؟') + ' ھ' : '';
|
||||||
|
}
|
||||||
|
function link(url, text) { return '<a href="#' + esc(url) + '">' + esc(text) + '</a>'; }
|
||||||
|
|
||||||
|
function crumbs(fullTitle, fullUrl) {
|
||||||
|
// FullTitle "poet » cat » ... » poem" lines up with FullUrl segments "/p1/cat/.../sh9"
|
||||||
|
var titles = fullTitle.split(' » '), segs = fullUrl.split('/').filter(Boolean), out = [link('/', 'سرورق')];
|
||||||
|
titles.forEach(function (t, i) {
|
||||||
|
var u = '/' + segs.slice(0, i + 1).join('/');
|
||||||
|
out.push(i < titles.length - 1 ? link(u, t) : '<span>' + esc(t) + '</span>');
|
||||||
|
});
|
||||||
|
return '<div class="crumbs">' + out.join('') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function home() {
|
||||||
|
return get('manifest.json').then(function (m) {
|
||||||
|
app.innerHTML = '<input type="search" id="q" placeholder="شاعر تلاش کریں (' + ud(m.PoetsCount) + ' شعرا، ' + ud(m.PoemsCount) + ' تخلیقات)">' +
|
||||||
|
'<div class="grid" id="poets"></div>';
|
||||||
|
var box = document.getElementById('poets');
|
||||||
|
function render(q) {
|
||||||
|
box.innerHTML = m.Poets.filter(function (p) { return !q || p.Nickname.indexOf(q) >= 0; })
|
||||||
|
.map(function (p) { return link(p.FullUrl, p.Nickname); }).join('');
|
||||||
|
}
|
||||||
|
render('');
|
||||||
|
document.getElementById('q').oninput = function (e) { render(e.target.value.trim()); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function category(url) {
|
||||||
|
var slug = url.split('/')[1];
|
||||||
|
return Promise.all([get('poets' + url + '/_cat.json'), get('poets/' + slug + '/poet.json')]).then(function (r) {
|
||||||
|
var c = r[0], poet = r[1], isRoot = c.ParentId == null;
|
||||||
|
var html = isRoot
|
||||||
|
? '<div class="crumbs">' + link('/', 'سرورق') + '<span>' + esc(poet.Nickname) + '</span></div><h2>' + esc(poet.Name) + ' <span class="years">' + years(poet) + '</span></h2>' +
|
||||||
|
(poet.Description ? '<div class="intro">' + esc(poet.Description) + '</div>' : '')
|
||||||
|
: '<h2>' + esc(c.Title) + '</h2>';
|
||||||
|
if (!isRoot) {
|
||||||
|
// breadcrumb for a sub-category: walk up via FullUrl
|
||||||
|
var segs = url.split('/').filter(Boolean);
|
||||||
|
html = '<div class="crumbs">' + link('/', 'سرورق') + link('/' + segs[0], poet.Nickname) +
|
||||||
|
segs.slice(1).map(function (s, i) { return i < segs.length - 2 ? link('/' + segs.slice(0, i + 2).join('/'), '…') : '<span>' + esc(c.Title) + '</span>'; }).join('') + '</div>' + html;
|
||||||
|
}
|
||||||
|
html += '<div class="list">' + c.ChildCats.map(function (x) { return link(x.FullUrl, '📚 ' + x.Title); }).join('') +
|
||||||
|
c.Poems.map(function (x) { return link(x.FullUrl, x.Title); }).join('') + '</div>';
|
||||||
|
app.innerHTML = html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function poem(url) {
|
||||||
|
return get('poets' + url + '.json').then(function (p) {
|
||||||
|
var html = crumbs(p.FullTitle, p.FullUrl) + '<h2>' + esc(p.Title) + '</h2><div class="poem">', pair = [];
|
||||||
|
p.Verses.forEach(function (v) {
|
||||||
|
if (v.Position === 'Right') pair = [v.Text];
|
||||||
|
else if (v.Position === 'Left') { html += '<div class="b"><p>' + esc(pair[0] || '') + '</p><p>' + esc(v.Text) + '</p></div>'; pair = []; }
|
||||||
|
else if (v.Position === 'Paragraph') html += '<p class="para">' + esc(v.Text) + '</p>';
|
||||||
|
else html += '<p class="s">' + esc(v.Text) + '</p>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
if (p.SourceUrl) html += '<div class="source">ماخذ: <a href="' + esc(p.SourceUrl) + '" target="_blank" rel="noopener">' + esc(p.SourceName || 'ویکی ماخذ') + '</a></div>';
|
||||||
|
app.innerHTML = html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function route() {
|
||||||
|
var url = decodeURIComponent(location.hash.replace(/^#/, '')) || '/';
|
||||||
|
var view = url === '/' ? home() : /\/sh\d+$/.test(url) ? poem(url) : category(url);
|
||||||
|
view.then(function () { window.scrollTo(0, 0); }).catch(function (e) { app.innerHTML = '<p class="msg">نہیں مل سکا: ' + esc(e.message) + '</p>'; });
|
||||||
|
}
|
||||||
|
window.addEventListener('hashchange', route);
|
||||||
|
route();
|
||||||
|
|
||||||
|
var fontBtn = document.getElementById('font');
|
||||||
|
fontBtn.textContent = document.documentElement.classList.contains('naskh') ? 'نسخ' : 'نستعلیق';
|
||||||
|
fontBtn.onclick = function () {
|
||||||
|
var naskh = document.documentElement.classList.toggle('naskh');
|
||||||
|
try { localStorage.setItem('divan-font', naskh ? 'naskh' : 'nastaliq'); } catch (e) {}
|
||||||
|
fontBtn.textContent = naskh ? 'نسخ' : 'نستعلیق';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user