"use client"; /** * /vani — choose the Vāṇī of the Day. * * Days without a pinned pick are left alone: the BFF chooses one at random on * the day's first request, avoiding anything served in the last 90 days. This * page only overrides — so an empty schedule is the normal, healthy state, not * something to fill in. * * Pinning sends only a reference. The BFF rebuilds the display blob with the * same builders the random picker uses, so a pinned pick renders exactly like * an automatic one. */ import { useEffect, useState } from "react"; import { toast } from "sonner"; import { vani, vaniKindFor, type VaniCandidate, type VaniPick, } from "@/lib/api"; /** Today in UTC — picks are keyed on a UTC calendar day. */ function todayUTC(): string { return new Date().toISOString().slice(0, 10); } export default function VaniPage() { const [schedule, setSchedule] = useState(null); const [day, setDay] = useState(todayUTC()); const [query, setQuery] = useState(""); const [results, setResults] = useState<{ q: string; rows: VaniCandidate[] }>({ q: "", rows: [], }); const [busy, setBusy] = useState(false); const refresh = () => vani .schedule() .then(setSchedule) .catch(() => { toast.error("Could not load the schedule"); setSchedule([]); }); useEffect(() => { void refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const q = query.trim(); const searchable = q.length >= 2; // Results carry the query they belong to, so a stale page simply stops // matching rather than needing to be cleared. const visible = results.q === q ? results.rows : []; useEffect(() => { if (!searchable) return; const timer = setTimeout(() => { vani .search(q) .then((rows) => setResults({ q, rows })) .catch(() => setResults({ q, rows: [] })); }, 300); return () => clearTimeout(timer); }, [q, searchable]); const pin = async (candidate: VaniCandidate) => { const kind = vaniKindFor(candidate.collection); if (!kind) return; setBusy(true); try { await vani.set({ day, kind, ref_id: candidate.doc_id }); toast.success(`Set for ${day}`); setQuery(""); setResults({ q: "", rows: [] }); await refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : "Could not set it"); } finally { setBusy(false); } }; const clear = async (pick: VaniPick) => { if ( !window.confirm( `Clear ${pick.day}? A random pick will be chosen instead.`, ) ) { return; } setBusy(true); try { await vani.clear(pick.day); toast.success("Cleared"); await refresh(); } catch (err) { toast.error(err instanceof Error ? err.message : "Could not clear"); } finally { setBusy(false); } }; const field = "rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-primary/40"; return (

Vāṇī of the Day

Pin a verse, chapter or lecture to a date. Days you leave alone get a random pick that avoids anything shown in the last 90 days.

setQuery(e.target.value)} placeholder="Search verses, chapters and lectures" className={`${field} min-w-0 flex-1`} />
{!searchable ? (

Type at least two characters to search.

) : visible.length === 0 ? (

Nothing found.

) : (
    {visible.map((c) => (
  • {c.title}

    {vaniKindFor(c.collection)} · {c.snippet}

  • ))}
)}

Pinned from today onward

{schedule === null ? (

Loading…

) : schedule.length === 0 ? (

Nothing pinned — every day is chosen at random.

) : (
    {schedule.map((p) => (
  • {p.day}

    {p.payload.title}

    {p.payload.label} {p.payload.book_title ? ` · ${p.payload.book_title}` : ""}

  • ))}
)}
); }