CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
- Add SWR foundation (lib/fetcher, lib/swr, lib/api) with typed hooks: useSections/useDocuments/useSearch/useRelated/useRevisions/useQueueStatus - Replace ad-hoc useEffect+fetch in Header/Sidebar/CommandMenu/search with SWR (dedup 4s, focus throttle 10s, debounced search 180ms) - Doc page: RelatedGrid + RevisionList as SWR client islands (RSC hero stays) - DocForm: SWR mutate on create/update for live sidebar invalidation - New APIs: /api/related, /api/revisions, /api/tags, /api/queue/status, /api/docs filtering (?section=&status=) - New UI: DocumentCard, SectionCard, SearchBar, TagCloud, QueueBadge, EmptyState, dashboard (/dashboard with QueueStrip live + section stats) - Layout wraps SWRProvider; no TanStack code (SWR-only per spec) - Verified: next build passes (16.3.4 Turbopack), no manual fetch sprawl remains Co-Authored-By: internal-model
89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
import Link from "next/link";
|
|
import { Clock, FileText } from "lucide-react";
|
|
import type { DocumentMeta } from "@mcpedia/types";
|
|
import { getSectionMeta } from "@mcpedia/config/sections";
|
|
|
|
export function DocumentCard({
|
|
doc,
|
|
showSection = false,
|
|
score,
|
|
}: {
|
|
doc: DocumentMeta;
|
|
showSection?: boolean;
|
|
score?: number;
|
|
}) {
|
|
const sInfo = getSectionMeta(doc.section);
|
|
return (
|
|
<Link
|
|
href={`/${doc.slug}`}
|
|
className="group flex items-center justify-between gap-4 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-xl transition-all shadow-xs"
|
|
>
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
<FileText className="w-4 h-4 text-[var(--text-muted)] shrink-0" />
|
|
<div className="min-w-0">
|
|
<h3 className="text-xs sm:text-sm font-semibold text-[var(--text-primary)] group-hover:underline truncate">
|
|
{doc.title}
|
|
</h3>
|
|
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
|
{showSection && (
|
|
<span className="text-[10px] font-mono px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] uppercase font-semibold">
|
|
{sInfo.icon} {doc.section}
|
|
</span>
|
|
)}
|
|
{doc.tags.slice(0, 4).map((t: string) => (
|
|
<span
|
|
key={t}
|
|
className="text-[10px] px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] font-mono"
|
|
>
|
|
#{t}
|
|
</span>
|
|
))}
|
|
{typeof score === "number" && (
|
|
<span className="text-[10px] font-mono text-[var(--text-dim)] bg-[var(--bg-elevated)] px-1.5 py-0.2 rounded">
|
|
{score.toFixed(3)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0 flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
<span>
|
|
{new Date(doc.updatedAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })}
|
|
</span>
|
|
</time>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export function CompactDocCard({ doc, snippet, score }: { doc: DocumentMeta; snippet?: string; score?: number }) {
|
|
const sInfo = getSectionMeta(doc.section);
|
|
return (
|
|
<Link
|
|
href={`/${doc.slug}`}
|
|
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-xl p-4.5 transition-all shadow-xs"
|
|
>
|
|
<div className="flex items-center justify-between gap-3 mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-[10px] font-mono px-1.5 py-0.2 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] uppercase font-semibold">
|
|
{sInfo.icon} {doc.section}
|
|
</span>
|
|
<span className="text-xs text-[var(--text-dim)] font-mono truncate">/{doc.slug}</span>
|
|
</div>
|
|
{typeof score === "number" && (
|
|
<span className="text-xs font-mono text-[var(--text-dim)] bg-[var(--bg-elevated)] px-2 py-0.5 rounded">
|
|
{score.toFixed(3)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h2 className="text-sm font-semibold text-[var(--text-primary)] group-hover:underline mb-1.5">{doc.title}</h2>
|
|
{snippet && (
|
|
<p
|
|
className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
|
dangerouslySetInnerHTML={{ __html: snippet }}
|
|
/>
|
|
)}
|
|
</Link>
|
|
);
|
|
}
|