142 lines
10 KiB
Plaintext
142 lines
10 KiB
Plaintext
---
|
||
// Home page sections (site permission): rename, add, remove and reorder sections; choose and order the poets of a
|
||
// chosen-poets section (e.g. مقبول شعرا); sort a section by hand, alphabetically or by timeline. Saved all at once.
|
||
import Base from '../../layouts/Base.astro';
|
||
import ModNav from '../../components/ModNav.astro';
|
||
import { asUser, COOKIE } from '../../lib/auth';
|
||
import { centuryName } from '../../lib/urdu';
|
||
|
||
const me = Astro.locals.user;
|
||
if (!me) return Astro.redirect(`/signin?next=${Astro.url.pathname}`);
|
||
const token = Astro.cookies.get(COOKIE)!.value;
|
||
let error = '', done = '';
|
||
if (Astro.request.method === 'POST') {
|
||
let sections: unknown = null;
|
||
try { sections = JSON.parse(String((await Astro.request.formData()).get('layout'))); } catch {}
|
||
const r = await asUser(token, '/api/mod/site', { sections });
|
||
if (r.ok) done = 'سرورق محفوظ ہو گیا';
|
||
else error = r.data.error ?? 'کچھ غلط ہو گیا';
|
||
}
|
||
const res = await asUser(token, '/api/mod/site');
|
||
const centuries = Object.fromEntries(Array.from({ length: 16 }, (_, c) => [c, centuryName(c || null)]));
|
||
---
|
||
<Base title="سرورق کے حصے">
|
||
<h1>سرورق کے حصے</h1>
|
||
<ModNav current="site" />
|
||
{!res.ok ? <p class="form-error center" role="alert">{res.data.error ?? 'یہ صفحہ دیکھنے کی اجازت نہیں'}</p> : (
|
||
<>
|
||
{error && <p class="form-error" role="alert">{error}</p>}
|
||
{done && <p class="form-done" role="status">{done} · <a href="/">سرورق دیکھیں</a></p>}
|
||
<p class="muted center">
|
||
حصوں کے نام بدلیں، ↑ ↓ سے ان کی جگہ بدلیں، اور ہر حصے کی ترتیب چنیں: دستی، حروفِ تہجی یا زمانی (پیدائش کے سال سے)۔
|
||
صدی کے حصے میں اس صدی میں پیدا ہونے والے تمام شعرا خود آتے ہیں؛ «منتخب شعرا» کے حصے میں آپ شعرا چن کر ترتیب دیتے ہیں۔
|
||
</p>
|
||
<form method="post" id="site-form">
|
||
<div id="sections"></div>
|
||
<p class="filter-actions">
|
||
<button type="button" class="tool" id="add-list">+ منتخب شعرا کا حصہ</button>
|
||
<select id="add-century" class="tool" aria-label="صدی کا حصہ">
|
||
<option value="">+ صدی کا حصہ</option>
|
||
{Object.entries(centuries).map(([c, name]) => <option value={c}>{name}</option>)}
|
||
</select>
|
||
</p>
|
||
<input type="hidden" name="layout" id="layout" />
|
||
<p class="filter-actions"><button>محفوظ کریں</button> <a href="/">سرورق</a></p>
|
||
</form>
|
||
<script type="application/json" id="site-data" set:html={JSON.stringify({ ...res.data, centuries })} />
|
||
</>
|
||
)}
|
||
<script>
|
||
// the layout lives in `state`; every change redraws the list (a few hundred elements at most)
|
||
const el = document.getElementById('site-data');
|
||
if (el) {
|
||
const data = JSON.parse(el.textContent!);
|
||
const names = new Map<number, string>(data.poets.map((p: any) => [p.id, p.name]));
|
||
const state: any[] = data.sections.map((s: any) => ({ title: s.title ?? '', century: s.century, sort: s.sort, poets: [...s.poets] }));
|
||
const host = document.getElementById('sections')!;
|
||
const norm = (s: string) => s.normalize('NFC').replace(/[ً-ْٰٔ]/g, '').replace(/[يى]/g, 'ی').replace(/ك/g, 'ک').replace(/[هۂ]/g, 'ہ');
|
||
// arrows, ✕ and ہٹائیں are drawn as icons (the same Lucide paths as Icon.astro); the words stay as tooltip and label
|
||
const ICON: Record<string, string> = { '↑': '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>', '↓': '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>', '✕': '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>', 'ہٹائیں': '<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"/>' };
|
||
const btn = (label: string, title: string, fn: () => void) => {
|
||
const b = document.createElement('button'); b.type = 'button'; b.className = 'tool'; b.title = title;
|
||
if (ICON[label]) { b.innerHTML = `<svg class="icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON[label]}</svg>`; b.setAttribute('aria-label', title); }
|
||
else b.textContent = label;
|
||
b.onclick = () => { fn(); draw(); }; return b;
|
||
};
|
||
// a box with the Urdu keyboard and Roman typing buttons inside it, as in search (Base.astro handles them)
|
||
const KB = '<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>';
|
||
// a redrawn box shows the keyboard/Roman state only if it is the box being typed in
|
||
let typing = '';
|
||
host.addEventListener('focusin', (e) => { const t = e.target as HTMLElement; if (t.matches('input[data-urdu]')) typing = t.id; });
|
||
const pressed = (header: string, id: string) => (id === typing && document.getElementById(header)?.getAttribute('aria-pressed')) || 'false';
|
||
const keyed = (input: HTMLInputElement, id: string, cls = '') => {
|
||
input.id = id; input.setAttribute('data-urdu', '');
|
||
const wrap = document.createElement('div'); wrap.className = `in-box-field ${cls}`;
|
||
const rom = document.createElement('button'); rom.type = 'button'; rom.className = 'in-box rom'; rom.textContent = 'اب';
|
||
rom.title = 'رومن حروف سے اردو'; rom.dataset.urduRom = id; rom.setAttribute('aria-pressed', pressed('rom-toggle', id));
|
||
const kb = document.createElement('button'); kb.type = 'button'; kb.className = 'in-box'; kb.innerHTML = KB;
|
||
kb.title = 'اردو کی بورڈ'; kb.setAttribute('aria-label', 'اردو کی بورڈ'); kb.dataset.urduKb = id; kb.setAttribute('aria-pressed', pressed('kb-toggle', id));
|
||
wrap.append(input, rom, kb);
|
||
return wrap;
|
||
};
|
||
const move = (xs: any[], i: number, d: number) => { const j = i + d; if (j >= 0 && j < xs.length) [xs[i], xs[j]] = [xs[j], xs[i]]; };
|
||
const SORTS: [string, string][] = [['manual', 'دستی'], ['alpha', 'حروفِ تہجی'], ['timeline', 'زمانی']];
|
||
|
||
function draw() {
|
||
host.replaceChildren(...state.map((s, i) => {
|
||
const box = document.createElement('fieldset'); box.className = 'site-section';
|
||
const head = document.createElement('div'); head.className = 'site-head';
|
||
const title = document.createElement('input'); title.value = s.title;
|
||
title.placeholder = s.century === null ? 'حصے کا نام' : data.centuries[s.century]; title.setAttribute('aria-label', 'حصے کا نام');
|
||
title.oninput = () => (s.title = title.value);
|
||
const sort = document.createElement('select'); sort.className = 'tool'; sort.setAttribute('aria-label', 'ترتیب');
|
||
for (const [v, l] of SORTS) if (v !== 'manual' || s.century === null) sort.append(new Option(l, v, false, s.sort === v));
|
||
sort.onchange = () => { s.sort = sort.value; draw(); };
|
||
const kind = document.createElement('small'); kind.className = 'muted';
|
||
kind.textContent = s.century === null ? `منتخب شعرا (${s.poets.length})` : `صدی: ${data.centuries[s.century]}`;
|
||
head.append(keyed(title, `site-title-${i}`, 'site-title'), sort, kind,
|
||
btn('↑', 'حصہ اوپر', () => move(state, i, -1)), btn('↓', 'حصہ نیچے', () => move(state, i, 1)),
|
||
btn('ہٹائیں', 'یہ حصہ ہٹائیں', () => { if (confirm('یہ حصہ ہٹا دیں؟')) state.splice(i, 1); }));
|
||
box.append(head);
|
||
if (s.century === null) box.append(poetList(s, i));
|
||
return box;
|
||
}));
|
||
}
|
||
|
||
// a chosen-poets section: its poets (↑ ↓ ✕ when sorted by hand) and a name box to add one
|
||
function poetList(s: any, i: number) {
|
||
const wrap = document.createElement('div');
|
||
const ol = document.createElement('ol'); ol.className = 'site-poets';
|
||
s.poets.forEach((id: number, j: number) => {
|
||
const li = document.createElement('li'); li.append(names.get(id) ?? String(id));
|
||
if (s.sort === 'manual') li.append(btn('↑', 'اوپر', () => move(s.poets, j, -1)), btn('↓', 'نیچے', () => move(s.poets, j, 1)));
|
||
li.append(btn('✕', 'ہٹائیں', () => s.poets.splice(j, 1)));
|
||
ol.append(li);
|
||
});
|
||
const find = document.createElement('div'); find.className = 'author-find';
|
||
const box = document.createElement('input'); box.placeholder = 'شاعر یا ادیب کا نام لکھ کر شامل کریں'; box.autocomplete = 'off';
|
||
const list = document.createElement('div'); list.className = 'author-suggest'; list.hidden = true;
|
||
box.oninput = () => {
|
||
const t = norm(box.value.trim());
|
||
const hits = t ? data.poets.filter((p: any) => !s.poets.includes(p.id) && norm(p.name).includes(t)).slice(0, 10) : [];
|
||
// after adding, the (redrawn) name box keeps the focus for the next name
|
||
list.replaceChildren(...hits.map((p: any) => btn(p.name, 'شامل کریں', () => { s.poets.push(p.id); setTimeout(() => document.getElementById(`site-find-${i}`)?.focus()); })));
|
||
list.hidden = !hits.length;
|
||
};
|
||
box.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); (list.querySelector('button') as HTMLButtonElement | null)?.click(); } };
|
||
find.append(keyed(box, `site-find-${i}`), list);
|
||
wrap.append(ol, find);
|
||
return wrap;
|
||
}
|
||
|
||
document.getElementById('add-list')!.onclick = () => { state.push({ title: '', century: null, sort: 'manual', poets: [] }); draw(); host.lastElementChild?.querySelector('input')?.focus(); };
|
||
const addC = document.getElementById('add-century') as HTMLSelectElement;
|
||
addC.onchange = () => { if (addC.value !== '') state.push({ title: '', century: Number(addC.value), sort: 'timeline', poets: [] }); addC.value = ''; draw(); };
|
||
document.getElementById('site-form')!.addEventListener('submit', () => {
|
||
(document.getElementById('layout') as HTMLInputElement).value = JSON.stringify(state);
|
||
});
|
||
draw();
|
||
}
|
||
</script>
|
||
</Base>
|