feat: vani of the day

This commit is contained in:
gosangam
2026-08-31 13:40:08 +05:30
parent 53e00863f7
commit 082815fc53
2 changed files with 61 additions and 5 deletions
+45 -1
View File
@@ -36,6 +36,7 @@ export default function VaniPage() {
rows: [], rows: [],
}); });
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [link, setLink] = useState("");
const refresh = () => const refresh = () =>
vani vani
@@ -85,6 +86,27 @@ export default function VaniPage() {
} }
}; };
// Pasting the link is the primary path: it names one exact item, where
// search can only match words. A slug searched as text finds its
// neighbours, not itself.
const pinByLink = async () => {
const url = link.trim();
if (!url) return;
setBusy(true);
try {
const pick = await vani.setByURL({ day, url });
toast.success(`Set ${pick.payload.title} for ${day}`);
setLink("");
await refresh();
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Could not use that link",
);
} finally {
setBusy(false);
}
};
const clear = async (pick: VaniPick) => { const clear = async (pick: VaniPick) => {
if ( if (
!window.confirm( !window.confirm(
@@ -121,7 +143,29 @@ export default function VaniPage() {
</header> </header>
<section className="space-y-3 rounded-xl border border-border p-4"> <section className="space-y-3 rounded-xl border border-border p-4">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-2">
<input
value={link}
onChange={(e) => setLink(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && void pinByLink()}
placeholder="Paste a link — /transcriptions/720720sb-london"
className={`${field} min-w-0 flex-1`}
/>
<button
type="button"
disabled={busy || !link.trim()}
onClick={pinByLink}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
Use this link
</button>
</div>
<p className="text-xs text-muted-foreground">
Or search by words. A slug only matches its own words, so paste the
link when you know exactly which item you want.
</p>
<div className="flex flex-wrap items-center gap-3 border-t border-border pt-3">
<label className="text-sm text-muted-foreground"> <label className="text-sm text-muted-foreground">
Date Date
<input <input
+16 -4
View File
@@ -535,14 +535,26 @@ export const vani = {
clear: (day: string) => clear: (day: string) =>
call<void>(`/admin/vani/${encodeURIComponent(day)}`, { method: "DELETE" }), call<void>(`/admin/vani/${encodeURIComponent(day)}`, { method: "DELETE" }),
/** Pin whatever an app link points at. The only reliable way to choose a
* specific item: slugs are not indexed, so searching one matches on its
* words instead ("720720sb-london" → anything mentioning London). */
setByURL: (input: { day: string; url: string }) =>
call<VaniPick>("/admin/vani/by-url", {
method: "POST",
body: JSON.stringify(input),
}).then((r) => r.data),
/** /**
* Search the three vāṇī-eligible collections. Uses the public search * Search the vāṇī-eligible collections.
* endpoint — the same index the app searches — and keeps only hits that map *
* to a pinnable kind. * SCOPED server-side via `type`, not filtered here: `type=all` blends every
* collection and ranks by relevance, so the top hits are dominated by
* letters and glossary entries and a client-side filter throws the whole
* page away — which is exactly what made this look broken.
*/ */
search: async (q: string): Promise<VaniCandidate[]> => { search: async (q: string): Promise<VaniCandidate[]> => {
const { data } = await call<{ results: VaniCandidate[] }>( const { data } = await call<{ results: VaniCandidate[] }>(
`/search?type=all&limit=20&q=${encodeURIComponent(q)}`, `/search?type=verses,prose,transcriptions&limit=20&q=${encodeURIComponent(q)}`,
); );
return (data?.results ?? []).filter( return (data?.results ?? []).filter(
(r) => r.doc_id && vaniKindFor(r.collection), (r) => r.doc_id && vaniKindFor(r.collection),