divan/web/src/pages/search.astro

162 lines
9.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
// Search: poets/writers by name and books/chapters by title (first page), then the content results, paged
import Base from '../layouts/Base.astro';
import Icon from '../components/Icon.astro';
import Seg from '../components/Seg.astro';
import Tags from '../components/Tags.astro';
import { api } from '../lib/api';
import { ud, years, highlighter, parts, excerpt } from '../lib/urdu';
const q = Astro.url.searchParams.get('q')?.trim() ?? '';
// one or more poets/writers to search within (?poet=1&poet=2, or ?poet=1,2)
const poetIds = [...new Set(Astro.url.searchParams.getAll('poet').flatMap((v) => v.split(',')).map(Number).filter((n) => n > 0))];
const pageNo = Math.max(1, Number(Astro.url.searchParams.get('page')) || 1);
const res = q ? await api(`/api/search?q=${encodeURIComponent(q)}&page=${pageNo}${poetIds.length ? `&poet=${poetIds.join(',')}` : ''}`) : null;
// every poet/writer for the name box (382; filtered in the browser)
const allPoets = res ? ((await api('/api/poets')) ?? []).map((p: any) => ({ id: p.id, name: p.nickname })) : [];
const pages = res ? Math.ceil(res.total / res.pageSize) : 0;
const re = highlighter(q);
const hl = (t: string) => parts(t, re).map((s, i): [string, string] => [s, i % 2 ? 'mark' : '']);
const open = (url: string) => `${url}?hl=${encodeURIComponent(q)}`;
const link = (n: number, poets: number[] = poetIds) => `/search?q=${encodeURIComponent(q)}${poets.length ? `&poet=${poets.join(',')}` : ''}${n > 1 ? `&page=${n}` : ''}`;
// author chips: the selected ones first, then the authors the results come from
const chips = res ? [...res.selected.map((p: any) => ({ ...p, n: res.authors.find((a: any) => a.id === p.id)?.n ?? 0 })),
...res.authors.filter((a: any) => !poetIds.includes(a.id))] : [];
// numbered pages: first, last, and two either side of the current one; gaps shown as …
const numbers = (() => {
const keep = new Set([1, pages, pageNo - 2, pageNo - 1, pageNo, pageNo + 1, pageNo + 2].filter((n) => n >= 1 && n <= pages));
const sorted = [...keep].sort((a, b) => a - b), out: (number | null)[] = [];
sorted.forEach((n, i) => { if (i && n - sorted[i - 1] > 1) out.push(null); out.push(n); });
return out;
})();
const from = res ? (pageNo - 1) * res.pageSize + 1 : 0, to = res ? Math.min(res.total, pageNo * res.pageSize) : 0;
---
<Base title={q ? `تلاش: ${q}` : 'تلاش'} q={q}>
{!q && <p class="muted">اوپر کے خانے میں شاعر کا نام، شعر، لفظ یا عبارت لکھیں۔ پوری عبارت کے لیے اسے "…" میں لکھیں۔ کسی ٹیگ والا کلام: <bdi>ٹیگ:عشق</bdi> (نام میں فاصلہ ہو تو <bdi>ٹیگ:"حب الوطنی"</bdi>)۔ <a href="/tags">تمام ٹیگ</a></p>}
{res && (
<>
<h1>تلاش: {q}</h1>
{res.poets.length > 0 && (
<section class="search-group">
<h2>شعرا و ادبا <small class="muted">{ud(res.poets.length)}</small></h2>
<div class="poets">
{res.poets.map((p: any) => (
<div class="poet-result">
<a class="poet-card" href={p.url}><span class="name"><Seg s={hl(p.nickname)} /></span><small>{years(p)}</small></a>
<a class="muted small" href={link(1, [p.id])}>{ud(p.works)} کلام میں «{q}»</a>
</div>
))}
</div>
</section>
)}
{res.books.length > 0 && (
<section class="search-group">
<h2>کتابیں اور ابواب <small class="muted">{ud(res.books.length)}</small></h2>
<ul class="book-results">
{res.books.map((b: any) => (
<li><a href={b.url}><Seg s={hl(b.title)} /></a> · <a class="muted" href={b.poet_url}>{b.poet}</a> <small class="muted">({ud(b.works)})</small></li>
))}
</ul>
</section>
)}
{res.ebooks?.length > 0 && (
<section class="search-group">
<h2>ای بکس <small class="muted">{ud(res.ebooks.length)}</small></h2>
<ul class="book-results">
{res.ebooks.map((b: any) => <li><a href={`/ebook/${b.id}`}><Seg s={hl(b.title)} /></a> · <a class="muted" href={b.poet_url}>{b.poet}</a></li>)}
</ul>
</section>
)}
{res.tags?.length > 0 && (
<section class="search-group">
<h2>ٹیگ <small class="muted">{ud(res.tags.length)}</small></h2>
<Tags tags={res.tags} />
</section>
)}
<section class="search-group">
<h2>کلام <small class="muted">{ud(res.total)}</small></h2>
{chips.length > 0 && (
<form method="get" action="/search" class="author-filter" id="author-filter">
<input type="hidden" name="q" value={q} />
<p class="muted">شاعر یا ادیب کے لحاظ سے محدود کریں:</p>
<div class="author-chips">
{chips.map((a: any, i: number) => (
<label class={`chip${i >= 12 && !poetIds.includes(a.id) ? ' extra' : ''}`}>
<input type="checkbox" name="poet" value={a.id} checked={poetIds.includes(a.id)} />
{a.nickname} <small>{ud(a.n)}</small>
</label>
))}
{chips.length > 12 && <button type="button" class="chip more" onclick="this.parentElement.classList.add('all'); this.remove()">مزید {ud(chips.length - 12)}</button>}
</div>
<div class="author-find in-box-field">
<input id="author-name" data-urdu placeholder="کسی اور شاعر یا ادیب کا نام" autocomplete="off" aria-label="شاعر کا نام" />
<button type="button" class="in-box rom" data-urdu-rom="author-name" aria-pressed="false" title="رومن حروف سے اردو">اب</button>
<button type="button" class="in-box" data-urdu-kb="author-name" aria-pressed="false" title="اردو کی بورڈ" aria-label="اردو کی بورڈ">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10" stroke-linecap="round" stroke-width="2"/></svg>
</button>
<div class="author-suggest" id="author-suggest" hidden></div>
</div>
<p class="filter-actions">
<button type="submit">نتائج محدود کریں</button>
{poetIds.length > 0 && <a href={link(1, [])}>سب شعرا میں تلاش</a>}
</p>
<script type="application/json" id="all-poets" set:html={JSON.stringify(allPoets)} />
</form>
)}
<p class="muted center">
{res.total ? (pages > 1 ? `نتائج ${ud(from)}–${ud(to)} از ${ud(res.total)}` : `${ud(res.total)} نتائج`)
: 'کلام میں کوئی نتیجہ نہیں ملا۔ کم الفاظ یا کلیدی لفظ آزمائیں۔'}
</p>
{res.results.map((r: any) => (
<div class="result">
<a href={open(r.url)}><Seg s={hl(r.title)} /></a> · <a class="muted" href={r.poet_url}>{r.poet}</a>
{r.snippet.length > 0 && (
<a class="snippet" href={open(r.url)}>
{r.prose ? <p><Seg s={hl(excerpt(r.snippet[0], re))} /></p> : r.snippet.map((l: string) => <p><Seg s={hl(l)} /></p>)}
</a>
)}
</div>
))}
{pages > 1 && (
<nav class="pager" aria-label="صفحات">
{pageNo > 1 ? <a class="icon-btn" href={link(pageNo - 1)} rel="prev" title="پچھلا" aria-label="پچھلا"><Icon name="prev" /></a> : <span />}
<span class="pages">
{numbers.map((n) => n === null ? <span class="gap">…</span>
: n === pageNo ? <span class="current" aria-current="page">{ud(n)}</span>
: <a href={link(n)}>{ud(n)}</a>)}
</span>
{pageNo < pages ? <a class="icon-btn" href={link(pageNo + 1)} rel="next" title="اگلا" aria-label="اگلا"><Icon name="next" /></a> : <span />}
</nav>
)}
</section>
</>
)}
<script is:inline>
// author filter: ticking a chip applies it; the name box suggests any poet/writer (Urdu-normalised)
(() => {
const form = document.getElementById('author-filter'); if (!form) return;
form.addEventListener('change', (e) => { if (e.target.name === 'poet') form.submit(); });
const poets = JSON.parse(document.getElementById('all-poets').textContent);
const norm = (s) => s.normalize('NFC').replace(/[\u064B-\u0652\u0654\u0670\u0614]/g, '').replace(/[يىئ]/g, 'ی').replace(/ك/g, 'ک').replace(/[هۂ]/g, 'ہ').replace(/[أإ]/g, 'ا');
const box = document.getElementById('author-name'), list = document.getElementById('author-suggest');
const add = (p) => {
const i = document.createElement('input'); i.type = 'hidden'; i.name = 'poet'; i.value = p.id;
form.append(i); form.submit();
};
box.addEventListener('input', () => {
const t = norm(box.value.trim());
const hits = t ? poets.filter((p) => norm(p.name).includes(t)).slice(0, 12) : [];
list.replaceChildren(...hits.map((p) => {
const b = document.createElement('button'); b.type = 'button'; b.textContent = p.name; b.onclick = () => add(p); return b;
}));
list.hidden = !hits.length;
});
box.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); list.querySelector('button')?.click(); } });
})();
</script>
</Base>