Merge remote-tracking branch 'gitea/main' into research/content-model

This commit is contained in:
Anas Rashid 2026-10-09 00:16:13 +02:00
commit 89231985b9
6 changed files with 33 additions and 23 deletions

View File

@ -3,7 +3,7 @@
const { current } = Astro.props as { current: 'bookmarks' | 'words' }; const { current } = Astro.props as { current: 'bookmarks' | 'words' };
--- ---
<nav class="admin-nav"> <nav class="admin-nav">
<a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}>🔖 نشانات</a> <a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}><span class="bm-icon on" aria-hidden="true"></span> نشانات</a>
<a href="/words" aria-current={current === 'words' ? 'page' : undefined}>ذخیرۂ الفاظ</a> <a href="/words" aria-current={current === 'words' ? 'page' : undefined}>ذخیرۂ الفاظ</a>
<a href="/account">پروفائل</a> <a href="/account">پروفائل</a>
</nav> </nav>

View File

@ -57,7 +57,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
<div id="selmenu" class="selmenu" role="toolbar" aria-label="منتخب عبارت" hidden> <div id="selmenu" class="selmenu" role="toolbar" aria-label="منتخب عبارت" hidden>
<button type="button" data-act="meaning" title="معنی اور تلفظ">معنی</button> <button type="button" data-act="meaning" title="معنی اور تلفظ">معنی</button>
<button type="button" data-act="saveword" class="plus" title="ذخیرۂ الفاظ میں ڈالیں">ذخیرۂ الفاظ</button> <button type="button" data-act="saveword" class="plus" title="ذخیرۂ الفاظ میں ڈالیں">ذخیرۂ الفاظ</button>
<button type="button" data-act="bookmark" title="اس عبارت پر نشان لگائیں">نشان</button> <button type="button" data-act="bookmark" title="یہ عبارت محفوظ کریں">محفوظ کریں</button>
<button type="button" data-act="search" title="دیوان میں تلاش">تلاش</button> <button type="button" data-act="search" title="دیوان میں تلاش">تلاش</button>
<button type="button" data-act="copy" title="حوالے کے ساتھ کاپی">کاپی</button> <button type="button" data-act="copy" title="حوالے کے ساتھ کاپی">کاپی</button>
<button type="button" data-act="share" title="اس عبارت کا لنک">شیئر</button> <button type="button" data-act="share" title="اس عبارت کا لنک">شیئر</button>
@ -158,7 +158,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
return e; return e;
}; };
let current = ''; let current = '';
// reader's library: 🔖 bookmarks (poets, books/chapters, poems, couplets, phrases) and the word book (+); // reader's library: bookmarks (poets, books/chapters, poems, couplets, phrases) and the word book (+);
// signed-out readers go to sign-in // signed-out readers go to sign-in
let source = {}; // the couplet the selected word came from let source = {}; // the couplet the selected word came from
async function save(item) { async function save(item) {
@ -173,7 +173,10 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
if (b.dataset.confirm && !confirm(b.dataset.confirm)) return; if (b.dataset.confirm && !confirm(b.dataset.confirm)) return;
const item = JSON.parse(b.dataset.save), d = await save(item); const item = JSON.parse(b.dataset.save), d = await save(item);
if (d && item.kind === 'phrase') location.reload(); // the highlight is drawn by the page if (d && item.kind === 'phrase') location.reload(); // the highlight is drawn by the page
else if (d) b.setAttribute('aria-pressed', String(d.saved)); else if (d) {
b.setAttribute('aria-pressed', String(d.saved));
if (b.classList.contains('save')) b.textContent = d.saved ? 'محفوظ شدہ' : 'محفوظ کریں';
}
}); });
const wordSave = (w) => { // sidebar button, with the word's saved state const wordSave = (w) => { // sidebar button, with the word's saved state
const b = el('button', 'wordbook', null, { type: 'button' }); const b = el('button', 'wordbook', null, { type: 'button' });
@ -349,7 +352,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
switch (b.dataset.act) { switch (b.dataset.act) {
case 'meaning': showWord(oneWord(selText)); break; case 'meaning': showWord(oneWord(selText)); break;
case 'bookmark': save({ kind: 'phrase', poemId: source.poemId, couplet: source.couplet, phrase: selText }) case 'bookmark': save({ kind: 'phrase', poemId: source.poemId, couplet: source.couplet, phrase: selText })
.then((d) => d && (d.saved ? location.reload() : flash(b, 'نشان پہلے سے تھا، ہٹا دیا'))); break; .then((d) => d && (d.saved ? location.reload() : flash(b, 'پہلے سے محفوظ تھی، ہٹا دی'))); break;
case 'saveword': save({ kind: 'word', word: oneWord(selText), ...source }).then((d) => d && flash(b, d.saved ? 'ذخیرۂ الفاظ میں ڈال دیا' : 'ذخیرۂ الفاظ سے نکال دیا')); break; case 'saveword': save({ kind: 'word', word: oneWord(selText), ...source }).then((d) => d && flash(b, d.saved ? 'ذخیرۂ الفاظ میں ڈال دیا' : 'ذخیرۂ الفاظ سے نکال دیا')); break;
case 'search': location.href = `/search?q=${encodeURIComponent(q)}`; break; case 'search': location.href = `/search?q=${encodeURIComponent(q)}`; break;
case 'google': open(`https://www.google.com/search?q=${enc}`, '_blank', 'noopener'); break; case 'google': open(`https://www.google.com/search?q=${enc}`, '_blank', 'noopener'); break;

View File

@ -47,13 +47,13 @@ for (const p of page.poems ?? []) {
groups.at(-1)!.poems.push(p); groups.at(-1)!.poems.push(p);
} }
const crumbs = page.breadcrumbs ?? []; const crumbs = page.breadcrumbs ?? [];
// what the signed-in reader has bookmarked on this page (🔖 poet, book/chapter, poem, couplets, phrases) // 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 token = Astro.locals.user ? Astro.cookies.get(COOKIE)?.value : undefined;
const poetId = page.poet?.id, poemId = page.type === 'poem' ? page.poem.id : undefined; const poetId = page.poet?.id, poemId = page.type === 'poem' ? page.poem.id : undefined;
const categoryId = page.type === 'category' ? page.category.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 const saved = token ? (await asUser(token, `/api/library/state?poet=${poetId ?? ''}&category=${categoryId ?? ''}&poem=${poemId ?? ''}`)).data
: { poet: false, category: false, poem: false, couplets: [], phrases: [] }; : { poet: false, category: false, poem: false, couplets: [], phrases: [] };
// on contents pages: which works are bookmarked or hold bookmarks (🔖), and which books/chapters do // 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 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' : ''); 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 // the marked part of each line of a block, for its bookmarked phrases
@ -65,6 +65,7 @@ const lineMarks = (c: number, lines: string[]) => {
} }
return out; return out;
}; };
const label = (on: unknown) => (on ? 'محفوظ شدہ' : 'محفوظ کریں'); // bookmark buttons
const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couplet: c }); 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; const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
--- ---
@ -80,7 +81,7 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
{page.type === 'poet' && ( {page.type === 'poet' && (
<> <>
<h1>{page.poet.name}</h1> <h1>{page.poet.name}</h1>
<p class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poet', poetId })} aria-pressed={String(!!saved.poet)} title="اس شاعر پر نشان لگائیں">نشان</button></p> <p class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poet', poetId })} aria-pressed={String(!!saved.poet)} title="یہ شاعر محفوظ کریں">{label(saved.poet)}</button></p>
<p class="muted">{years(page.poet)}</p> <p class="muted">{years(page.poet)}</p>
{page.poet.description && <div class="intro">{page.poet.description}</div>} {page.poet.description && <div class="intro">{page.poet.description}</div>}
</> </>
@ -88,7 +89,7 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
{page.type === 'category' && ( {page.type === 'category' && (
<> <>
<h1>{page.category.title}</h1> <h1>{page.category.title}</h1>
<p class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'category', categoryId: page.category.id })} aria-pressed={String(!!saved.category)} title="اس کتاب یا باب پر نشان لگائیں">نشان</button></p> <p 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></p>
</> </>
)} )}
@ -114,11 +115,11 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
«{hlq}» نمایاں · <a href={`/search?q=${encodeURIComponent(hlq)}`}>تلاش کے نتائج</a> · <a href={url}>نشان ہٹائیں</a> «{hlq}» نمایاں · <a href={`/search?q=${encodeURIComponent(hlq)}`}>تلاش کے نتائج</a> · <a href={url}>نشان ہٹائیں</a>
</p> </p>
)} )}
<p class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poem', poemId })} aria-pressed={String(!!saved.poem)} title="اس کلام پر نشان لگائیں">نشان</button></p> <p class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poem', poemId })} aria-pressed={String(!!saved.poem)} title="یہ کلام محفوظ کریں">{label(saved.poem)}</button></p>
{page.poem.radif != null && <p class="muted radif">{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}</p>} {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}> <article class="poem" data-cite={`${page.poet.nickname}، ${page.poem.title}`} data-poem={poemId}>
{blocks.map((b) => { {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 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); 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>)}</div> 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>)}</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> : b.kind === 'para' ? <p class="para" id={`c${b.c}`} data-c={b.c}>{bm}<Seg s={show(b.lines[0], lm[0])} /></p>

