Icons and colour roles: header icons with tooltips and a red log out; buttons coloured by what they do; محفوظ شدہ and پسندیدہ لکھاری

- 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>
This commit is contained in:
Anas Rashid 2026-10-09 15:23:06 +02:00
parent 3f6f7e503a
commit c4bf52dd97
13 changed files with 156 additions and 70 deletions

View File

@ -0,0 +1,35 @@
---
// Icons (Lucide, ISC licence; https://lucide.dev): drawn with currentColor, so they take the colour of their button
// or link. Decorative: the button or link carries the words (aria-label/title, or its text).
const { name, size = 20 } = Astro.props as { name: keyof typeof PATHS; size?: number };
const PATHS = {
admin: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
edit: '<path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/>',
bookmark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
words: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="m8 13 4-7 4 7"/><path d="M9.1 11h5.7"/>',
user: '<circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>',
login: '<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/>',
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/>',
theme: '<circle cx="12" cy="12" r="10"/><path d="M12 18a6 6 0 0 0 0-12v12z"/>',
font: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>',
tag: '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r="1" fill="currentColor"/>',
arrange: '<path d="M10 12h11"/><path d="M10 18h11"/><path d="M10 6h11"/><path d="M4 10h2"/><path d="M4 6h1v4"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>',
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
check: '<path d="M20 6 9 17l-5-5"/>',
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>',
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
save: '<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/>',
full: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
next: '<path d="m15 18-6-6 6-6"/>', // points left: forward in a right-to-left page
prev: '<path d="m9 18 6-6-6-6"/>',
trash: '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>',
key: '<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/>',
image: '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.828 0L6 21"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
external: '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
};
---
<svg class="icon" viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" set:html={PATHS[name]} />

View File

@ -1,9 +1,10 @@
--- ---
// tabs between the reader's bookmarks and their word book (kept separate) // tabs of the reader's own space: saved texts, favourite writers, the word book (kept separate), profile
const { current } = Astro.props as { current: 'bookmarks' | 'words' }; const { current } = Astro.props as { current: 'bookmarks' | 'writers' | 'words' };
--- ---
<nav class="admin-nav"> <nav class="admin-nav">
<a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}><span class="bm-icon on" aria-hidden="true"></span> نشانات</a> <a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}><span class="bm-icon on" aria-hidden="true"></span> محفوظ شدہ</a>
<a href="/writers" aria-current={current === 'writers' ? 'page' : undefined}>پسندیدہ لکھاری</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

@ -3,4 +3,4 @@
const { s } = Astro.props as { s: [string, string, string?][] }; const { s } = Astro.props as { s: [string, string, string?][] };
--- ---
{s.map(([t, k, d]) => (k === 'mark' ? <mark>{t}</mark> : k === 'pen' ? <span class="takhallus">{t}</span> {s.map(([t, k, d]) => (k === 'mark' ? <mark>{t}</mark> : k === 'pen' ? <span class="takhallus">{t}</span>
: k === 'bmk' ? <span class="bmk" data-save={d} data-confirm="یہ نشان ہٹائیں؟" title="نشان زدہ عبارت (ہٹانے کے لیے دبائیں)">{t}</span> : t))} : k === 'bmk' ? <span class="bmk" data-save={d} data-confirm="یہ عبارت محفوظ شدہ سے ہٹائیں؟" title="محفوظ شدہ عبارت (ہٹانے کے لیے دبائیں)">{t}</span> : t))}

View File

