"use client"; import { useState, useEffect, useRef, useCallback, type ReactNode } from "react"; import { createShortLink } from "@/lib/share"; // --------------------------------------------------------------------------- // Kautilya Icon // --------------------------------------------------------------------------- export function KautilyaIcon({ className }: { className?: string }) { return ( ); } // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type Position = { x: number; y: number }; export type ToolbarAction = { key: string; label: string; icon: ReactNode; onClick: () => void; className?: string; }; // --------------------------------------------------------------------------- // Hook: useMediaQuery // --------------------------------------------------------------------------- export function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(() => { if (typeof window === "undefined") return false; return window.matchMedia(query).matches; }); useEffect(() => { const mq = window.matchMedia(query); const handler = (e: MediaQueryListEvent) => setMatches(e.matches); mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, [query]); return matches; } // --------------------------------------------------------------------------- // Hook: copy to clipboard with success feedback // --------------------------------------------------------------------------- // Match a URL inside the copy payload that carries a `?hl=` highlight // fragment. Used by useCopyFeedback to transparently swap such URLs for the // short `/share/` form before writing to the clipboard. The native // ⌘C / Ctrl+C path goes through a different (synchronous) handler in // text-selection-toolbar.tsx and is intentionally not affected. // // The regex stops at whitespace or trailing punctuation that wouldn't appear // in a real URL (`>`, `]`, etc.), so it works against multi-line copy // payloads like the source-attribution block produced by formatCopyText(). const HL_URL_RE = /https?:\/\/[^\s<>"'`)\]]+\?hl=[^\s<>"'`)\]]+/g; async function shortenHighlightUrlsIn(text: string): Promise { // Cheap pre-check — most text passed to copy() has no `?hl=` URL. if (!text.includes("?hl=")) return text; const matches = Array.from(text.matchAll(HL_URL_RE)); if (matches.length === 0) return text; let result = text; for (const m of matches) { const longUrl = m[0]; try { const u = new URL(longUrl); const relativePath = u.pathname + u.search + u.hash; const code = await createShortLink(relativePath); if (!code) continue; // BFF failed — leave the long URL alone. const shortUrl = new URL(`/share/${code}`, u.origin).toString(); result = result.replace(longUrl, shortUrl); } catch { // Malformed URL or unexpected error — fall through with the original. } } return result; } export function useCopyFeedback() { const [copied, setCopied] = useState(false); const timerRef = useRef | null>(null); const copy = useCallback(async (text: string) => { let success = false; // Why we don't just `await shortenHighlightUrlsIn(text)` first: the URL // shortening is a network round-trip, and awaiting it consumes the // transient user activation that navigator.clipboard needs. The write // would then silently fail on the *first* click and only succeed on the // second (when the short link is cached and the shorten resolves // synchronously). Instead we hand the clipboard a *Promise* of the text // via ClipboardItem — the browser keeps the gesture alive while the // async work finishes, so a single click copies the shortened URL. if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) { try { const item = new ClipboardItem({ "text/plain": shortenHighlightUrlsIn(text).then( (s) => new Blob([s], { type: "text/plain" }), ), }); await navigator.clipboard.write([item]); success = true; } catch { // ClipboardItem-with-promise unsupported or rejected — fall through. } } if (!success) { // Legacy path: now that ClipboardItem failed, await the text and try // the synchronous fallbacks. (May still need a second click on very old // browsers, but those lack ClipboardItem entirely.) const finalText = await shortenHighlightUrlsIn(text); try { await navigator.clipboard.writeText(finalText); success = true; } catch { // Fallback for older browsers without the async clipboard API. const ta = document.createElement("textarea"); ta.value = finalText; ta.style.position = "fixed"; ta.style.top = "0"; ta.style.left = "0"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand("copy"); success = true; } catch { // silent fail } document.body.removeChild(ta); } } if (success) { setCopied(true); if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => setCopied(false), 1500); } }, []); useEffect(() => { return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, []); return { copied, copy }; } // --------------------------------------------------------------------------- // Pseudo collection data (placeholder for future backend) // --------------------------------------------------------------------------- export const PSEUDO_COLLECTIONS = [ { id: "favorites", name: "Favorites", count: 12 }, { id: "study-notes", name: "Study Notes", count: 8 }, { id: "share-worthy", name: "Share Worthy", count: 23 }, { id: "memorize", name: "To Memorize", count: 5 }, { id: "daily-reading", name: "Daily Reading", count: 17 }, ];