View File

@ -1,4 +1,4 @@
// Browser -> API for the 🔖 bookmark buttons and the word book: the reader's session cookie becomes the Bearer token. // Browser -> API for the bookmark buttons and the word book: the reader's session cookie becomes the Bearer token.
// JSON posts from other sites are refused: the Origin must be this site (and SameSite cookies are not sent). // JSON posts from other sites are refused: the Origin must be this site (and SameSite cookies are not sent).
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { asUser, COOKIE } from '../../lib/auth'; import { asUser, COOKIE } from '../../lib/auth';

View File

@ -33,7 +33,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
<p class="muted center">{ud(total)} نشانات</p> <p class="muted center">{ud(total)} نشانات</p>
{total === 0 && ( {total === 0 && (
<p class="muted center">ابھی کوئی نشان نہیں۔ کسی شاعر، کتاب، باب یا کلام پر 🔖 "نشان" دبائیں، کسی شعر کے ساتھ 🔖 لگائیں، یا کوئی عبارت منتخب کر کے "نشان" لگائیں۔</p> <p class="muted center">ابھی کچھ محفوظ نہیں۔ کسی شاعر، کتاب، باب یا کلام پر "محفوظ کریں" دبائیں، کسی شعر کے ساتھ والا نشان دبائیں، یا کوئی عبارت منتخب کر کے "محفوظ کریں" دبائیں۔</p>
)} )}
{lib.poets.length > 0 && ( {lib.poets.length > 0 && (

View File

@ -214,11 +214,19 @@ h1 + .muted { text-align: center; margin-top: 0; }
.grant-form legend { font-size: .85rem; padding: 0 6px; } .grant-form legend { font-size: .85rem; padding: 0 6px; }
.grant-form label.check { flex-direction: row; align-items: center; gap: 6px; } .grant-form label.check { flex-direction: row; align-items: center; gap: 6px; }
/* bookmarks (🔖): poets, books/chapters, poems, couplets; the word book is separate (+) */ /* bookmarks: our own rounded bookmark icon (soft corners and notch, like common app icon packs), a grey outline
when not set and filled in the brand colour when set; drawn as a mask so it follows the theme. The word book is
separate (+). */
:root { --bm-outline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.5 3h9A2.5 2.5 0 0 1 19 5.5v14.6a1 1 0 0 1-1.55.83L12 17.3l-5.45 3.63A1 1 0 0 1 5 20.1V5.5A2.5 2.5 0 0 1 7.5 3z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); --bm-filled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.5 3h9A2.5 2.5 0 0 1 19 5.5v14.6a1 1 0 0 1-1.55.83L12 17.3l-5.45 3.63A1 1 0 0 1 5 20.1V5.5A2.5 2.5 0 0 1 7.5 3z' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.bm-icon, .save::before, .bm::before, .bmk::after, .mark-fav::after, .mark-bm::after {
content: ''; display: inline-block; width: 1.05em; height: 1.05em; vertical-align: -.18em; background-color: var(--muted);
-webkit-mask: var(--bm-outline) center / contain no-repeat; mask: var(--bm-outline) center / contain no-repeat; user-select: none; }
.save[aria-pressed='true']::before, .bm[aria-pressed='true']::before, .bmk::after, .mark-fav::after, .mark-bm::after, .bm-icon.on {
background-color: var(--brand); -webkit-mask-image: var(--bm-filled); mask-image: var(--bm-filled); }
.save { font: inherit; font-size: .85rem; padding: 1px 14px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; } .save { font: inherit; font-size: .85rem; padding: 1px 14px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; }
.save::before { content: '🔖 '; filter: grayscale(1); opacity: .6; } .save::before { margin-inline-end: 4px; }
.save[aria-pressed='true'] { border-color: var(--brand); color: var(--brand); } .save[aria-pressed='true'] { border-color: var(--brand); color: var(--brand); font-weight: 700;
.save[aria-pressed='true']::before { filter: none; opacity: 1; } background: color-mix(in srgb, var(--brand) 12%, var(--inner)); } /* محفوظ شدہ: accent tint, filled glyph */
/* word book (ذخیرۂ الفاظ): + to add, ✓ when added (hearts are for poets and works) */ /* word book (ذخیرۂ الفاظ): + to add, ✓ when added (hearts are for poets and works) */
.wordbook { font: inherit; font-size: .85rem; padding: 1px 14px; margin-bottom: 6px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; } .wordbook { font: inherit; font-size: .85rem; padding: 1px 14px; margin-bottom: 6px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; }
.wordbook::before { content: '+ '; font-weight: 700; color: var(--gold); } .wordbook::before { content: '+ '; font-weight: 700; color: var(--gold); }
@ -227,9 +235,8 @@ h1 + .muted { text-align: center; margin-top: 0; }
.couplet, .poem .para, .poem .single { position: relative; } .couplet, .poem .para, .poem .single { position: relative; }
.bm { position: absolute; inset-inline-end: -26px; top: .5em; width: 20px; height: 22px; padding: 0; border: 0; background: none; cursor: pointer; .bm { position: absolute; inset-inline-end: -26px; top: .5em; width: 20px; height: 22px; padding: 0; border: 0; background: none; cursor: pointer;
opacity: 0; transition: opacity .15s; user-select: none; } opacity: 0; transition: opacity .15s; user-select: none; }
.bm::before { content: '🔖'; font-size: .85rem; filter: grayscale(1); opacity: .6; } .bm::before { width: 18px; height: 18px; }
.bm[aria-pressed='true'] { opacity: 1; } .bm[aria-pressed='true'] { opacity: 1; }
.bm[aria-pressed='true']::before { filter: none; opacity: 1; }
.couplet:hover .bm, .poem .para:hover .bm, .poem .single:hover .bm, .bm:focus-visible { opacity: 1; } .couplet:hover .bm, .poem .para:hover .bm, .poem .single:hover .bm, .bm:focus-visible { opacity: 1; }
@media (hover: none) { .bm { opacity: 1; } } @media (hover: none) { .bm { opacity: 1; } }
@media (max-width: 700px) { .bm { inset-inline-end: -4px; top: 0; } } @media (max-width: 700px) { .bm { inset-inline-end: -4px; top: 0; } }
@ -251,11 +258,10 @@ h1 + .muted { text-align: center; margin-top: 0; }
.lib-remove { position: absolute; left: 8px; top: 8px; margin: 0; } .lib-remove { position: absolute; left: 8px; top: 8px; margin: 0; }
.lib-remove button { color: var(--brand); } .lib-remove button { color: var(--brand); }
/* bookmarked phrases, and 🔖 where the reader's bookmarks are */ /* bookmarked phrases, and the bookmark icon where the reader's bookmarks are */
.bmk { text-decoration: underline dotted var(--brand) 2px; text-underline-offset: 6px; cursor: pointer; background: var(--gold-subtle); border-radius: 4px; } .bmk { text-decoration: underline dotted var(--brand) 2px; text-underline-offset: 6px; cursor: pointer; background: var(--gold-subtle); border-radius: 4px; }
.bmk::after { content: '🔖'; font-size: .55em; vertical-align: super; margin-inline-start: 1px; user-select: none; } .bmk::after { width: .7em; height: .7em; vertical-align: super; margin-inline-start: 2px; }
.mark-fav::after, .mark-bm::after { font-size: .65em; margin-inline-start: 6px; vertical-align: middle; user-select: none; } .mark-fav::after, .mark-bm::after { width: .85em; height: .85em; margin-inline-start: 6px; vertical-align: -.1em; }
.mark-fav::after, .mark-bm::after { content: '🔖'; }
/* search: poets/writers, books/chapters, content (paged) */ /* search: poets/writers, books/chapters, content (paged) */
.search-group { margin: 18px 0 26px; } .search-group { margin: 18px 0 26px; }