@ -8,6 +8,7 @@ import '../styles/global.css';
interface Props { title?: string; description?: string; q?: string } interface Props { title?: string; description?: string; q?: string }
const { title, description = 'اردو کی کلاسیکی شاعری اور نثر', q = '' } = Astro.props; const { title, description = 'اردو کی کلاسیکی شاعری اور نثر', q = '' } = Astro.props;
const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کی کلاسیکی شاعری اور نثر'; const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کی کلاسیکی شاعری اور نثر';
import Icon from '../components/Icon.astro';
--- ---
<!doctype html> <!doctype html>
<html lang="ur-PK" dir="rtl"> <html lang="ur-PK" dir="rtl">
@ -43,17 +44,23 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5"/></svg> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5"/></svg>
</button> </button>
</form> </form>
<div class="toggles"> <nav class="toggles" aria-label="سائٹ">
{Astro.locals.user?.role === 'admin' && <a class="account-link" href="/admin">ایڈمن</a>} {Astro.locals.user?.role === 'admin' && <a class="icon-btn" href="/admin" title="ایڈمن" aria-label="ایڈمن"><Icon name="admin" /></a>}
{['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && <a class="account-link" href="/mod">موڈریشن</a>} {['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && <a class="icon-btn" href="/mod" title="موڈریشن" aria-label="موڈریشن"><Icon name="edit" /></a>}
{Astro.locals.user && <a class="account-link" href="/library">نشانات</a>} {Astro.locals.user && <a class="icon-btn" href="/library" title="محفوظ شدہ" aria-label="محفوظ شدہ"><Icon name="bookmark" /></a>}
{Astro.locals.user && <a class="account-link" href="/words">ذخیرۂ الفاظ</a>} {Astro.locals.user && <a class="icon-btn" href="/words" title="ذخیرۂ الفاظ" aria-label="ذخیرۂ الفاظ"><Icon name="words" /></a>}
{Astro.locals.user {Astro.locals.user
? <a class="account-link" href="/account" title={Astro.locals.user.email}>{Astro.locals.user.full_name.split(' ')[0] || 'اکاؤنٹ'}</a> ? <a class="icon-btn" href="/account" title={`اکاؤنٹ: ${Astro.locals.user.full_name || Astro.locals.user.email}`} aria-label="اکاؤنٹ"><Icon name="user" /></a>
: <a class="account-link" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`}>لاگ ان</a>} : <a class="icon-btn" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`} title="لاگ ان" aria-label="لاگ ان"><Icon name="login" /></a>}
<button type="button" id="font-toggle" aria-label="خط بدلیں">نسخ</button> <button type="button" class="icon-btn" id="font-toggle" aria-label="خط بدلیں" title="خط: نسخ"><Icon name="font" /></button>
<button type="button" id="theme-toggle" aria-label="روشن یا تاریک">◐</button> <button type="button" class="icon-btn" id="theme-toggle" aria-label="روشن یا تاریک" title="روشن یا تاریک"><Icon name="theme" /></button>
</div> {Astro.locals.user && (
<form method="post" action="/account" class="inline-form">
<input type="hidden" name="act" value="signout" />
<button class="icon-btn danger" title="لاگ آؤٹ" aria-label="لاگ آؤٹ"><Icon name="logout" /></button>
</form>
)}
</nav>
</div> </div>
<div id="kb" class="wrap kb" hidden></div> <div id="kb" class="wrap kb" hidden></div>
</header> </header>
@ -88,7 +95,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
<script is:inline> <script is:inline>
const root = document.documentElement; const root = document.documentElement;
const fontBtn = document.getElementById('font-toggle'); const fontBtn = document.getElementById('font-toggle');
const label = () => (fontBtn.textContent = root.classList.contains('nastaliq') ? 'نستعلیق' : 'نسخ'); const label = () => (fontBtn.title = `خط: ${root.classList.contains('nastaliq') ? 'نستعلیق' : 'نسخ'}`); // an icon; the name is in the tooltip
label(); label();
fontBtn.onclick = () => { fontBtn.onclick = () => {
root.classList.toggle('nastaliq'); root.classList.toggle('nastaliq');

View File

@ -57,7 +57,7 @@ export async function pdfReader(root: HTMLElement) {
const full = root.querySelector<HTMLButtonElement>('.pdf-full')!; const full = root.querySelector<HTMLButtonElement>('.pdf-full')!;
const toggle = () => (document.fullscreenElement ? document.exitFullscreen() : root.requestFullscreen()).catch(() => {}); const toggle = () => (document.fullscreenElement ? document.exitFullscreen() : root.requestFullscreen()).catch(() => {});
full.onclick = toggle; full.onclick = toggle;
document.addEventListener('fullscreenchange', () => { full.textContent = document.fullscreenElement === root ? 'واپس' : 'پوری اسکرین'; show(page); }); document.addEventListener('fullscreenchange', () => { full.title = document.fullscreenElement === root ? 'پوری اسکرین سے واپس (F)' : 'پوری اسکرین (F)'; show(page); });
// a click on the half the book turns towards goes forward // a click on the half the book turns towards goes forward
pagesBox.onclick = (e) => { const left = e.clientX < pagesBox.getBoundingClientRect().left + pagesBox.clientWidth / 2; step(left === rtl ? 1 : -1); }; pagesBox.onclick = (e) => { const left = e.clientX < pagesBox.getBoundingClientRect().left + pagesBox.clientWidth / 2; step(left === rtl ? 1 : -1); };
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {

View File

@ -82,6 +82,7 @@ const ebookCount = page.type === 'poet' ? ((await api<any[]>(`/api/ebooks?poet=$
const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`; const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`;
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;
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}> <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' && ( {page.type !== 'poet' && (
@ -95,8 +96,8 @@ 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>
<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">ترتیب دیں</button></form> <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="mod-history" href={`/mod/order/${page.category.id}`}>ترتیب کا تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div> <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> <p class="muted">{years(page.poet)}</p>
<Tags tags={pageTags} /> <Tags tags={pageTags} />
{page.poet.description && <div class="intro">{page.poet.description}</div>} {page.poet.description && <div class="intro">{page.poet.description}</div>}
@ -106,8 +107,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
<> <>
<h1>{page.category.title}</h1> <h1>{page.category.title}</h1>
<Tags tags={pageTags} /> <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">ترتیب دیں</button></form> <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="mod-history" href={`/mod/order/${page.category.id}`}>ترتیب کا تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div> <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>
</> </>
)} )}
@ -135,8 +136,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
</p> </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> <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">ترمیم کریں</button></form> {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="mod-history" href={`/mod/work/${poemId}`}>تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div> <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} /> <Tags tags={pageTags} />
{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}>

View File

@ -18,11 +18,12 @@ const cover = (b: any) => (b.has_cover ? `/ebooks/cover/${b.id}` : b.kind === 'a
const token = Astro.locals.user ? Astro.cookies.get(COOKIE)?.value : undefined; const token = Astro.locals.user ? Astro.cookies.get(COOKIE)?.value : undefined;
const mayAdd = token && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user!.role) const mayAdd = token && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user!.role)
&& (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data.ebooks; && (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data.ebooks;
import Icon from '../../components/Icon.astro';
--- ---
<Base title={`ای بکس · ${page.poet.nickname}`}> <Base title={`ای بکس · ${page.poet.nickname}`}>
<nav class="crumbs"><a href={page.poet.url}>{page.poet.nickname}</a></nav> <nav class="crumbs"><a href={page.poet.url}>{page.poet.nickname}</a></nav>
<h1>ای بکس</h1> <h1>ای بکس</h1>
{mayAdd && <p class="center"><a class="mod-edit-btn" href={`/mod/ebook/new?poet=${page.poet.id}`}>ای بک شامل کریں</a></p>} {mayAdd && <p class="center"><a class="btn primary" href={`/mod/ebook/new?poet=${page.poet.id}`}><Icon name="plus" size={18} /> ای بک شامل کریں</a></p>}
<nav class="admin-nav ebook-tabs" aria-label="کتابوں کی قسم"> <nav class="admin-nav ebook-tabs" aria-label="کتابوں کی قسم">
{tabs.map(([k, ur, en]) => { {tabs.map(([k, ur, en]) => {
const n = books.filter((b) => b.kind === k).length; const n = books.filter((b) => b.kind === k).length;

View File

@ -36,6 +36,7 @@ if (Astro.request.method === 'POST') {
} }
} }
const since = new Date(user.created_at); const since = new Date(user.created_at);
import Icon from '../components/Icon.astro';
--- ---
<Base title="میرا اکاؤنٹ"> <Base title="میرا اکاؤنٹ">
<h1>{user.full_name || 'میرا اکاؤنٹ'}</h1> <h1>{user.full_name || 'میرا اکاؤنٹ'}</h1>
@ -49,12 +50,12 @@ const since = new Date(user.created_at);
<input type="hidden" name="act" value="profile" /> <input type="hidden" name="act" value="profile" />
<label>پورا نام<input name="full_name" value={user.full_name} maxlength="100" dir="auto" autocomplete="name" /></label> <label>پورا نام<input name="full_name" value={user.full_name} maxlength="100" dir="auto" autocomplete="name" /></label>
<label>تعارف<textarea name="bio" maxlength="1000" rows="4" dir="auto">{user.bio}</textarea></label> <label>تعارف<textarea name="bio" maxlength="1000" rows="4" dir="auto">{user.bio}</textarea></label>
<button type="submit">محفوظ کریں</button> <button type="submit" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
</form> </form>
<form method="post" class="account-form"> <form method="post" class="account-form">
<input type="hidden" name="act" value="signout" /> <input type="hidden" name="act" value="signout" />
<button type="submit">لاگ آؤٹ</button> <button type="submit" class="btn danger"><Icon name="logout" size={18} /> لاگ آؤٹ</button>
</form> </form>
<form method="post" class="account-form"> <form method="post" class="account-form">
@ -63,7 +64,7 @@ const since = new Date(user.created_at);
<label>موجودہ پاس ورڈ<input type="password" name="current" required autocomplete="current-password" dir="ltr" /></label> <label>موجودہ پاس ورڈ<input type="password" name="current" required autocomplete="current-password" dir="ltr" /></label>
<label>نیا پاس ورڈ<input type="password" name="next" required minlength="8" autocomplete="new-password" dir="ltr" /></label> <label>نیا پاس ورڈ<input type="password" name="next" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
<label>نیا پاس ورڈ دوبارہ<input type="password" name="next2" required minlength="8" autocomplete="new-password" dir="ltr" /></label> <label>نیا پاس ورڈ دوبارہ<input type="password" name="next2" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
<button type="submit">پاس ورڈ بدلیں</button> <button type="submit" class="btn"><Icon name="key" size={18} /> پاس ورڈ بدلیں</button>
</form> </form>
<form method="post" class="account-form danger"> <form method="post" class="account-form danger">
@ -71,6 +72,6 @@ const since = new Date(user.created_at);
<p class="muted">اکاؤنٹ اور اس کا تمام ڈیٹا مستقل طور پر حذف ہو جائے گا۔</p> <p class="muted">اکاؤنٹ اور اس کا تمام ڈیٹا مستقل طور پر حذف ہو جائے گا۔</p>
<input type="hidden" name="act" value="delete" /> <input type="hidden" name="act" value="delete" />
<label>پاس ورڈ<input type="password" name="password" required autocomplete="current-password" dir="ltr" /></label> <label>پاس ورڈ<input type="password" name="password" required autocomplete="current-password" dir="ltr" /></label>
<button type="submit">اکاؤنٹ مستقل طور پر ختم کریں</button> <button type="submit" class="btn danger"><Icon name="trash" size={18} /> اکاؤنٹ مستقل طور پر ختم کریں</button>
</form> </form>
</Base> </Base>

View File

@ -18,6 +18,7 @@ const mayCover = token && poetPage && b.kind !== 'archive' && ['mod-l2', 'mod-l1
// the reader's bookmarked pages of this book // the reader's bookmarked pages of this book
const marks = token && b.published && b.kind === 'pdf' ? (await asUser(token, `/api/library/state?ebook=${b.id}`)).data.pages ?? [] : []; const marks = token && b.published && b.kind === 'pdf' ? (await asUser(token, `/api/library/state?ebook=${b.id}`)).data.pages ?? [] : [];
const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size / 1024)))} KB` : `${ud((b.size / 1048576).toFixed(1))} MB`; const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size / 1024)))} KB` : `${ud((b.size / 1048576).toFixed(1))} MB`;
import Icon from '../../components/Icon.astro';
--- ---
<Base title={`${b.title} · ${b.poet}`}> <Base title={`${b.title} · ${b.poet}`}>
<nav class="crumbs"><a href={b.poet_url}>{b.poet}</a> » <a href={`${b.poet_url}/ebooks?kind=${b.kind}`}>ای بکس</a></nav> <nav class="crumbs"><a href={b.poet_url}>{b.poet}</a> » <a href={`${b.poet_url}/ebooks?kind=${b.kind}`}>ای بکس</a></nav>
@ -25,7 +26,7 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
{!b.published && <p class="form-error center">یہ کتاب ابھی جائزے میں ہے؛ صرف موڈریٹر دیکھ سکتے ہیں۔</p>} {!b.published && <p class="form-error center">یہ کتاب ابھی جائزے میں ہے؛ صرف موڈریٹر دیکھ سکتے ہیں۔</p>}
<p class="center muted"> <p class="center muted">
{KIND[b.kind]}{mb && <> · {mb}</>}{b.source && <> · ماخذ: {b.source}</>}{b.licence && <> · {b.licence}</>} {KIND[b.kind]}{mb && <> · {mb}</>}{b.source && <> · ماخذ: {b.source}</>}{b.licence && <> · {b.licence}</>}
{b.file && <> · <a href={file} download>ڈاؤن لوڈ</a></>} {b.file && <> · <a class="btn" href={file} download><Icon name="download" size={17} /> ڈاؤن لوڈ</a></>}
{b.kind === 'archive' && <> · <a href={`https://archive.org/details/${b.archive_id}`}>archive.org پر</a></>} {b.kind === 'archive' && <> · <a href={`https://archive.org/details/${b.archive_id}`}>archive.org پر</a></>}
</p> </p>
<p class="center eb-people"> <p class="center eb-people">
@ -38,8 +39,8 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
{mayCover && ( {mayCover && (
<p class="center cover-tools" data-id={b.id} data-kind={b.kind} data-src={file}> <p class="center cover-tools" data-id={b.id} data-kind={b.kind} data-src={file}>
{b.cover && <img class="cover-small" src={`/ebooks/cover/${b.id}`} alt="سرورق" />} {b.cover && <img class="cover-small" src={`/ebooks/cover/${b.id}`} alt="سرورق" />}
{(b.kind === 'pdf' || b.kind === 'epub') && <button type="button" class="mod-edit-btn" data-make>{b.kind === 'pdf' ? 'پہلے صفحے سے سرورق بنائیں' : 'کتاب کا سرورق لیں'}</button>} {(b.kind === 'pdf' || b.kind === 'epub') && <button type="button" class="mod-edit-btn" data-make><Icon name="image" size={17} /> {b.kind === 'pdf' ? 'پہلے صفحے سے سرورق بنائیں' : 'کتاب کا سرورق لیں'}</button>}
<label class="mod-edit-btn">اپنی تصویر سرورق بنائیں<input type="file" accept="image/jpeg,image/png,image/webp" hidden data-own /></label> <label class="mod-edit-btn"><Icon name="upload" size={17} /> اپنی تصویر سرورق بنائیں<input type="file" accept="image/jpeg,image/png,image/webp" hidden data-own /></label>
<span class="muted" data-cover-status></span> <span class="muted" data-cover-status></span>
</p> </p>
)} )}
@ -47,15 +48,15 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
{b.kind === 'pdf' && ( {b.kind === 'pdf' && (
<div class="pdf-reader" data-src={file} data-ebook={b.id} data-marks={JSON.stringify(marks)} dir={b.direction === 'ltr' ? 'ltr' : 'rtl'}> <div class="pdf-reader" data-src={file} data-ebook={b.id} data-marks={JSON.stringify(marks)} dir={b.direction === 'ltr' ? 'ltr' : 'rtl'}>
<nav class="pdf-bar"> <nav class="pdf-bar">
<button type="button" class="mod-edit-btn pdf-prev">‹ پچھلا</button> <button type="button" class="icon-btn pdf-prev" title="پچھلا صفحہ" aria-label="پچھلا صفحہ"><Icon name={b.direction === 'ltr' ? 'next' : 'prev'} /></button>
<span>صفحہ <input class="pdf-page" type="number" min="1" value="1" aria-label="صفحہ نمبر" /> از <span class="pdf-total">…</span></span> <span>صفحہ <input class="pdf-page" type="number" min="1" value="1" aria-label="صفحہ نمبر" /> از <span class="pdf-total">…</span></span>
<button type="button" class="mod-edit-btn pdf-next">اگلا ›</button> <button type="button" class="icon-btn pdf-next" title="اگلا صفحہ" aria-label="اگلا صفحہ"><Icon name={b.direction === 'ltr' ? 'prev' : 'next'} /></button>
<button type="button" class="mod-edit-btn pdf-full" title="پوری اسکرین (F)">پوری اسکرین</button> <button type="button" class="icon-btn pdf-full" title="پوری اسکرین (F)" aria-label="پوری اسکرین"><Icon name="full" /></button>
{b.published && <button type="button" class="save pdf-mark" aria-pressed="false">صفحہ محفوظ کریں</button>} {b.published && <button type="button" class="save pdf-mark" aria-pressed="false">صفحہ محفوظ کریں</button>}
</nav> </nav>
<p class="tag-chips pdf-marks" hidden></p> <p class="tag-chips pdf-marks" hidden></p>
<div class="pdf-pages" title="بائیں طرف کلک: اگلا صفحہ"><p class="muted">کتاب کھل رہی ہے…</p></div> <div class="pdf-pages" title="بائیں طرف کلک: اگلا صفحہ"><p class="muted">کتاب کھل رہی ہے…</p></div>
<p class="muted center small-line" dir="rtl">{b.direction === 'ltr' ? '→ اگلا صفحہ · ← پچھلا صفحہ' : '← اگلا صفحہ · → پچھلا صفحہ'} · <a href={file} target="_blank" rel="noopener">نئی ٹیب میں کھولیں</a></p> <p class="muted center small-line" dir="rtl">{b.direction === 'ltr' ? '→ اگلا صفحہ · ← پچھلا صفحہ' : '← اگلا صفحہ · → پچھلا صفحہ'} · <a href={file} target="_blank" rel="noopener"><Icon name="external" size={15} /> نئی ٹیب میں کھولیں</a></p>
</div> </div>
)} )}
{b.kind === 'archive' && <iframe class="ebook-frame" src={`https://archive.org/embed/${b.archive_id}`} title={b.title} allowfullscreen></iframe>} {b.kind === 'archive' && <iframe class="ebook-frame" src={`https://archive.org/embed/${b.archive_id}`} title={b.title} allowfullscreen></iframe>}

View File

@ -1,5 +1,5 @@
--- ---
// نشانات: the reader's bookmarks (poets, books/chapters, works, couplets, phrases), each with its full path in // محفوظ شدہ: the reader's saved books/chapters, works, couplets, phrases and e-book pages, each with its full path in
// the site (poet » book » chapter » work) and a link to the couplet. The word book is separate: /words. // the site (poet » book » chapter » work) and a link to the couplet. The word book is separate: /words.
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
import { asUser, COOKIE } from '../lib/auth'; import { asUser, COOKIE } from '../lib/auth';
@ -17,7 +17,7 @@ if (Astro.request.method === 'POST') {
return Astro.redirect(`/library#i${id}`, 303); return Astro.redirect(`/library#i${id}`, 303);
} }
const lib = (await asUser(token, '/api/library')).data; const lib = (await asUser(token, '/api/library')).data;
const total = lib.poets.length + lib.categories.length + lib.poems.length + lib.couplets.length + lib.phrases.length + (lib.pages?.length ?? 0); const total = lib.categories.length + lib.poems.length + lib.couplets.length + lib.phrases.length + (lib.pages?.length ?? 0);
// a bookmarked phrase shown inside its couplet: [before, phrase, after] per line // a bookmarked phrase shown inside its couplet: [before, phrase, after] per line
const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).map((part, i) => { const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).map((part, i) => {
if (!part) return [lines[i], '', '']; if (!part) return [lines[i], '', ''];
@ -26,28 +26,19 @@ const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).m
}); });
const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != null ? `#c${item.poem.couplet}` : ''}` : '#'); const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != null ? `#c${item.poem.couplet}` : ''}` : '#');
--- ---
<Base title="نشانات"> <Base title="محفوظ شدہ">
<h1>{me.full_name || 'نشانات'}</h1> <h1>{me.full_name || 'محفوظ شدہ'}</h1>
{me.bio && <p class="profile-bio">{me.bio}</p>} {me.bio && <p class="profile-bio">{me.bio}</p>}
<LibraryNav current="bookmarks" /> <LibraryNav current="bookmarks" />
<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 && (
<section class="lib-section">
<h2>نشان زد شعرا</h2>
<div class="poets">
{lib.poets.map((p: any) => p.poet && <a class="poet-card" id={`i${p.id}`} href={p.poet.url}>{p.poet.name}</a>)}
</div>
</section>
)} )}
{lib.categories.length > 0 && ( {lib.categories.length > 0 && (
<section class="lib-section"> <section class="lib-section">
<h2>نشان زد کتابیں اور ابواب</h2> <h2>محفوظ کتابیں اور ابواب</h2>
<ul class="lib-list"> <ul class="lib-list">
{lib.categories.map((c: any) => ( {lib.categories.map((c: any) => (
<li id={`i${c.id}`}> <li id={`i${c.id}`}>
@ -64,7 +55,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
{lib.poems.length > 0 && ( {lib.poems.length > 0 && (
<section class="lib-section"> <section class="lib-section">
<h2>نشان زد کلام</h2> <h2>محفوظ کلام</h2>
<ul class="lib-list"> <ul class="lib-list">
{lib.poems.map((p: any) => ( {lib.poems.map((p: any) => (
<li id={`i${p.id}`}> <li id={`i${p.id}`}>
@ -81,7 +72,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
{lib.couplets.length > 0 && ( {lib.couplets.length > 0 && (
<section class="lib-section"> <section class="lib-section">
<h2>نشان زد اشعار</h2> <h2>محفوظ اشعار</h2>
<ul class="lib-list"> <ul class="lib-list">
{lib.couplets.map((c: any) => ( {lib.couplets.map((c: any) => (
<li id={`i${c.id}`}> <li id={`i${c.id}`}>
@ -103,7 +94,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
{lib.phrases.length > 0 && ( {lib.phrases.length > 0 && (
<section class="lib-section"> <section class="lib-section">
<h2>نشان زد عبارتیں</h2> <h2>محفوظ عبارتیں</h2>
<ul class="lib-list"> <ul class="lib-list">
{lib.phrases.map((p: any) => ( {lib.phrases.map((p: any) => (
<li id={`i${p.id}`}> <li id={`i${p.id}`}>

View File

@ -42,7 +42,10 @@ const ebook = rev.entity === 'ebook';
const EB: [string, string][] = [['title', 'عنوان'], ['writer', 'مصنف'], ['coauthors', 'شریک مصنفین'], ['language', 'زبان'], ['direction', 'سمت'], const EB: [string, string][] = [['title', 'عنوان'], ['writer', 'مصنف'], ['coauthors', 'شریک مصنفین'], ['language', 'زبان'], ['direction', 'سمت'],
['tags', 'ٹیگ'], ['source', 'ماخذ'], ['licence', 'اجازت'], ['note', 'تفصیل']]; ['tags', 'ٹیگ'], ['source', 'ماخذ'], ['licence', 'اجازت'], ['note', 'تفصیل']];
const ebMeta = ebook ? Object.fromEntries(EB.map(([k, label]) => [k, (rev.content.split('\n').find((l: string) => l.startsWith(label + ':')) ?? '').slice(label.length + 1).trim()])) : {}; const ebMeta = ebook ? Object.fromEntries(EB.map(([k, label]) => [k, (rev.content.split('\n').find((l: string) => l.startsWith(label + ':')) ?? '').slice(label.length + 1).trim()])) : {};
const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیجیں'], ['reject', 'مسترد کریں'], ['publish', 'شائع کریں']] as const).filter(([a]) => may[a]); // each action in its colour and with its icon: approve and publish confirm, return goes back, reject is destructive
const acts = ([['approve', 'منظور کریں', 'ok', 'check'], ['return', 'واپس بھیجیں', '', 'undo'], ['reject', 'مسترد کریں', 'danger', 'close'],
['publish', 'شائع کریں', 'ok', 'upload']] as const).filter(([a]) => may[a]);
import Icon from '../../../components/Icon.astro';
--- ---
<Base title={`مسودہ: ${rev.work_title}`}> <Base title={`مسودہ: ${rev.work_title}`}>
<h1>{order && 'ترتیب: '}{tagging && 'ٹیگ: '}{ebook && 'ای بک: '}{rev.work_title}</h1> <h1>{order && 'ترتیب: '}{tagging && 'ٹیگ: '}{ebook && 'ای بک: '}{rev.work_title}</h1>
@ -62,8 +65,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<EbookFields v={ebMeta} /> <EbookFields v={ebMeta} />
<textarea name="content" id="eb-content" hidden>{rev.content}</textarea> <textarea name="content" id="eb-content" hidden>{rev.content}</textarea>
<div class="filter-actions"> <div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button> <button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button> <button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div> </div>
</form> </form>
) : may.save && tagging ? ( ) : may.save && tagging ? (
@ -92,8 +95,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<p><button type="button" class="tool" id="tag-add">+ ٹیگ</button></p> <p><button type="button" class="tool" id="tag-add">+ ٹیگ</button></p>
<textarea name="content" id="tag-content" hidden>{rev.content}</textarea> <textarea name="content" id="tag-content" hidden>{rev.content}</textarea>
<div class="filter-actions"> <div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button> <button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button> <button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div> </div>
</form> </form>
) : may.save && order ? ( ) : may.save && order ? (
@ -115,8 +118,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
</ol> </ol>
<textarea name="content" id="order-content" hidden>{rev.content}</textarea> <textarea name="content" id="order-content" hidden>{rev.content}</textarea>
<div class="filter-actions"> <div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button> <button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button> <button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div> </div>
</form> </form>
) : may.save ? ( ) : may.save ? (
@ -160,8 +163,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<textarea name="content" id="mod-content" data-urdu rows="22" dir="rtl" spellcheck="false">{rev.content}</textarea> <textarea name="content" id="mod-content" data-urdu rows="22" dir="rtl" spellcheck="false">{rev.content}</textarea>
</label> </label>
<div class="filter-actions"> <div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button> <button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button> <button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div> </div>
</form> </form>
) : rev.summary && <p class="center">خلاصہ: {rev.summary}</p>} ) : rev.summary && <p class="center">خلاصہ: {rev.summary}</p>}
@ -186,7 +189,7 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
{acts.length > 0 && ( {acts.length > 0 && (
<form method="post" class="account-form mod-actions"> <form method="post" class="account-form mod-actions">
<label>تبصرہ (واپس بھیجنے یا مسترد کرنے کے لیے ضروری)<textarea name="comment" rows="2" data-urdu maxlength="2000"></textarea></label> <label>تبصرہ (واپس بھیجنے یا مسترد کرنے کے لیے ضروری)<textarea name="comment" rows="2" data-urdu maxlength="2000"></textarea></label>
<div class="filter-actions">{acts.map(([a, label]) => <button name="act" value={a} class={`act-${a}`}>{label}</button>)}</div> <div class="filter-actions">{acts.map(([a, label, role, icon]) => <button name="act" value={a} class={`btn ${role} act-${a}`}><Icon name={icon} size={18} /> {label}</button>)}</div>
</form> </form>
)} )}

View File

@ -0,0 +1,22 @@
---
// پسندیدہ لکھاری: the writers (poets and prose writers) the reader saved, apart from their saved texts
import Base from '../layouts/Base.astro';
import LibraryNav from '../components/LibraryNav.astro';
import { asUser, COOKIE } from '../lib/auth';
import { ud } from '../lib/urdu';
const me = Astro.locals.user;
if (!me) return Astro.redirect('/signin?next=/writers');
const lib = (await asUser(Astro.cookies.get(COOKIE)!.value, '/api/library')).data;
const writers = lib.poets.filter((p: any) => p.poet);
---
<Base title="پسندیدہ لکھاری">
<h1>پسندیدہ لکھاری</h1>
<LibraryNav current="writers" />
{writers.length === 0
? <p class="muted center">ابھی کوئی لکھاری محفوظ نہیں۔ کسی شاعر یا ادیب کے صفحے پر "محفوظ کریں" دبائیں۔</p>
: <>
<p class="muted center">{ud(writers.length)} لکھاری</p>
<div class="poets">{writers.map((p: any) => <a class="poet-card" href={p.poet.url} title={p.poet.name}><span class="name">{p.poet.name}</span></a>)}</div>
</>}
</Base>

View File

@ -4,6 +4,8 @@
--bg: #fbf6ea; --paper: #fffdf7; --poem: #f6edd6; --inner: #f6efe3; --nested: #eee3d0; --bg: #fbf6ea; --paper: #fffdf7; --poem: #f6edd6; --inner: #f6efe3; --nested: #eee3d0;
--ink: #17130f; --muted: #635543; --lapis: #0d1d30; --gold: #b88828; --gold-light: #dfcd9f; --ink: #17130f; --muted: #635543; --lapis: #0d1d30; --gold: #b88828; --gold-light: #dfcd9f;
--gold-subtle: #f5edd8; --border: #b8a78c; --brand: #bf3f2a; --gold-subtle: #f5edd8; --border: #b8a78c; --brand: #bf3f2a;
/* roles: primary actions (filled), confirming (approve, publish), destructive (log out, delete, reject) */
--action: #1f4e79; --action-ink: #fff; --ok: #2e7d32; --danger: #b3261e;
--font: 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', serif; --verse-lh: 2.1; --font: 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', serif; --verse-lh: 2.1;
color-scheme: light; color-scheme: light;
} }
@ -12,12 +14,14 @@
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620; --bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b; --ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
--gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark; --gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark;
--action: #7db3e8; --action-ink: #0d1520; --ok: #7cc47f; --danger: #f07a6e;
} }
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620; --bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b; --ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
--gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark; --gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark;
--action: #7db3e8; --action-ink: #0d1520; --ok: #7cc47f; --danger: #f07a6e;
} }
:root.nastaliq { --font: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', serif; --verse-lh: 2.7; } :root.nastaliq { --font: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', serif; --verse-lh: 2.7; }
@ -32,16 +36,14 @@ a:hover { color: var(--brand); }
.site-header .wrap { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-top: 12px; } .site-header .wrap { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-top: 12px; }
.wordmark { font-family: 'Noto Nastaliq Urdu', serif; font-size: 2rem; line-height: 1.5; color: var(--brand); font-weight: 700; } .wordmark { font-family: 'Noto Nastaliq Urdu', serif; font-size: 2rem; line-height: 1.5; color: var(--brand); font-weight: 700; }
.search-form { flex: 1; min-width: 200px; display: flex; position: relative; } .search-form { flex: 1; min-width: 200px; display: flex; position: relative; }
.search-form input, .toggles button { .search-form input {
font: inherit; border: 1.5px solid var(--border); background: var(--inner); color: var(--ink); border-radius: 10px; font: inherit; border: 1.5px solid var(--border); background: var(--inner); color: var(--ink); border-radius: 10px;
} }
.search-form input { flex: 1; padding: 4px 14px; padding-inline-end: 106px; } .search-form input { flex: 1; padding: 4px 14px; padding-inline-end: 106px; }
.search-form input:focus { outline: none; border-color: var(--gold); } .search-form input:focus { outline: none; border-color: var(--gold); }
.toggles { display: flex; gap: 6px; } .toggles { display: flex; gap: 6px; align-items: center; } /* icon buttons with tooltips */
.toggles button { padding: 2px 12px; cursor: pointer; font-size: .85rem; }
/* controls keep Naskh at normal height whatever reading font is chosen */ /* controls keep Naskh at normal height whatever reading font is chosen */
.search-form input, .toggles button { font-family: 'Noto Naskh Arabic', serif; line-height: 1.8; } .search-form input { font-family: 'Noto Naskh Arabic', serif; line-height: 1.8; }
.toggles button:hover { border-color: var(--brand); }
main { padding: 16px 0 48px; } main { padding: 16px 0 48px; }
h1, h2 { line-height: 1.6; margin: .2em 0 .4em; font-weight: 700; text-align: center; color: var(--lapis); } h1, h2 { line-height: 1.6; margin: .2em 0 .4em; font-weight: 700; text-align: center; color: var(--lapis); }
@ -329,8 +331,6 @@ h1 + .muted { text-align: center; margin-top: 0; }
.mod-edit textarea { font-size: 1.05rem; line-height: 2; min-height: 26em; resize: vertical; } .mod-edit textarea { font-size: 1.05rem; line-height: 2; min-height: 26em; resize: vertical; }
.mod-edit textarea:focus, .mod-edit input:focus { outline: none; border-color: var(--gold); } .mod-edit textarea:focus, .mod-edit input:focus { outline: none; border-color: var(--gold); }
.mod-actions { max-width: 700px; } .mod-actions { max-width: 700px; }
.mod-actions .act-publish { border-color: #3d8b4f; color: #3d8b4f; }
.mod-actions .act-reject, .mod-actions .act-return { border-color: var(--brand); color: var(--brand); }
.diff { max-width: 900px; margin: 0 auto; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; } .diff { max-width: 900px; margin: 0 auto; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.diff div { padding: 2px 12px; white-space: pre-wrap; line-height: 1.9; } .diff div { padding: 2px 12px; white-space: pre-wrap; line-height: 1.9; }
.diff div span { display: inline-block; width: 1.2em; font-weight: 700; } .diff div span { display: inline-block; width: 1.2em; font-weight: 700; }
@ -457,3 +457,26 @@ h1 + .muted { text-align: center; margin-top: 0; }
.eb-co .author-find input { width: 100%; } .eb-co .author-find input { width: 100%; }
.eb-people { color: var(--muted); } .eb-people { color: var(--muted); }
.account-form.ebook-form, .mod-edit.account-form { max-width: 560px; } .account-form.ebook-form, .mod-edit.account-form { max-width: 560px; }
/* buttons by role (owner request): one shape, colour says what it does
.btn secondary (gold outline) .btn.primary main action (filled)
.btn.ok confirms (approve, publish) .btn.danger destructive (log out, delete, reject)
.icon-btn icon only, round, with a tooltip (title) and aria-label */
.btn, .mod-edit-btn, .save-like { display: inline-flex; align-items: center; gap: 6px; font: inherit; font-family: 'Noto Naskh Arabic', serif;
font-size: .85rem; line-height: 1.8; padding: 2px 14px; border: 1.5px solid var(--gold); border-radius: 999px; background: var(--inner);
color: var(--ink); cursor: pointer; text-decoration: none; }
.btn:hover, .mod-edit-btn:hover, .save-like:hover { border-color: var(--brand); color: var(--brand); }
.btn.primary { background: var(--action); border-color: var(--action); color: var(--action-ink); }
.btn.primary:hover { filter: brightness(1.12); color: var(--action-ink); }
.btn.ok { border-color: var(--ok); color: var(--ok); }
.btn.ok:hover { background: var(--ok); color: var(--bg); }
.btn.danger { border-color: var(--danger); color: var(--danger); }
.btn.danger:hover { background: var(--danger); color: #fff; }
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; border: 1.5px solid var(--border);
border-radius: 50%; background: var(--inner); color: var(--ink); cursor: pointer; text-decoration: none; }
.icon-btn:hover, .icon-btn[aria-current='page'] { border-color: var(--brand); color: var(--brand); }
.icon-btn.danger { color: var(--danger); border-color: var(--danger); }
.icon-btn.danger:hover { background: var(--danger); color: #fff; }
.icon { flex: none; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.icon-btn.small { width: 30px; height: 30px; }