feat(api, i18n): add agent_node_id to various admin queries and enhance multi-language support
Introduced the agent_node_id field in AdminDrawListQuery, AdminPlayerListQuery, AdminSettlementBatchListQuery, TicketItemsListQuery, and TransferOrderListQuery to improve filtering capabilities. Updated the admin-breadcrumb and admin-sidebar components to include new translations for agent-related terms in English, Nepali, and Chinese, enhancing the overall user experience and multi-language support across the admin interface.
This commit is contained in:
256
src/components/admin/admin-sidebar-nav.tsx
Normal file
256
src/components/admin/admin-sidebar-nav.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useEffect, useMemo, useState, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
ADMIN_NAV_GROUP_ICON,
|
||||
ADMIN_NAV_GROUP_ORDER,
|
||||
groupAdminNavItems,
|
||||
} from "@/lib/admin-nav-groups";
|
||||
import { adminNavLabel } from "@/lib/admin-nav-label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveAdminNavIcon } from "@/modules/_config/admin-nav-icons";
|
||||
import { ADMIN_BASE, type AdminNavGroup, type AdminNavItem } from "@/modules/_config/admin-nav";
|
||||
|
||||
const NAV_BTN =
|
||||
"h-8 gap-2 px-2.5 py-0 text-[13px] leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground [&_svg]:size-4";
|
||||
const NAV_ACTIVE = "data-active:bg-red-600 data-active:text-white data-active:font-medium data-active:shadow-sm";
|
||||
const SUB_NAV =
|
||||
"h-8 min-h-8 rounded-sm px-2.5 py-0 text-sm leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground data-[size=md]:text-sm data-[size=sm]:text-sm [&>span]:text-sm";
|
||||
|
||||
function isActive(
|
||||
pathname: string,
|
||||
item: { href: string; activeMatchPrefix?: string; segment?: string },
|
||||
): boolean {
|
||||
const { href, activeMatchPrefix, segment } = item;
|
||||
const prefix = activeMatchPrefix ?? href;
|
||||
if (prefix === ADMIN_BASE || prefix === `${ADMIN_BASE}/`) {
|
||||
return pathname === ADMIN_BASE || pathname === `${ADMIN_BASE}/`;
|
||||
}
|
||||
if (segment === "settings") {
|
||||
return pathname === href;
|
||||
}
|
||||
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
||||
}
|
||||
|
||||
function defaultOpenGroups(
|
||||
groups: { group: AdminNavGroup; items: AdminNavItem[] }[],
|
||||
pathname: string,
|
||||
): Record<AdminNavGroup, boolean> {
|
||||
const open = Object.fromEntries(
|
||||
ADMIN_NAV_GROUP_ORDER.map((g) => [g, true]),
|
||||
) as Record<AdminNavGroup, boolean>;
|
||||
|
||||
for (const { group, items } of groups) {
|
||||
if (items.some((item) => isActive(pathname, item))) {
|
||||
open[group] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return open;
|
||||
}
|
||||
|
||||
function NavLeaf({
|
||||
item,
|
||||
pathname,
|
||||
t,
|
||||
}: {
|
||||
item: AdminNavItem;
|
||||
pathname: string;
|
||||
t: TFunction;
|
||||
}): ReactElement {
|
||||
const Icon = resolveAdminNavIcon(item.segment);
|
||||
const active = isActive(pathname, item);
|
||||
const label = adminNavLabel(item.segment, t, item.label);
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
tooltip={label}
|
||||
isActive={active}
|
||||
render={<Link href={item.href} />}
|
||||
className={cn(NAV_BTN, NAV_ACTIVE)}
|
||||
>
|
||||
<Icon data-icon="inline-start" aria-hidden />
|
||||
<span>{label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function NavSubLeaf({
|
||||
item,
|
||||
pathname,
|
||||
t,
|
||||
}: {
|
||||
item: AdminNavItem;
|
||||
pathname: string;
|
||||
t: TFunction;
|
||||
}): ReactElement {
|
||||
const active = isActive(pathname, item);
|
||||
const label = adminNavLabel(item.segment, t, item.label);
|
||||
|
||||
return (
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
size="md"
|
||||
isActive={active}
|
||||
render={<Link href={item.href} />}
|
||||
className={cn(SUB_NAV, NAV_ACTIVE)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
}
|
||||
|
||||
function NavCollapsibleGroup({
|
||||
group,
|
||||
items,
|
||||
pathname,
|
||||
open,
|
||||
onToggle,
|
||||
t,
|
||||
}: {
|
||||
group: AdminNavGroup;
|
||||
items: AdminNavItem[];
|
||||
pathname: string;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
t: TFunction;
|
||||
}): ReactElement {
|
||||
const GroupIcon = ADMIN_NAV_GROUP_ICON[group];
|
||||
const groupLabel = t(`sidebar.group.${group}`, { ns: "common", defaultValue: group });
|
||||
const hasActiveChild = items.some((item) => isActive(pathname, item));
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
type="button"
|
||||
data-open={open ? "" : undefined}
|
||||
isActive={hasActiveChild && !open}
|
||||
className={cn(NAV_BTN, hasActiveChild && !open && NAV_ACTIVE)}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<GroupIcon aria-hidden />
|
||||
<span className="flex-1 truncate">{groupLabel}</span>
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"ml-auto size-4 shrink-0 text-sidebar-foreground/50 transition-transform duration-200",
|
||||
open && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
{open ? (
|
||||
<SidebarMenuSub className="mx-2 gap-0.5 border-sidebar-border/50 px-1.5 py-0.5">
|
||||
{items.map((item) => (
|
||||
<NavSubLeaf key={item.segment} item={item} pathname={pathname} t={t} />
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminSidebarNav({
|
||||
items,
|
||||
}: {
|
||||
items: readonly AdminNavItem[];
|
||||
}): ReactElement {
|
||||
const { t } = useTranslation("common");
|
||||
const pathname = usePathname();
|
||||
const navGroups = useMemo(() => groupAdminNavItems(items), [items]);
|
||||
|
||||
const [openGroups, setOpenGroups] = useState<Record<AdminNavGroup, boolean>>(() =>
|
||||
defaultOpenGroups(navGroups, pathname),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenGroups((prev) => {
|
||||
const next = { ...prev };
|
||||
let changed = false;
|
||||
for (const { group, items: groupItems } of navGroups) {
|
||||
if (groupItems.some((item) => isActive(pathname, item)) && !next[group]) {
|
||||
next[group] = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [pathname, navGroups]);
|
||||
|
||||
const overview = navGroups.find((g) => g.group === "overview");
|
||||
const collapsible = navGroups.filter((g) => g.group !== "overview");
|
||||
|
||||
return (
|
||||
<SidebarMenu className="gap-0.5 px-1.5 py-1.5">
|
||||
{overview?.items.map((item) => (
|
||||
<NavLeaf key={item.segment} item={item} pathname={pathname} t={t} />
|
||||
))}
|
||||
{collapsible.map(({ group, items: groupItems }) => (
|
||||
<NavCollapsibleGroup
|
||||
key={group}
|
||||
group={group}
|
||||
items={groupItems}
|
||||
pathname={pathname}
|
||||
open={openGroups[group] ?? true}
|
||||
onToggle={() =>
|
||||
setOpenGroups((prev) => ({
|
||||
...prev,
|
||||
[group]: !(prev[group] ?? true),
|
||||
}))
|
||||
}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminSidebarNavSkeleton(): ReactElement {
|
||||
const { t } = useTranslation("common");
|
||||
const widths = ["68%", "74%", "58%", "70%", "62%"] as const;
|
||||
|
||||
return (
|
||||
<SidebarMenu className="gap-0.5 px-1.5 py-1.5 motion-safe:opacity-90">
|
||||
<SidebarMenuItem>
|
||||
<div aria-hidden className="flex h-8 items-center gap-2 rounded-md px-2.5">
|
||||
<span className="size-4 rounded-sm bg-white/12 motion-safe:animate-pulse" />
|
||||
<span className="h-2.5 w-14 rounded-full bg-white/12 motion-safe:animate-pulse" />
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
{widths.map((width, i) => (
|
||||
<SidebarMenuItem key={i}>
|
||||
<div aria-hidden className="flex h-8 items-center gap-2 rounded-md px-2.5">
|
||||
<span
|
||||
className="size-4 rounded-sm bg-white/12 motion-safe:animate-pulse"
|
||||
style={{ animationDelay: `${i * 55}ms` }}
|
||||
/>
|
||||
<span
|
||||
className="h-2 rounded-full bg-white/12 motion-safe:animate-pulse"
|
||||
style={{ width, animationDelay: `${i * 55 + 40}ms` }}
|
||||
/>
|
||||
<span className="ml-auto size-3 rounded-sm bg-white/10 motion-safe:animate-pulse" />
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<span className="sr-only">{t("auth.checking")}</span>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user