import { useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router'; import { useClient } from 'hooks/useClient'; import { Input } from '@/components/ui/input'; import { ExternalLink, FileText } from 'lucide-react'; import { ALL } from './CollectionsPanel'; /** * Your saved links, as the calling Officer user. * * Every request goes through `/api/linkwarden/api/*` — the authenticated door, which finds the caller's * mapped account and puts THEIR token on the request. This panel holds no credential and could not send * one if it wanted to, which is the property the whole plugin exists for. * * ── Search is Linkwarden's, not ours ── * * `searchQueryString` searches names, descriptions, tags AND the archived page text, which is the thing * a bookmark bar cannot do. Re-implementing it here would mean pulling every link to the browser to * filter it, and would still only see the fields we happened to fetch. Verified against the live * instance: 50 links unfiltered, 8 for "arch", 0 for a string that matches nothing. */ type Link = { id: number; name: string; url: string; description?: string; textContent?: string; collection?: { id: number; name: string }; tags?: { id: number; name: string }[]; createdAt?: string; }; export const LinksPanel = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [params] = useSearchParams(); const collection = params.get('collection') ?? ALL; const [query, setQuery] = useState(''); const [rows, setRows] = useState(null); const [error, setError] = useState(null); const load = useCallback(async (collectionId: string, search: string) => { const qs = new URLSearchParams(); if (collectionId !== ALL) qs.set('collectionId', collectionId); if (search.trim()) qs.set('searchQueryString', search.trim()); try { const body = await clientRef.current.get<{ response: Link[] }>( `/linkwarden/api/v1/links${qs.toString() ? `?${qs}` : ''}`, ); setRows(body.response ?? []); setError(null); } catch (failure) { setError(failure instanceof Error ? failure.message : 'Could not read your links.'); setRows(null); } }, []); /* Debounced, because this types straight into a request. 250 ms is chosen against what it is: somebody typing a word, who wants the list to move while they type rather than after they stop. Long enough that a five-letter word is one query instead of five. */ useEffect(() => { const timer = setTimeout(() => void load(collection, query), 250); return () => clearTimeout(timer); }, [collection, query, load]); return (
setQuery(event.target.value)} placeholder="Search names, tags and the text of saved pages…" className="h-8 text-xs" />
{error && (

{error} {/* The state every user starts in, said as an instruction rather than a failure. */} {error.includes('no Linkwarden account') && ' — the owner maps accounts in Settings.'}

)} {rows === null && !error ? ( Loading… ) : rows && rows.length === 0 ? ( {query.trim() ? `Nothing matches “${query.trim()}”.` : 'Nothing saved here yet.'} ) : (
    {rows?.map((link) => (
  • {link.name || link.url}

    {link.url}

    {(link.collection || link.tags?.length) && (

    {link.collection && {link.collection.name}} {link.tags?.map((tag) => ( {tag.name} ))}

    )}
  • ))}
)}
); }; const Centred = ({ children }: { children: React.ReactNode }) => (
{children}
);