"use client" import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from "react" import type { ServerStatus, ServerType } from "@/lib/data" import { Flag } from "@/components/flag" import { StatusDot } from "@/components/status-dot" import { Badge } from "@/components/reui/badge" import { IconTile } from "@/components/reui/icon-tile" import { Frame, FrameHeader, FramePanel, FrameTitle, } from "@/components/reui/frame" import { Item, ItemActions, ItemContent, ItemMedia, ItemTitle, } from "@/components/ui/item" import { ScrollArea } from "@/components/ui/scroll-area" import { Spinner } from "@/components/ui/spinner" import { InputGroup, InputGroupAddon, InputGroupInput, } from "@/components/ui/input-group" import { cn } from "@/lib/utils" import { LayersIcon, SearchIcon, ServerIcon } from "lucide-react" /** Preview: https://reui.io/preview/base/list-9 · https://reui.io/docs/components/base/icon-tile · https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/badge */ export const ALL_SERVERS_ID = "all" export interface ServerTileItem { id: string name: string country?: string status?: ServerStatus type?: ServerType count?: number enabled?: boolean selectable?: boolean title?: string site?: string host?: string meta?: string } function typeBadgeVariant( type: ServerType, ): "focus-light" | "info-light" | "success-light" { if (type === "jump-host") return "focus-light" if (type === "home-router") return "success-light" return "info-light" } function typeBadgeLabel(type: ServerType): string { if (type === "jump-host") return "JH" if (type === "home-router") return "HR" return "EN" } function ServerTypeBadge({ type }: { type: ServerType }) { return ( {typeBadgeLabel(type)} ) } function hostnameOf(item: ServerTileItem): string { return item.name || item.host || "" } function matchesQuery(item: ServerTileItem, q: string): boolean { if (!q) return true const hay = [item.name, item.title ?? "", item.host ?? "", item.site ?? "", item.country ?? ""] .join(" ") .toLowerCase() return hay.includes(q) } function isSelectable(item: ServerTileItem): boolean { return item.selectable !== false } function ServerTileRail({ items, selectedId, onSelect, allCount = 0, showHeader = true, showAll = true, showCount = true, showType = true, headerRight, loading = false, className, }: { items: ServerTileItem[] selectedId: string onSelect: (id: string) => void allCount?: number showHeader?: boolean showAll?: boolean showCount?: boolean showType?: boolean headerRight?: ReactNode loading?: boolean className?: string }) { const [query, setQuery] = useState("") const q = query.trim().toLowerCase() const listRef = useRef(null) const filtered = useMemo( () => items.filter((item) => matchesQuery(item, q)), [items, q], ) const uniqueSites = useMemo(() => { const sites = new Set( filtered.map((item) => item.site?.trim()).filter((site): site is string => Boolean(site)), ) return sites.size }, [filtered]) const groupBySite = uniqueSites >= 2 && !q const groups = useMemo(() => { if (!groupBySite) { return [{ site: "", items: filtered }] } const bySite = new Map() for (const item of filtered) { const site = item.site?.trim() || "—" const list = bySite.get(site) if (list) list.push(item) else bySite.set(site, [item]) } return [...bySite.entries()].map(([site, siteItems]) => ({ site, items: siteItems })) }, [filtered, groupBySite]) const showAllTile = showAll && !q const resolvedId = useMemo(() => { if (showAll && selectedId === ALL_SERVERS_ID) return ALL_SERVERS_ID if (items.some((item) => item.id === selectedId)) return selectedId if (showAll) return ALL_SERVERS_ID return items.find(isSelectable)?.id ?? items[0]?.id ?? "" }, [items, selectedId, showAll]) useEffect(() => { if (resolvedId && resolvedId !== selectedId) onSelect(resolvedId) }, [onSelect, resolvedId, selectedId]) const selectableIds = useMemo(() => { const ids: string[] = [] if (showAllTile) ids.push(ALL_SERVERS_ID) for (const item of filtered) { if (isSelectable(item)) ids.push(item.id) } return ids }, [filtered, showAllTile]) function moveSelection(delta: number) { if (selectableIds.length === 0) return const idx = selectableIds.indexOf(resolvedId) const nextIdx = idx < 0 ? delta > 0 ? 0 : selectableIds.length - 1 : Math.min(selectableIds.length - 1, Math.max(0, idx + delta)) const nextId = selectableIds[nextIdx] if (nextId) onSelect(nextId) } function handleListKeyDown(event: KeyboardEvent) { if (loading) return if (event.key === "ArrowDown") { event.preventDefault() moveSelection(1) return } if (event.key === "ArrowUp") { event.preventDefault() moveSelection(-1) return } if (event.key === "Home") { event.preventDefault() if (selectableIds[0]) onSelect(selectableIds[0]) return } if (event.key === "End") { event.preventDefault() const last = selectableIds[selectableIds.length - 1] if (last) onSelect(last) } } const allItem: ServerTileItem = { id: ALL_SERVERS_ID, name: "Все серверы", title: "Все серверы", count: allCount, meta: String(allCount), } return ( {showHeader ? (
Серверы {loading ? : null} {headerRight}
) : headerRight || loading ? (
{loading ? : null} {headerRight}
) : null} setQuery(e.target.value)} placeholder="Поиск…" aria-label="Поиск сервера" disabled={loading} />
{showAllTile ? ( onSelect(ALL_SERVERS_ID)} showCount={showCount} showType={false} icon={ } /> ) : null} {groups.map((group) => (
{groupBySite && group.site ? (

{group.site}

) : null} {group.items.map((item) => ( onSelect(item.id)} showCount={showCount} showType={showType} showSite={!groupBySite} icon={ } /> ))}
))} {filtered.length === 0 && !showAllTile ? (

{loading ? "Загрузка серверов…" : items.length === 0 ? "Нет доступных серверов" : "Ничего не найдено"}

) : null}
) } function ServerTileButton({ item, selected, onSelect, showCount, showType, showSite = true, icon, }: { item: ServerTileItem selected: boolean onSelect: () => void showCount: boolean showType: boolean showSite?: boolean icon: ReactNode }) { const isAll = item.id === ALL_SERVERS_ID const selectable = isSelectable(item) const hostname = isAll ? "Все серверы" : hostnameOf(item) const meta = item.meta ?? (showCount && item.count != null ? String(item.count) : undefined) const showStatus = Boolean(item.status && item.status !== "online") return ( } className={cn( "h-11 min-h-11 flex-nowrap rounded-md py-0", selected && "ring-1 ring-border", item.enabled === false && !selected && "opacity-40", !selectable && "cursor-not-allowed opacity-40 hover:bg-transparent", )} > {icon} {showStatus ? : null} {hostname} {!isAll && showSite && item.site ? ( {item.site} ) : null} {!isAll && showType && item.type ? : null} {meta ? ( {meta} ) : null} ) } export { ServerTileRail, ServerTypeBadge, hostnameOf }