From 4dedb0fcd8129b4418d900a7271e179da0863dde Mon Sep 17 00:00:00 2001 From: snomiao Date: Tue, 31 Mar 2026 19:10:32 +0900 Subject: [PATCH 1/6] fix: resolve search ranking showing N/A by fetching individual node data The /nodes/search endpoint doesn't include search_ranking in its response, causing all rankings to display as "N/A". Added a NodeSearchRankingCell component that fetches each node's details via GET /nodes/{nodeId} (which returns search_ranking for authenticated admin users) to display the actual ranking values. Co-Authored-By: Claude Opus 4.6 (1M context) --- pages/admin/search-ranking.tsx | 156 ++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 61 deletions(-) diff --git a/pages/admin/search-ranking.tsx b/pages/admin/search-ranking.tsx index ab3b9e1e..cbecb5df 100644 --- a/pages/admin/search-ranking.tsx +++ b/pages/admin/search-ranking.tsx @@ -1,72 +1,84 @@ -import { Breadcrumb, Button, Spinner, TextInput } from "flowbite-react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { HiHome } from "react-icons/hi"; -import { MdEdit } from "react-icons/md"; -import { useRouterQuery } from "src/hooks/useRouterQuery"; -import { CustomPagination } from "@/components/common/CustomPagination"; -import withAdmin from "@/components/common/HOC/authAdmin"; -import { formatDownloadCount } from "@/components/nodes/NodeDetails"; -import SearchRankingEditModal from "@/components/nodes/SearchRankingEditModal"; -import { Node, useSearchNodes } from "@/src/api/generated"; -import { useNextTranslation } from "@/src/hooks/i18n"; +import { Breadcrumb, Button, Spinner, TextInput } from 'flowbite-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { HiHome } from 'react-icons/hi' +import { MdEdit } from 'react-icons/md' +import { useRouterQuery } from 'src/hooks/useRouterQuery' +import { CustomPagination } from '@/components/common/CustomPagination' +import withAdmin from '@/components/common/HOC/authAdmin' +import { formatDownloadCount } from '@/components/nodes/NodeDetails' +import SearchRankingEditModal from '@/components/nodes/SearchRankingEditModal' +import { Node, useGetNode, useSearchNodes } from '@/src/api/generated' +import { useNextTranslation } from '@/src/hooks/i18n' + +function NodeSearchRankingCell({ nodeId }: { nodeId: string }) { + const { t } = useNextTranslation() + const { data: node, isLoading } = useGetNode(nodeId) + if (isLoading) return + return <>{node?.search_ranking != null ? node.search_ranking : t('N/A')} +} function SearchRankingAdminPage() { - const { t } = useNextTranslation(); - const router = useRouter(); - const [selectedNode, setSelectedNode] = useState(null); + const { t } = useNextTranslation() + const router = useRouter() + const [selectedNode, setSelectedNode] = useState(null) // Use the custom hook for query parameters - const [query, updateQuery] = useRouterQuery(); + const [query, updateQuery] = useRouterQuery() // Extract and parse query parameters directly - const page = Number(query.page || 1); - const searchQuery = String(query.search || ""); + const page = Number(query.page || 1) + const searchQuery = String(query.search || '') // Fetch all nodes with pagination - searchQuery being undefined is handled properly const { data, isLoading, isError } = useSearchNodes({ page, limit: 24, search: searchQuery || undefined, - }); + }) // Handle page change - just update router const handlePageChange = (newPage: number) => { - updateQuery({ page: String(newPage) }); - }; + updateQuery({ page: String(newPage) }) + } // Handle search form submission const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - const form = e.currentTarget; - const searchInput = (form.elements.namedItem("search-nodes") as HTMLInputElement)?.value || ""; + e.preventDefault() + const form = e.currentTarget + const searchInput = + (form.elements.namedItem('search-nodes') as HTMLInputElement)?.value || '' updateQuery({ search: searchInput, page: String(1), // Reset to first page on new search - }); - }; + }) + } const handleEditRanking = (node: Node) => { - setSelectedNode(node); - }; + setSelectedNode(node) + } if (isLoading) { return (
- ); + ) } if (isError) { return (
-

{t("Search Ranking Management")}

-
{t("Error loading nodes. Please try again later.")}
+

+ {t('Search Ranking Management')} +

+
+ {t('Error loading nodes. Please try again later.')} +
- ); + ) } return ( @@ -77,74 +89,94 @@ function SearchRankingAdminPage() { href="/" icon={HiHome} onClick={(e) => { - e.preventDefault(); - router.push("/"); + e.preventDefault() + router.push('/') }} className="dark" > - {t("Home")} + {t('Home')} { - e.preventDefault(); - router.push("/admin"); + e.preventDefault() + router.push('/admin') }} className="dark" > - {t("Admin Dashboard")} + {t('Admin Dashboard')} + + + {t('Search Ranking Management')} - {t("Search Ranking Management")} -

{t("Search Ranking Management")}

+

+ {t('Search Ranking Management')} +

{/* Search form */}
{/* Nodes table */}
-

{t("Nodes List")}

+

+ {t('Nodes List')} +

- {t("Total")}: {data?.total || 0} {t("nodes")} + {t('Total')}: {data?.total || 0} {t('nodes')}
    {/* Table header */}
  • -
    {t("Node ID")}
    -
    {t("Publisher ID")}
    -
    {t("Downloads")}
    -
    {t("Search Ranking")}
    -
    {t("Operations")}
    +
    {t('Node ID')}
    +
    {t('Publisher ID')}
    +
    {t('Downloads')}
    +
    {t('Search Ranking')}
    +
    {t('Operations')}
  • {/* Table rows */} {data?.nodes?.map((node) => ( -
  • +
  • - + {node.id}
    -
    {node.publisher?.id || t("N/A")}
    -
    {formatDownloadCount(node.downloads || 0)}
    +
    + {node.publisher?.id || t('N/A')} +
    - {node.search_ranking !== undefined ? node.search_ranking : t("N/A")} + {formatDownloadCount(node.downloads || 0)} +
    +
    +
    -
  • @@ -152,7 +184,9 @@ function SearchRankingAdminPage() { {/* Empty state */} {(!data?.nodes || data.nodes.length === 0) && ( -
  • {t("No nodes found")}
  • +
  • + {t('No nodes found')} +
  • )}
@@ -168,14 +202,14 @@ function SearchRankingAdminPage() { {/* Edit Modal */} {selectedNode && ( setSelectedNode(null)} /> )}
- ); + ) } -export default withAdmin(SearchRankingAdminPage); +export default withAdmin(SearchRankingAdminPage) From 576e3d5e8d8ec1f2ff3bbbdb4d5d7321064d0673 Mon Sep 17 00:00:00 2001 From: snomiao <7323030+snomiao@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:12:34 +0000 Subject: [PATCH 2/6] format: Apply prettier --fix changes --- pages/admin/search-ranking.tsx | 157 ++++++++++++++------------------- 1 file changed, 65 insertions(+), 92 deletions(-) diff --git a/pages/admin/search-ranking.tsx b/pages/admin/search-ranking.tsx index cbecb5df..d83b0699 100644 --- a/pages/admin/search-ranking.tsx +++ b/pages/admin/search-ranking.tsx @@ -1,84 +1,79 @@ -import { Breadcrumb, Button, Spinner, TextInput } from 'flowbite-react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useState } from 'react' -import { HiHome } from 'react-icons/hi' -import { MdEdit } from 'react-icons/md' -import { useRouterQuery } from 'src/hooks/useRouterQuery' -import { CustomPagination } from '@/components/common/CustomPagination' -import withAdmin from '@/components/common/HOC/authAdmin' -import { formatDownloadCount } from '@/components/nodes/NodeDetails' -import SearchRankingEditModal from '@/components/nodes/SearchRankingEditModal' -import { Node, useGetNode, useSearchNodes } from '@/src/api/generated' -import { useNextTranslation } from '@/src/hooks/i18n' +import { Breadcrumb, Button, Spinner, TextInput } from "flowbite-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { HiHome } from "react-icons/hi"; +import { MdEdit } from "react-icons/md"; +import { useRouterQuery } from "src/hooks/useRouterQuery"; +import { CustomPagination } from "@/components/common/CustomPagination"; +import withAdmin from "@/components/common/HOC/authAdmin"; +import { formatDownloadCount } from "@/components/nodes/NodeDetails"; +import SearchRankingEditModal from "@/components/nodes/SearchRankingEditModal"; +import { Node, useGetNode, useSearchNodes } from "@/src/api/generated"; +import { useNextTranslation } from "@/src/hooks/i18n"; function NodeSearchRankingCell({ nodeId }: { nodeId: string }) { - const { t } = useNextTranslation() - const { data: node, isLoading } = useGetNode(nodeId) - if (isLoading) return - return <>{node?.search_ranking != null ? node.search_ranking : t('N/A')} + const { t } = useNextTranslation(); + const { data: node, isLoading } = useGetNode(nodeId); + if (isLoading) return ; + return <>{node?.search_ranking != null ? node.search_ranking : t("N/A")}; } function SearchRankingAdminPage() { - const { t } = useNextTranslation() - const router = useRouter() - const [selectedNode, setSelectedNode] = useState(null) + const { t } = useNextTranslation(); + const router = useRouter(); + const [selectedNode, setSelectedNode] = useState(null); // Use the custom hook for query parameters - const [query, updateQuery] = useRouterQuery() + const [query, updateQuery] = useRouterQuery(); // Extract and parse query parameters directly - const page = Number(query.page || 1) - const searchQuery = String(query.search || '') + const page = Number(query.page || 1); + const searchQuery = String(query.search || ""); // Fetch all nodes with pagination - searchQuery being undefined is handled properly const { data, isLoading, isError } = useSearchNodes({ page, limit: 24, search: searchQuery || undefined, - }) + }); // Handle page change - just update router const handlePageChange = (newPage: number) => { - updateQuery({ page: String(newPage) }) - } + updateQuery({ page: String(newPage) }); + }; // Handle search form submission const handleSearch = (e: React.FormEvent) => { - e.preventDefault() - const form = e.currentTarget - const searchInput = - (form.elements.namedItem('search-nodes') as HTMLInputElement)?.value || '' + e.preventDefault(); + const form = e.currentTarget; + const searchInput = (form.elements.namedItem("search-nodes") as HTMLInputElement)?.value || ""; updateQuery({ search: searchInput, page: String(1), // Reset to first page on new search - }) - } + }); + }; const handleEditRanking = (node: Node) => { - setSelectedNode(node) - } + setSelectedNode(node); + }; if (isLoading) { return (
- ) + ); } if (isError) { return (
-

- {t('Search Ranking Management')} -

-
- {t('Error loading nodes. Please try again later.')} -
+

{t("Search Ranking Management")}

+
{t("Error loading nodes. Please try again later.")}
- ) + ); } return ( @@ -89,94 +84,74 @@ function SearchRankingAdminPage() { href="/" icon={HiHome} onClick={(e) => { - e.preventDefault() - router.push('/') + e.preventDefault(); + router.push("/"); }} className="dark" > - {t('Home')} + {t("Home")} { - e.preventDefault() - router.push('/admin') + e.preventDefault(); + router.push("/admin"); }} className="dark" > - {t('Admin Dashboard')} - - - {t('Search Ranking Management')} + {t("Admin Dashboard")} + {t("Search Ranking Management")} -

- {t('Search Ranking Management')} -

+

{t("Search Ranking Management")}

{/* Search form */}
{/* Nodes table */}
-

- {t('Nodes List')} -

+

{t("Nodes List")}

- {t('Total')}: {data?.total || 0} {t('nodes')} + {t("Total")}: {data?.total || 0} {t("nodes")}
    {/* Table header */}
  • -
    {t('Node ID')}
    -
    {t('Publisher ID')}
    -
    {t('Downloads')}
    -
    {t('Search Ranking')}
    -
    {t('Operations')}
    +
    {t("Node ID")}
    +
    {t("Publisher ID")}
    +
    {t("Downloads")}
    +
    {t("Search Ranking")}
    +
    {t("Operations")}
  • {/* Table rows */} {data?.nodes?.map((node) => ( -
  • +
  • - + {node.id}
    -
    - {node.publisher?.id || t('N/A')} -
    -
    - {formatDownloadCount(node.downloads || 0)} -
    +
    {node.publisher?.id || t("N/A")}
    +
    {formatDownloadCount(node.downloads || 0)}
    - +
    -
  • @@ -184,9 +159,7 @@ function SearchRankingAdminPage() { {/* Empty state */} {(!data?.nodes || data.nodes.length === 0) && ( -
  • - {t('No nodes found')} -
  • +
  • {t("No nodes found")}
  • )}
@@ -202,14 +175,14 @@ function SearchRankingAdminPage() { {/* Edit Modal */} {selectedNode && ( setSelectedNode(null)} /> )}
- ) + ); } -export default withAdmin(SearchRankingAdminPage) +export default withAdmin(SearchRankingAdminPage); From c2a079be74726485bb23ba1b825c4a6abb3270d8 Mon Sep 17 00:00:00 2001 From: sno Date: Sat, 4 Apr 2026 19:06:19 +0900 Subject: [PATCH 3/6] Update pages/admin/search-ranking.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pages/admin/search-ranking.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pages/admin/search-ranking.tsx b/pages/admin/search-ranking.tsx index d83b0699..26e6a8ab 100644 --- a/pages/admin/search-ranking.tsx +++ b/pages/admin/search-ranking.tsx @@ -14,7 +14,10 @@ import { useNextTranslation } from "@/src/hooks/i18n"; function NodeSearchRankingCell({ nodeId }: { nodeId: string }) { const { t } = useNextTranslation(); - const { data: node, isLoading } = useGetNode(nodeId); + const { data: node, isLoading } = useGetNode(nodeId, { + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + }); if (isLoading) return ; return <>{node?.search_ranking != null ? node.search_ranking : t("N/A")}; } From f54b79725fa770c62e0389aa0ff1012f0807a23b Mon Sep 17 00:00:00 2001 From: sno Date: Fri, 10 Apr 2026 17:56:00 +0900 Subject: [PATCH 4/6] ci: run Next.js CI on all pull requests (#255) Previously only triggered on PRs targeting dev/staging/main, which meant PRs to feature branches skipped lint, format, and build checks. Co-authored-by: Claude Opus 4.6 (1M context) From 7e31cd5282791ccc3f8ad50563681beafd15bdaf Mon Sep 17 00:00:00 2001 From: snomiao Date: Fri, 10 Apr 2026 17:58:17 +0900 Subject: [PATCH 5/6] fix: pass query options correctly to useGetNode and handle error state - Move staleTime/refetchOnWindowFocus to 3rd arg (options.query) instead of 2nd arg (params) - Add isError handling to show explicit error state instead of silent N/A - Guard node.id before rendering NodeSearchRankingCell to handle missing IDs explicitly - Add enabled: !!nodeId to prevent queries with empty string Co-Authored-By: Claude Opus 4.6 (1M context) --- pages/admin/search-ranking.tsx | 175 ++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 68 deletions(-) diff --git a/pages/admin/search-ranking.tsx b/pages/admin/search-ranking.tsx index 26e6a8ab..885ecc15 100644 --- a/pages/admin/search-ranking.tsx +++ b/pages/admin/search-ranking.tsx @@ -1,82 +1,95 @@ -import { Breadcrumb, Button, Spinner, TextInput } from "flowbite-react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { HiHome } from "react-icons/hi"; -import { MdEdit } from "react-icons/md"; -import { useRouterQuery } from "src/hooks/useRouterQuery"; -import { CustomPagination } from "@/components/common/CustomPagination"; -import withAdmin from "@/components/common/HOC/authAdmin"; -import { formatDownloadCount } from "@/components/nodes/NodeDetails"; -import SearchRankingEditModal from "@/components/nodes/SearchRankingEditModal"; -import { Node, useGetNode, useSearchNodes } from "@/src/api/generated"; -import { useNextTranslation } from "@/src/hooks/i18n"; +import { Breadcrumb, Button, Spinner, TextInput } from 'flowbite-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { HiHome } from 'react-icons/hi' +import { MdEdit } from 'react-icons/md' +import { useRouterQuery } from 'src/hooks/useRouterQuery' +import { CustomPagination } from '@/components/common/CustomPagination' +import withAdmin from '@/components/common/HOC/authAdmin' +import { formatDownloadCount } from '@/components/nodes/NodeDetails' +import SearchRankingEditModal from '@/components/nodes/SearchRankingEditModal' +import { Node, useGetNode, useSearchNodes } from '@/src/api/generated' +import { useNextTranslation } from '@/src/hooks/i18n' function NodeSearchRankingCell({ nodeId }: { nodeId: string }) { - const { t } = useNextTranslation(); - const { data: node, isLoading } = useGetNode(nodeId, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - }); - if (isLoading) return ; - return <>{node?.search_ranking != null ? node.search_ranking : t("N/A")}; + const { t } = useNextTranslation() + const { + data: node, + isLoading, + isError, + } = useGetNode(nodeId, undefined, { + query: { + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + enabled: !!nodeId, + }, + }) + if (isLoading) return + if (isError) return <>{t('Error')} + return <>{node?.search_ranking != null ? node.search_ranking : t('N/A')} } function SearchRankingAdminPage() { - const { t } = useNextTranslation(); - const router = useRouter(); - const [selectedNode, setSelectedNode] = useState(null); + const { t } = useNextTranslation() + const router = useRouter() + const [selectedNode, setSelectedNode] = useState(null) // Use the custom hook for query parameters - const [query, updateQuery] = useRouterQuery(); + const [query, updateQuery] = useRouterQuery() // Extract and parse query parameters directly - const page = Number(query.page || 1); - const searchQuery = String(query.search || ""); + const page = Number(query.page || 1) + const searchQuery = String(query.search || '') // Fetch all nodes with pagination - searchQuery being undefined is handled properly const { data, isLoading, isError } = useSearchNodes({ page, limit: 24, search: searchQuery || undefined, - }); + }) // Handle page change - just update router const handlePageChange = (newPage: number) => { - updateQuery({ page: String(newPage) }); - }; + updateQuery({ page: String(newPage) }) + } // Handle search form submission const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - const form = e.currentTarget; - const searchInput = (form.elements.namedItem("search-nodes") as HTMLInputElement)?.value || ""; + e.preventDefault() + const form = e.currentTarget + const searchInput = + (form.elements.namedItem('search-nodes') as HTMLInputElement)?.value || '' updateQuery({ search: searchInput, page: String(1), // Reset to first page on new search - }); - }; + }) + } const handleEditRanking = (node: Node) => { - setSelectedNode(node); - }; + setSelectedNode(node) + } if (isLoading) { return (
- ); + ) } if (isError) { return (
-

{t("Search Ranking Management")}

-
{t("Error loading nodes. Please try again later.")}
+

+ {t('Search Ranking Management')} +

+
+ {t('Error loading nodes. Please try again later.')} +
- ); + ) } return ( @@ -87,74 +100,98 @@ function SearchRankingAdminPage() { href="/" icon={HiHome} onClick={(e) => { - e.preventDefault(); - router.push("/"); + e.preventDefault() + router.push('/') }} className="dark" > - {t("Home")} + {t('Home')} { - e.preventDefault(); - router.push("/admin"); + e.preventDefault() + router.push('/admin') }} className="dark" > - {t("Admin Dashboard")} + {t('Admin Dashboard')} + + + {t('Search Ranking Management')} - {t("Search Ranking Management")} -

{t("Search Ranking Management")}

+

+ {t('Search Ranking Management')} +

{/* Search form */}
{/* Nodes table */}
-

{t("Nodes List")}

+

+ {t('Nodes List')} +

- {t("Total")}: {data?.total || 0} {t("nodes")} + {t('Total')}: {data?.total || 0} {t('nodes')}
    {/* Table header */}
  • -
    {t("Node ID")}
    -
    {t("Publisher ID")}
    -
    {t("Downloads")}
    -
    {t("Search Ranking")}
    -
    {t("Operations")}
    +
    {t('Node ID')}
    +
    {t('Publisher ID')}
    +
    {t('Downloads')}
    +
    {t('Search Ranking')}
    +
    {t('Operations')}
  • {/* Table rows */} {data?.nodes?.map((node) => ( -
  • +
  • - + {node.id}
    -
    {node.publisher?.id || t("N/A")}
    -
    {formatDownloadCount(node.downloads || 0)}
    +
    + {node.publisher?.id || t('N/A')} +
    +
    + {formatDownloadCount(node.downloads || 0)} +
    - + {node.id ? ( + + ) : ( + t('N/A') + )}
    -
  • @@ -162,7 +199,9 @@ function SearchRankingAdminPage() { {/* Empty state */} {(!data?.nodes || data.nodes.length === 0) && ( -
  • {t("No nodes found")}
  • +
  • + {t('No nodes found')} +
  • )}
@@ -178,14 +217,14 @@ function SearchRankingAdminPage() { {/* Edit Modal */} {selectedNode && ( setSelectedNode(null)} /> )}
- ); + ) } -export default withAdmin(SearchRankingAdminPage); +export default withAdmin(SearchRankingAdminPage) From b86c9cb88ced8ca6560c39bf336bd6b9a15b7372 Mon Sep 17 00:00:00 2001 From: snomiao <7323030+snomiao@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:00:10 +0000 Subject: [PATCH 6/6] format: Apply prettier --fix changes --- pages/admin/search-ranking.tsx | 163 +++++++++++++-------------------- 1 file changed, 66 insertions(+), 97 deletions(-) diff --git a/pages/admin/search-ranking.tsx b/pages/admin/search-ranking.tsx index 885ecc15..2259fe0a 100644 --- a/pages/admin/search-ranking.tsx +++ b/pages/admin/search-ranking.tsx @@ -1,19 +1,19 @@ -import { Breadcrumb, Button, Spinner, TextInput } from 'flowbite-react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useState } from 'react' -import { HiHome } from 'react-icons/hi' -import { MdEdit } from 'react-icons/md' -import { useRouterQuery } from 'src/hooks/useRouterQuery' -import { CustomPagination } from '@/components/common/CustomPagination' -import withAdmin from '@/components/common/HOC/authAdmin' -import { formatDownloadCount } from '@/components/nodes/NodeDetails' -import SearchRankingEditModal from '@/components/nodes/SearchRankingEditModal' -import { Node, useGetNode, useSearchNodes } from '@/src/api/generated' -import { useNextTranslation } from '@/src/hooks/i18n' +import { Breadcrumb, Button, Spinner, TextInput } from "flowbite-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { HiHome } from "react-icons/hi"; +import { MdEdit } from "react-icons/md"; +import { useRouterQuery } from "src/hooks/useRouterQuery"; +import { CustomPagination } from "@/components/common/CustomPagination"; +import withAdmin from "@/components/common/HOC/authAdmin"; +import { formatDownloadCount } from "@/components/nodes/NodeDetails"; +import SearchRankingEditModal from "@/components/nodes/SearchRankingEditModal"; +import { Node, useGetNode, useSearchNodes } from "@/src/api/generated"; +import { useNextTranslation } from "@/src/hooks/i18n"; function NodeSearchRankingCell({ nodeId }: { nodeId: string }) { - const { t } = useNextTranslation() + const { t } = useNextTranslation(); const { data: node, isLoading, @@ -24,72 +24,67 @@ function NodeSearchRankingCell({ nodeId }: { nodeId: string }) { refetchOnWindowFocus: false, enabled: !!nodeId, }, - }) - if (isLoading) return - if (isError) return <>{t('Error')} - return <>{node?.search_ranking != null ? node.search_ranking : t('N/A')} + }); + if (isLoading) return ; + if (isError) return <>{t("Error")}; + return <>{node?.search_ranking != null ? node.search_ranking : t("N/A")}; } function SearchRankingAdminPage() { - const { t } = useNextTranslation() - const router = useRouter() - const [selectedNode, setSelectedNode] = useState(null) + const { t } = useNextTranslation(); + const router = useRouter(); + const [selectedNode, setSelectedNode] = useState(null); // Use the custom hook for query parameters - const [query, updateQuery] = useRouterQuery() + const [query, updateQuery] = useRouterQuery(); // Extract and parse query parameters directly - const page = Number(query.page || 1) - const searchQuery = String(query.search || '') + const page = Number(query.page || 1); + const searchQuery = String(query.search || ""); // Fetch all nodes with pagination - searchQuery being undefined is handled properly const { data, isLoading, isError } = useSearchNodes({ page, limit: 24, search: searchQuery || undefined, - }) + }); // Handle page change - just update router const handlePageChange = (newPage: number) => { - updateQuery({ page: String(newPage) }) - } + updateQuery({ page: String(newPage) }); + }; // Handle search form submission const handleSearch = (e: React.FormEvent) => { - e.preventDefault() - const form = e.currentTarget - const searchInput = - (form.elements.namedItem('search-nodes') as HTMLInputElement)?.value || '' + e.preventDefault(); + const form = e.currentTarget; + const searchInput = (form.elements.namedItem("search-nodes") as HTMLInputElement)?.value || ""; updateQuery({ search: searchInput, page: String(1), // Reset to first page on new search - }) - } + }); + }; const handleEditRanking = (node: Node) => { - setSelectedNode(node) - } + setSelectedNode(node); + }; if (isLoading) { return (
- ) + ); } if (isError) { return (
-

- {t('Search Ranking Management')} -

-
- {t('Error loading nodes. Please try again later.')} -
+

{t("Search Ranking Management")}

+
{t("Error loading nodes. Please try again later.")}
- ) + ); } return ( @@ -100,98 +95,74 @@ function SearchRankingAdminPage() { href="/" icon={HiHome} onClick={(e) => { - e.preventDefault() - router.push('/') + e.preventDefault(); + router.push("/"); }} className="dark" > - {t('Home')} + {t("Home")} { - e.preventDefault() - router.push('/admin') + e.preventDefault(); + router.push("/admin"); }} className="dark" > - {t('Admin Dashboard')} - - - {t('Search Ranking Management')} + {t("Admin Dashboard")} + {t("Search Ranking Management")} -

- {t('Search Ranking Management')} -

+

{t("Search Ranking Management")}

{/* Search form */}
{/* Nodes table */}
-

- {t('Nodes List')} -

+

{t("Nodes List")}

- {t('Total')}: {data?.total || 0} {t('nodes')} + {t("Total")}: {data?.total || 0} {t("nodes")}
    {/* Table header */}
  • -
    {t('Node ID')}
    -
    {t('Publisher ID')}
    -
    {t('Downloads')}
    -
    {t('Search Ranking')}
    -
    {t('Operations')}
    +
    {t("Node ID")}
    +
    {t("Publisher ID")}
    +
    {t("Downloads")}
    +
    {t("Search Ranking")}
    +
    {t("Operations")}
  • {/* Table rows */} {data?.nodes?.map((node) => ( -
  • +
  • - + {node.id}
    -
    - {node.publisher?.id || t('N/A')} -
    -
    - {formatDownloadCount(node.downloads || 0)} -
    +
    {node.publisher?.id || t("N/A")}
    +
    {formatDownloadCount(node.downloads || 0)}
    - {node.id ? ( - - ) : ( - t('N/A') - )} + {node.id ? : t("N/A")}
    -
  • @@ -199,9 +170,7 @@ function SearchRankingAdminPage() { {/* Empty state */} {(!data?.nodes || data.nodes.length === 0) && ( -
  • - {t('No nodes found')} -
  • +
  • {t("No nodes found")}
  • )}
@@ -217,14 +186,14 @@ function SearchRankingAdminPage() { {/* Edit Modal */} {selectedNode && ( setSelectedNode(null)} /> )}
- ) + ); } -export default withAdmin(SearchRankingAdminPage) +export default withAdmin(SearchRankingAdminPage);