History

fast search, readable UI, fix broken group links

Search - query parser: phrases, prefix*, -exclude, OR, from:, subject:, group:, after:/before: date bounds - meta_fts index over subject + from_addr (npm run index:meta) - drop the FTS->messages join from counts (11s -> ~25ms for "unix"); group scope seeks by rowid range instead of joining - highlighted snippets, relevance/newest/oldest sort, timing, clear errors UI - group links with @, spaces, commas no longer 404 (decode route params) - home: hierarchy overview, largest/all views, name filter, sortable columns - group: oldest/newest toggle, scoped search, windowed pager + jump-to-page - message: quote-depth tinting, dimmed signatures, clickable URLs, author/thread links, index link returns to the right page - decode RFC 2047 encoded subjects/authors - fixed table layout (no column overflow), 14px text, mobile layout - keyboard: j/k next/prev, u up, / search; help, 404, error, loading pages

Kris Yotam committed 1 month agomain

Commit c20846c1 parent 4ca247219 files · +2058 −474Browse files

Showing the changes to components (2 of 19 files). Show all

Unified · Split

2 files
  1. components/Pager.tsx +87 −0
  2. components/Window.tsx +42 −0
components/Pager.tsx+87−0
@@ -0,0 +1,87 @@
1+import Link from "next/link";
2+import { fmt } from "@/lib/format";
3+
4+/** Windowed pager: « ‹ 1 … 4 5 [6] 7 8 … 120 › » plus a jump-to-page box. */
5+export function Pager({
6+ page,
7+ totalPages,
8+ href,
9+ action,
10+ hidden = {},
11+ label,
12+}: {
13+ page: number;
14+ totalPages: number;
15+ href: (p: number) => string;
16+ /** form action for the jump box */
17+ action: string;
18+ /** extra params the jump box must carry */
19+ hidden?: Record<string, string | undefined>;
20+ label?: string;
21+}) {
22+ if (totalPages <= 1) return label ? <div className="p9-pager"><span className="count">{label}</span></div> : null;
23+
24+ const around = 2;
25+ const pages: (number | "gap")[] = [];
26+ const lo = Math.max(1, page - around);
27+ const hi = Math.min(totalPages, page + around);
28+ if (lo > 1) pages.push(1);
29+ if (lo > 2) pages.push("gap");
30+ for (let p = lo; p <= hi; p++) pages.push(p);
31+ if (hi < totalPages - 1) pages.push("gap");
32+ if (hi < totalPages) pages.push(totalPages);
33+
34+ return (
35+ <nav className="p9-pager" aria-label="Pagination">
36+ {page > 1 ? (
37+ <Link href={href(page - 1)} rel="prev" className="step">
38+ &#9666; prev
39+ </Link>
40+ ) : (
41+ <span className="step dim">&#9666; prev</span>
42+ )}
43+ <span className="pages">
44+ {pages.map((p, i) =>
45+ p === "gap" ? (
46+ <span key={`g${i}`} className="gap">
47+ …
48+ </span>
49+ ) : p === page ? (
50+ <span key={p} className="cur" aria-current="page">
51+ {fmt(p)}
52+ </span>
53+ ) : (
54+ <Link key={p} href={href(p)}>
55+ {fmt(p)}
56+ </Link>
57+ )
58+ )}
59+ </span>
60+ {page < totalPages ? (
61+ <Link href={href(page + 1)} rel="next" className="step">
62+ next &#9656;
63+ </Link>
64+ ) : (
65+ <span className="step dim">next &#9656;</span>
66+ )}
67+ <form action={action} method="GET" className="jump">
68+ {Object.entries(hidden).map(([k, v]) =>
69+ v ? <input key={k} type="hidden" name={k} value={v} /> : null
70+ )}
71+ <label>
72+ page{" "}
73+ <input
74+ type="number"
75+ name="page"
76+ min={1}
77+ max={totalPages}
78+ defaultValue={page}
79+ aria-label="Go to page"
80+ />{" "}
81+ / {fmt(totalPages)}
82+ </label>
83+ </form>
84+ {label && <span className="count">{label}</span>}
85+ </nav>
86+ );
87+}
components/Window.tsx+42−0
@@ -0,0 +1,42 @@
1+import Link from "next/link";
2+import type { ReactNode } from "react";
3+
4+/** A rio-style window: title bar, breadcrumb/tag bar, body. */
5+export function Window({
6+ path,
7+ meta,
8+ crumbs,
9+ children,
10+ wide,
11+}: {
12+ path: string;
13+ meta?: ReactNode;
14+ crumbs?: ReactNode;
15+ children: ReactNode;
16+ wide?: boolean;
17+}) {
18+ return (
19+ <main className={`p9-win${wide ? " wide" : ""}`}>
20+ <header className="p9-win-title">
21+ <h1 className="path" title={path}>
22+ {path}
23+ </h1>
24+ {meta && <span className="meta">{meta}</span>}
25+ </header>
26+ <nav className="p9-bread" aria-label="Breadcrumb">
27+ <span className="crumbs">
28+ <Link href="/">usenet</Link>
29+ {crumbs}
30+ </span>
31+ <span className="nav">
32+ <Link href="/">groups</Link>
33+ <Link href="/search">search</Link>
34+ <Link href="/help">help</Link>
35+ </span>
36+ </nav>
37+ {children}
38+ </main>
39+ );
40+}
41+
42+export const Sep = () => <span className="sep"> / </span>;