- Icon component (Lucide shapes, ISC), currentColor, decorative (the control keeps its words in aria-label/title). - Colour roles: --action (primary, filled), --ok (approve, publish), --danger (log out, delete, reject); .btn variants and .icon-btn; light and dark. - Header: admin, moderation, saved, word book, account/sign in, font, theme as icons; log out in red. - Review: save/send filled, approve/publish green, return plain, reject red, with icons. Account: save, log out, password, delete. Reading pages: edit, history, arrange, tag. E-books: add, download, covers; the reader's controls as icons. - نشانات is now محفوظ شدہ; favourite writers have their own page, پسندیدہ لکھاری (/writers). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
171 lines
12 KiB
Plaintext
171 lines
12 KiB
Plaintext
---
|
||
// Poet (/p238), category (/p238/shaeri/c641) and poem (/p238/shaeri/c641/sh6556) pages
|
||
import Base from '../layouts/Base.astro';
|
||
import Seg from '../components/Seg.astro';
|
||
import Tags from '../components/Tags.astro';
|
||
import { api } from '../lib/api';
|
||
import { asUser, COOKIE } from '../lib/auth';
|
||
import { phraseParts, phraseRe } from '../lib/phrases';
|
||
import { ud, years, highlighter, parts } from '../lib/urdu';
|
||
|
||
const url = '/' + (Astro.params.path ?? '');
|
||
const page = await api(`/api/page?url=${encodeURIComponent(url)}`);
|
||
if (!page) return new Response(null, { status: 404, statusText: 'Not found' });
|
||
|
||
// group verses into couplets / single lines / paragraphs for rendering
|
||
type Verse = { position: string; couplet: number; text: string };
|
||
const blocks: { kind: 'couplet' | 'single' | 'para'; lines: string[]; c: number; mark?: string }[] = [];
|
||
if (page.type === 'poem') {
|
||
const { matla, maqta } = page.poem;
|
||
for (const v of page.verses as Verse[]) {
|
||
if (v.position === 'Right') blocks.push({ kind: 'couplet', lines: [v.text], c: v.couplet,
|
||
mark: v.couplet === matla ? 'مطلع' : v.couplet === maqta ? 'مقطع' : undefined });
|
||
else if (v.position === 'Left' && blocks.at(-1)?.kind === 'couplet' && blocks.at(-1)!.lines.length === 1) blocks.at(-1)!.lines.push(v.text);
|
||
else if (v.position === 'Paragraph') blocks.push({ kind: 'para', lines: [v.text], c: v.couplet });
|
||
else blocks.push({ kind: 'single', lines: [v.text], c: v.couplet });
|
||
}
|
||
}
|
||
// pen name (marked ؔ in the text) set apart; split() with a capture group puts matches at odd indexes
|
||
// ?hl= (from search results) marks the searched words on the page; segments: [text, 'mark' | 'pen' | '']
|
||
const hlq = Astro.url.searchParams.get('hl') ?? '';
|
||
const re = highlighter(hlq);
|
||
// bookmarked phrases first ('bmk', carrying what to un-bookmark), then search hits, then the pen name
|
||
type Mark = { part: string; save: string };
|
||
const show = (line: string, marks: Mark[] = []): [string, string, string?][] => {
|
||
const plain = (text: string) => parts(text, re).flatMap((s, i): [string, string][] =>
|
||
i % 2 ? [[s, 'mark']] : s.split(/(\S*\u0614\S*)/).map((t, j) => [t, j % 2 ? 'pen' : '']));
|
||
if (!marks.length) return plain(line);
|
||
const bm = new RegExp(`(${marks.map((m) => phraseRe(m.part).source).join('|')})`, 'u');
|
||
return line.split(bm).flatMap((s, i): [string, string, string?][] =>
|
||
i % 2 ? [[s, 'bmk', marks.find((m) => phraseRe(m.part).test(s))?.save]] : plain(s));
|
||
};
|
||
// contents of a divan grouped by radif letter ("ردیف الف … ی"); ے is filed under ی as in printed divans
|
||
const LETTER = (l: string) => (l === 'ا' ? 'الف' : l === 'ے' ? 'ی' : l);
|
||
const groups: { letter: string | null; poems: any[] }[] = [];
|
||
for (const p of page.poems ?? []) {
|
||
const l = p.radif_letter ? LETTER(p.radif_letter) : null;
|
||
if (groups.at(-1)?.letter !== l || !groups.length) groups.push({ letter: l, poems: [] });
|
||
groups.at(-1)!.poems.push(p);
|
||
}
|
||
const crumbs = page.breadcrumbs ?? [];
|
||
// what the signed-in reader has bookmarked on this page (poet, book/chapter, poem, couplets, phrases)
|
||
const token = Astro.locals.user ? Astro.cookies.get(COOKIE)?.value : undefined;
|
||
const poetId = page.poet?.id, poemId = page.type === 'poem' ? page.poem.id : undefined;
|
||
const categoryId = page.type === 'category' ? page.category.id : undefined;
|
||
const saved = token ? (await asUser(token, `/api/library/state?poet=${poetId ?? ''}&category=${categoryId ?? ''}&poem=${poemId ?? ''}`)).data
|
||
: { poet: false, category: false, poem: false, couplets: [], phrases: [] };
|
||
// on contents pages: which works are bookmarked or hold bookmarks, and which books/chapters do
|
||
const marks = token && page.type !== 'poem' ? (await asUser(token, '/api/library/marks')).data : { poems: {}, categories: [] };
|
||
const glyph = (poemId: number) => (marks.poems[poemId]?.fav ? ' mark-fav' : '') + (marks.poems[poemId]?.bm ? ' mark-bm' : '');
|
||
// the marked part of each line of a block, for its bookmarked phrases
|
||
const lineMarks = (c: number, lines: string[]) => {
|
||
const out: Mark[][] = lines.map(() => []);
|
||
for (const p of saved.phrases.filter((x: any) => x.couplet === c)) {
|
||
const save = JSON.stringify({ kind: 'phrase', poemId, couplet: c, phrase: p.phrase });
|
||
phraseParts(lines, p.phrase).forEach((part, i) => part && out[i].push({ part, save }));
|
||
}
|
||
return out;
|
||
};
|
||
const label = (on: unknown) => (on ? 'محفوظ شدہ' : 'محفوظ کریں'); // bookmark buttons
|
||
// moderators who may edit this work get edit and history links
|
||
const modCan = token && poemId && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '')
|
||
? (await asUser(token, `/api/mod/can?poem=${poemId}`)).data : { edit: false };
|
||
// on poet and book/section pages, moderators with the arrange permission get "ترتیب دیں" (#41)
|
||
const catCan = token && page.type !== 'poem' && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '')
|
||
? (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data : {};
|
||
const arrange = catCan.arrange, tagCan = page.type === 'poem' ? modCan.tags : catCan.tags;
|
||
// tags (#52): the page's own, and each couplet's (tags count couplets from 1, verses from 0)
|
||
const pageTags = (page.tags ?? []).filter((t: any) => !t.couplet);
|
||
const coupletTags = (c: number) => (page.tags ?? []).filter((t: any) => t.couplet === c + 1);
|
||
// every poet has an ای بکس section beside their books and genres (/p17/ebooks)
|
||
const ebookCount = page.type === 'poet' ? ((await api<any[]>(`/api/ebooks?poet=${poetId}`)) ?? []).length : 0;
|
||
const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`;
|
||
const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couplet: c });
|
||
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
|
||
import Icon from '../components/Icon.astro';
|
||
---
|
||
<Base title={page.type === 'poem' ? `${page.poem.title} · ${page.poet.nickname}` : title} description={page.type === 'poet' ? page.poet.description?.slice(0, 160) : undefined}>
|
||
{page.type !== 'poet' && (
|
||
<nav class="crumbs">
|
||
{crumbs.map((c: any, i: number) => (
|
||
i < crumbs.length - 1 || page.type === 'poem' ? <span><a href={c.url}>{c.title}</a></span> : <span>{c.title}</span>
|
||
))}
|
||
</nav>
|
||
)}
|
||
|
||
{page.type === 'poet' && (
|
||
<>
|
||
<h1>{page.poet.name}</h1>
|
||
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poet', poetId })} aria-pressed={String(!!saved.poet)} title="یہ شاعر محفوظ کریں">{label(saved.poet)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
|
||
<a class="icon-btn small" href={`/mod/order/${page.category.id}`} title="ترتیب کا تاریخچہ" aria-label="ترتیب کا تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
|
||
<p class="muted">{years(page.poet)}</p>
|
||
<Tags tags={pageTags} />
|
||
{page.poet.description && <div class="intro">{page.poet.description}</div>}
|
||
</>
|
||
)}
|
||
{page.type === 'category' && (
|
||
<>
|
||
<h1>{page.category.title}</h1>
|
||
<Tags tags={pageTags} />
|
||
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'category', categoryId: page.category.id })} aria-pressed={String(!!saved.category)} title="یہ کتاب یا باب محفوظ کریں">{label(saved.category)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
|
||
<a class="icon-btn small" href={`/mod/order/${page.category.id}`} title="ترتیب کا تاریخچہ" aria-label="ترتیب کا تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
|
||
</>
|
||
)}
|
||
|
||
{page.type !== 'poem' && (page.children.length > 0 || page.type === 'poet') && (
|
||
<div class="cards">
|
||
{page.children.map((c: any) => (
|
||
<a class={`card${marks.categories.includes(c.id) ? ' mark-bm' : ''}`} href={c.url}>{c.title}{c.poems > 0 && <small>{ud(c.poems)}</small>}</a>
|
||
))}
|
||
{page.type === 'poet' && <a class="card" href={`${page.poet.url}/ebooks`}>ای بکس{ebookCount > 0 && <small>{ud(ebookCount)}</small>}</a>}
|
||
</div>
|
||
)}
|
||
{page.type !== 'poem' && groups.map((g) => (
|
||
<>
|
||
{g.letter && <h2 class="radif-head">ردیف {g.letter}</h2>}
|
||
<ol class="toc">{g.poems.map((p: any) => <li><a class={glyph(p.id)} href={p.url}>{p.title}</a></li>)}</ol>
|
||
</>
|
||
))}
|
||
|
||
{page.type === 'poem' && (
|
||
<>
|
||
<h1><Seg s={show(page.poem.title)} /></h1>
|
||
{re && (
|
||
<p class="hl-note muted">
|
||
«{hlq}» نمایاں · <a href={`/search?q=${encodeURIComponent(hlq)}`}>تلاش کے نتائج</a> · <a href={url}>نشان ہٹائیں</a>
|
||
</p>
|
||
)}
|
||
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poem', poemId })} aria-pressed={String(!!saved.poem)} title="یہ کلام محفوظ کریں">{label(saved.poem)}</button>
|
||
{modCan.edit && <> <form method="post" action={`/mod/work/${poemId}`} class="inline-form"><button class="mod-edit-btn"><Icon name="edit" size={17} /> ترمیم کریں</button></form>
|
||
<a class="icon-btn small" href={`/mod/work/${poemId}`} title="تاریخچہ" aria-label="تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
|
||
<Tags tags={pageTags} />
|
||
{page.poem.radif != null && <p class="muted radif">{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}</p>}
|
||
<article class="poem" data-cite={`${page.poet.nickname}، ${page.poem.title}`} data-poem={poemId}>
|
||
{blocks.map((b) => {
|
||
const bm = <button type="button" class="bm" data-save={bookmark(b.c)} aria-pressed={String(saved.couplets.includes(b.c))} aria-label="شعر محفوظ کریں" />;
|
||
const lm = lineMarks(b.c, b.lines);
|
||
return b.kind === 'couplet' ? <div class="couplet" id={`c${b.c}`} data-c={b.c} data-mark={b.mark}>{bm}{b.lines.map((l, i) => <p><Seg s={show(l, lm[i])} /></p>)}<Tags tags={coupletTags(b.c)} /></div>
|
||
: b.kind === 'para' ? <p class="para" id={`c${b.c}`} data-c={b.c}>{bm}<Seg s={show(b.lines[0], lm[0])} /></p>
|
||
: <p class="single" id={`c${b.c}`} data-c={b.c}>{bm}<Seg s={show(b.lines[0], lm[0])} /></p>;
|
||
})}
|
||
</article>
|
||
<p class="muted hint">کسی لفظ کو منتخب کریں تو اس کے معنی اور تلفظ ساتھ نظر آئیں گے۔</p>
|
||
<nav class="poem-nav">
|
||
{page.prev && <a href={page.prev} rel="prev">‹ پچھلا</a>}
|
||
{page.next && <a href={page.next} rel="next">اگلا ›</a>}
|
||
</nav>
|
||
{page.divan && (
|
||
<p class="source divan-edit">
|
||
دیوان کا ورژن {ud(page.divan.version)} · ترمیم: {page.divan.by}{page.divan.reviewedBy && <> · جائزہ: {page.divan.reviewedBy}</>} · اشاعت: {page.divan.publishedBy}
|
||
· <bdi dir="ltr">{new Date(page.divan.at).toISOString().slice(0, 10)}</bdi>
|
||
{page.divan.commit_url && <> · <a href={page.divan.commit_url}>تبدیلی دیکھیں</a></>}
|
||
</p>
|
||
)}
|
||
{page.poem.source_url && (
|
||
<p class="source">ماخذ: <a href={page.poem.source_url}>ویکی ماخذ</a> · {page.poet && <a href={page.poet.url}>{page.poet.nickname}</a>}</p>
|
||
)}
|
||
</>
|
||
)}
|
||
{re && <script is:inline>document.querySelector('.poem mark')?.scrollIntoView({ block: 'center' });</script>}
|
||
</Base>
|