Add performance optimizations and component refactors
- Cache read-heavy public API routes via redisCache (leaderboard, values, shop, articles, photos, guilds, teams, staff, users, home, radio, badges) - Add single-flight and bounded-memory cache layer with unit tests - Parallelize independent DB queries on search, rares, shop, staff, polls and profile pages - Push radio points leaderboard aggregation to SQL with a LIMIT - Split studio-client and import-furni-client into focused modules - Clean up next.config.ts
This commit is contained in:
1 parent
54f2bc5e0c
commit
f285a7cd98
42 files changed
+1616
-1205
No files matched your search
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, ChevronRight, FolderTree } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TreeNode } from "@/types/catalog";
|
||||
|
||||
export function CatalogRail({
|
||||
tree,
|
||||
selectedId,
|
||||
onSelect,
|
||||
}: {
|
||||
tree: TreeNode[];
|
||||
selectedId: number | null;
|
||||
onSelect: (id: number | null) => void;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState<Set<number>>(new Set());
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const hidden = new Set<number>();
|
||||
for (const node of tree) {
|
||||
if (hidden.has(node.parentId)) {
|
||||
hidden.add(node.id);
|
||||
continue;
|
||||
}
|
||||
if (collapsed.has(node.parentId)) {
|
||||
hidden.add(node.id);
|
||||
}
|
||||
}
|
||||
return tree.filter((n) => !hidden.has(n.id));
|
||||
}, [tree, collapsed]);
|
||||
|
||||
const toggle = (id: number) => {
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-h-0 w-64 shrink-0 flex-col border-r border-[var(--admin-border)] bg-[var(--admin-surface)]">
|
||||
<div className="flex items-center gap-2 border-b border-[var(--admin-border)] px-3 py-2.5">
|
||||
<FolderTree size={14} className="text-[var(--admin-accent)]" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-[var(--admin-text-muted)]">
|
||||
Catalog
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
|
||||
{visible.length === 0 ? (
|
||||
<p className="px-2 py-4 text-xs text-[var(--admin-text-muted)]">
|
||||
No catalog pages found.
|
||||
</p>
|
||||
) : (
|
||||
visible.map((node) => {
|
||||
const isRoot = node.parentId <= 0;
|
||||
const hasChildren = node.childCount > 0;
|
||||
const isCollapsed = collapsed.has(node.id);
|
||||
return (
|
||||
<button
|
||||
key={node.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (hasChildren) toggle(node.id);
|
||||
onSelect(selectedId === node.id ? null : node.id);
|
||||
}}
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-1.5 rounded-md px-1.5 py-1.5 text-left text-xs transition-colors",
|
||||
selectedId === node.id
|
||||
? "bg-[var(--admin-accent)]/15 text-[var(--admin-text)]"
|
||||
: "text-[var(--admin-text-muted)] hover:bg-[var(--admin-accent)]/10 hover:text-[var(--admin-text)]",
|
||||
)}
|
||||
style={{ paddingLeft: `${6 + node.depth * 12}px` }}
|
||||
>
|
||||
{hasChildren ? (
|
||||
isCollapsed ? (
|
||||
<ChevronRight size={12} className="shrink-0" />
|
||||
) : (
|
||||
<ChevronDown size={12} className="shrink-0" />
|
||||
)
|
||||
) : (
|
||||
<span className="w-3 shrink-0" />
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{isRoot ? (
|
||||
<span className="font-semibold">{node.caption}</span>
|
||||
) : (
|
||||
node.caption
|
||||
)}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-full bg-[var(--admin-accent)]/10 px-1.5 text-[0.6rem] font-semibold text-[var(--admin-accent)]">
|
||||
{node.itemCount}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user