docs: add ZXDB guide; refresh README & AGENTS
Expand and update documentation to reflect the current app (Registers + ZXDB Explorer), with clear setup and usage instructions. Changes - README: add project overview including ZXDB Explorer; routes tour; ZXDB setup (DB import, helper search tables, readonly role); environment configuration; selected API endpoints; implementation notes (Next 15 async params, Node runtime for mysql2, SSR/ISR usage); links to AGENTS.md and docs/ZXDB.md. - docs/ZXDB.md (new): deep-dive guide covering database preparation, helper tables, environment, Explorer UI, API reference under /api/zxdb, performance approach (helper tables, parallel queries, ISR), troubleshooting, and roadmap. - AGENTS.md: refresh Project Overview/Structure with ZXDB routes and server/client boundaries; document Next.js 15 dynamic params async pattern for pages and API routes; note Drizzle+mysql2, Node runtime, and lookup `text`→`name` mapping; keep commit workflow guidance. - example.env: add reference to docs/ZXDB.md and clarify mysql:// format and setup pointers. Notes - Documentation focuses on the current state of the codebase (what the code does), not a log of agent actions. - Helper SQL at ZXDB/scripts/ZXDB_help_search.sql is required for performant searches. Signed-off-by: Junie@lucy.xalior.com
This commit is contained in:
79
src/app/zxdb/languages/LanguagesSearch.tsx
Normal file
79
src/app/zxdb/languages/LanguagesSearch.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
type Language = { id: string; name: string };
|
||||
type Paged<T> = { items: T[]; page: number; pageSize: number; total: number };
|
||||
|
||||
export default function LanguagesSearch({ initial, initialQ }: { initial?: Paged<Language>; initialQ?: string }) {
|
||||
const router = useRouter();
|
||||
const [q, setQ] = useState(initialQ ?? "");
|
||||
const [data, setData] = useState<Paged<Language> | null>(initial ?? null);
|
||||
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initial) setData(initial);
|
||||
}, [initial]);
|
||||
|
||||
useEffect(() => {
|
||||
setQ(initialQ ?? "");
|
||||
}, [initialQ]);
|
||||
|
||||
function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set("q", q);
|
||||
params.set("page", "1");
|
||||
router.push(`/zxdb/languages?${params.toString()}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Languages</h1>
|
||||
<form className="row gy-2 gx-2 align-items-center" onSubmit={submit}>
|
||||
<div className="col-sm-8 col-md-6 col-lg-4">
|
||||
<input className="form-control" placeholder="Search languages…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-3">
|
||||
{data && data.items.length === 0 && <div className="alert alert-warning">No languages found.</div>}
|
||||
{data && data.items.length > 0 && (
|
||||
<ul className="list-group">
|
||||
{data.items.map((l) => (
|
||||
<li key={l.id} className="list-group-item d-flex justify-content-between align-items-center">
|
||||
<Link href={`/zxdb/languages/${l.id}`}>{l.name}</Link>
|
||||
<span className="badge text-bg-light">{l.id}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
<span>Page {data?.page ?? 1} / {totalPages}</span>
|
||||
<div className="ms-auto d-flex gap-2">
|
||||
<Link
|
||||
className={`btn btn-outline-secondary ${!data || (data.page <= 1) ? "disabled" : ""}`}
|
||||
aria-disabled={!data || data.page <= 1}
|
||||
href={`/zxdb/languages?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.max(1, (data?.page ?? 1) - 1))); return p.toString(); })()}`}
|
||||
>
|
||||
Prev
|
||||
</Link>
|
||||
<Link
|
||||
className={`btn btn-outline-secondary ${!data || (data.page >= totalPages) ? "disabled" : ""}`}
|
||||
aria-disabled={!data || data.page >= totalPages}
|
||||
href={`/zxdb/languages?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.min(totalPages, (data?.page ?? 1) + 1))); return p.toString(); })()}`}
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import LanguageList from "./LanguageList";
|
||||
import { listLanguages } from "@/server/repo/zxdb";
|
||||
import LanguagesSearch from "./LanguagesSearch";
|
||||
import { searchLanguages } from "@/server/repo/zxdb";
|
||||
|
||||
export const metadata = { title: "ZXDB Languages" };
|
||||
|
||||
export const revalidate = 3600;
|
||||
// Depends on searchParams (?q=, ?page=). Force dynamic so each request renders correctly.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function Page() {
|
||||
const items = await listLanguages();
|
||||
return <LanguageList items={items as any} />;
|
||||
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
||||
const sp = await searchParams;
|
||||
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
|
||||
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
|
||||
const initial = await searchLanguages({ q, page, pageSize: 20 });
|
||||
return <LanguagesSearch initial={initial as any} initialQ={q} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user