From 1e7257b957367d93a4836fe48974392a45bf97ef Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Fri, 5 Sep 2025 17:05:22 +0300 Subject: [PATCH 1/9] myrichlist so far --- components/ExploreMenu.tsx | 4 +- components/footer.tsx | 4 + components/navbar.tsx | 5 ++ components/ui/TopHoldersPagination.tsx | 46 +++++++++++ pages/top-holders.tsx | 109 +++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 components/ui/TopHoldersPagination.tsx create mode 100644 pages/top-holders.tsx diff --git a/components/ExploreMenu.tsx b/components/ExploreMenu.tsx index 11e778b35..a1aeed5ea 100644 --- a/components/ExploreMenu.tsx +++ b/components/ExploreMenu.tsx @@ -1,9 +1,10 @@ // src/components/layout/ExploreMenu.tsx +// At the top with imports import React, { useState, useEffect, useRef } from "react"; import Link from "next/link"; import { useI18n } from "@/i18n/i18n"; -import { Users, Vote, Menu, UserCheck, SettingsIcon } from "lucide-react"; +import { Users, Vote, Menu, UserCheck, SettingsIcon,Award } from "lucide-react"; import { Popover, PopoverContent, @@ -55,6 +56,7 @@ export function ExploreMenu() { setIsOpen(false)} /> setIsOpen(false)} /> setIsOpen(false)} /> + setIsOpen(false)}/> setIsOpen(false)} /> diff --git a/components/footer.tsx b/components/footer.tsx index 125d15fc5..3142c08fb 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -200,6 +200,10 @@ const Footer = () => { {t("footer.witnessSchedule")} +
  • + + Top Holders +
  • diff --git a/components/navbar.tsx b/components/navbar.tsx index a8264e309..033c4f4a2 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -99,6 +99,11 @@ export default function Navbar() { {t("navbar.witnessesTitle")} + setMenuOpen(false)}> + + Top Holders + + setMenuOpen(false)}> {t("navbar.additionalSettingsTitle")} diff --git a/components/ui/TopHoldersPagination.tsx b/components/ui/TopHoldersPagination.tsx new file mode 100644 index 000000000..b3c3efeaa --- /dev/null +++ b/components/ui/TopHoldersPagination.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { + Pagination, + PaginationItem, + PaginationLink, + PaginationPrevious, + PaginationNext, +} from "@/components/ui/pagination"; + +interface Props { + currentPage: number; + onPageChange: (page: number) => void; + totalPages: number; +} + +const TopHoldersPagination: React.FC = ({ currentPage, onPageChange, totalPages }) => { + const pagesToShow = 5; // Number of page buttons to display + const startPage = Math.max(1, currentPage - Math.floor(pagesToShow / 2)); + const endPage = Math.min(totalPages, startPage + pagesToShow - 1); + + const pageNumbers = []; + for (let i = startPage; i <= endPage; i++) pageNumbers.push(i); + + return ( + + onPageChange(Math.max(1, currentPage - 1))} + /> + {pageNumbers.map((page) => ( + + onPageChange(page)} + > + {page} + + + ))} + onPageChange(Math.min(totalPages, currentPage + 1))} + /> + + ); +}; + +export default TopHoldersPagination; diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx new file mode 100644 index 000000000..ac6231bdf --- /dev/null +++ b/pages/top-holders.tsx @@ -0,0 +1,109 @@ +// pages/top-holders.tsx +import { useEffect, useState } from "react"; +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; +import { Select } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; +import { Pagination } from "@/components/ui/pagination"; +import PageTitle from "@/components/PageTitle"; +import ErrorMessage from "@/components/ErrorMessage"; +import NoResult from "@/components/NoResult"; +import TopHoldersPagination from "@/components/ui/TopHoldersPagination"; + +interface Holder { + account: string; + balance: string; +} + +export default function TopHoldersPage() { + const [holders, setHolders] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const [coinType, setCoinType] = useState("HIVE"); + const [balanceType, setBalanceType] = useState("balance"); + + useEffect(() => { + fetchHolders(); + }, [page, coinType, balanceType]); + + const fetchHolders = async () => { + setLoading(true); + setError(null); + try { + const res = await fetch( + `https://api.syncad.com/balance-api/top-holders?coin-type=${coinType}&balance-type=${balanceType}&page=${page}` + ); + if (!res.ok) throw new Error(`API error: ${res.status}`); + const data = await res.json(); + setHolders(data?.holders || []); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
    + + + + + Filters + +
    + + + +
    +
    + + + + Richlist + +
    + {loading &&

    Loading...

    } + {error && } + {!loading && !error && holders.length === 0 && } + {!loading && !error && holders.length > 0 && ( + + + + Rank + Account + Balance + + + + {holders.map((holder, idx) => ( + + {idx + 1 + (page - 1) * 50} + {holder.account} + {holder.balance} + + ))} + +
    + )} + +
    + +
    +
    +
    +
    + ); +} -- GitLab From 24d1f49dd110f89bf6bb8959c743eb3b1f38f40d Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Mon, 8 Sep 2025 01:05:02 +0300 Subject: [PATCH 2/9] gar/#619_creating_richlist_page_progress --- components/ExploreMenu.tsx | 2 +- components/Modal.tsx | 69 +++++++++++++++++++++ components/footer.tsx | 2 +- components/navbar.tsx | 2 +- i18n/ar.json | 12 ++++ i18n/de.json | 14 ++++- i18n/en.json | 12 ++++ i18n/es.json | 14 ++++- i18n/fr.json | 14 ++++- i18n/it.json | 14 ++++- i18n/ja.json | 14 ++++- i18n/ko.json | 13 ++++ i18n/pl.json | 14 ++++- i18n/pt.json | 14 ++++- i18n/ro.json | 14 ++++- i18n/zh.json | 12 ++++ package-lock.json | 46 ++++++++++++++ package.json | 2 + pages/top-holders.tsx | 124 +++++++++++++++++++++++++------------ 19 files changed, 358 insertions(+), 50 deletions(-) create mode 100644 components/Modal.tsx diff --git a/components/ExploreMenu.tsx b/components/ExploreMenu.tsx index a1aeed5ea..4d69abcd6 100644 --- a/components/ExploreMenu.tsx +++ b/components/ExploreMenu.tsx @@ -56,7 +56,7 @@ export function ExploreMenu() { setIsOpen(false)} /> setIsOpen(false)} /> setIsOpen(false)} /> - setIsOpen(false)}/> + setIsOpen(false)}/> setIsOpen(false)} /> diff --git a/components/Modal.tsx b/components/Modal.tsx new file mode 100644 index 000000000..f1fbb2e17 --- /dev/null +++ b/components/Modal.tsx @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import BalanceHistoryChart from "@/components/balanceHistory/BalanceHistoryChart"; +import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory"; + +interface BalanceHistoryModalProps { + username: string; + coinType?: "HIVE" | "VESTS" | "HBD"; + onClose: () => void; +} + +const BalanceHistoryModal: React.FC = ({ + username, + coinType = "HIVE", + onClose, +}) => { + const [selectedCoinType, setSelectedCoinType] = useState<"HIVE" | "VESTS" | "HBD">(coinType); + + const { accountBalanceHistory, isAccountBalanceHistoryLoading, isAccountBalanceHistoryError } = + useBalanceHistory(username, selectedCoinType, 1, 100, "asc"); + + return ( +
    +
    +
    +

    {username} Balance History

    + +
    + + {/* Coin selection */} +
    + {["HIVE", "VESTS", "HBD"].map((coin) => ( + + ))} +
    + + {/* Content */} + {isAccountBalanceHistoryLoading ? ( +

    Loading balance history...

    + ) : isAccountBalanceHistoryError ? ( +

    Error loading balance history.

    + ) : !accountBalanceHistory?.operations_result?.length ? ( +

    No balance history available.

    + ) : ( +
    + +
    + )} +
    +
    + ); +}; + +export default BalanceHistoryModal; diff --git a/components/footer.tsx b/components/footer.tsx index 3142c08fb..4c6bf7b23 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -202,7 +202,7 @@ const Footer = () => {
  • - Top Holders + {t("pageTitle.topHolders")}
  • diff --git a/components/navbar.tsx b/components/navbar.tsx index 033c4f4a2..0eeb08287 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -101,7 +101,7 @@ export default function Navbar() { setMenuOpen(false)}> - Top Holders + {t("pageTitle.topHolders")} setMenuOpen(false)}> diff --git a/i18n/ar.json b/i18n/ar.json index 1a967f3f9..79ae1651a 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -905,4 +905,16 @@ "settingsPage.radialViewDescription": "عرض الموارد مثل قوة التصويت باستخدام مؤشرات دائرية. (الافتراضي)", "settingsPage.linearViewLabel": "عرض خطي", "settingsPage.linearViewDescription": "عرض الموارد مثل قوة التصويت باستخدام أشرطة أفقية تقليدية." + ,"pageTitle.topHolders": "أعلى الحائزين", + "filters.filtersAndSearch": "المرشحات والبحث", + "filters.balance": "الرصيد", + "filters.savings": "المدخرات", + "table.rank": "الترتيب", + "table.account": "الحساب", + "table.balance": "الرصيد", + "table.savings": "المدخرات", + "modal.loading": "جار التحميل...", + "modal.noHistory": "لا يوجد تاريخ متاح.", + "modal.error": "خطأ في تحميل تاريخ الرصيد.", + "modal.close": "إغلاق" } \ No newline at end of file diff --git a/i18n/de.json b/i18n/de.json index 9cdc64d31..965d33745 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -904,5 +904,17 @@ "settingsPage.radialViewLabel": "Radiale Ansicht", "settingsPage.radialViewDescription": "Ressourcen wie Stimmkraft mit kreisförmigen Indikatoren anzeigen. (Standard)", "settingsPage.linearViewLabel": "Lineare Ansicht", - "settingsPage.linearViewDescription": "Ressourcen wie Stimmkraft mit traditionellen horizontalen Balken anzeigen." + "settingsPage.linearViewDescription": "Ressourcen wie Stimmkraft mit traditionellen horizontalen Balken anzeigen.", + "pageTitle.topHolders": "Top-Inhaber", + "filters.filtersAndSearch": "Filter & Suche", + "filters.balance": "Kontostand", + "filters.savings": "Ersparnisse", + "table.rank": "Rang", + "table.account": "Konto", + "table.balance": "Kontostand", + "table.savings": "Ersparnisse", + "modal.loading": "Lädt...", + "modal.noHistory": "Keine Historie verfügbar.", + "modal.error": "Fehler beim Laden des Kontostandverlaufs.", + "modal.close": "Schließen" } \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 80ecbc939..d5c4d227d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -905,4 +905,16 @@ "settingsPage.radialViewDescription": "Show resources like Voting Power using circular indicators. (Default)", "settingsPage.linearViewLabel": "Linear View", "settingsPage.linearViewDescription": "Show resources like Voting Power using traditional horizontal bars." + ,"pageTitle.topHolders": "Top Holders", + "filters.filtersAndSearch": "Filters & Search", + "filters.balance": "Balance", + "filters.savings": "Savings", + "table.rank": "Rank", + "table.account": "Account", + "table.balance": "Balance", + "table.savings": "Savings", + "modal.loading": "Loading...", + "modal.noHistory": "No history available.", + "modal.error": "Error loading balance history.", + "modal.close": "Close" } \ No newline at end of file diff --git a/i18n/es.json b/i18n/es.json index 2d199fa96..e31aa72c3 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -903,5 +903,17 @@ "settingsPage.radialViewLabel": "Radiale Ansicht", "settingsPage.radialViewDescription": "Ressourcen wie Stimmkraft mit kreisförmigen Indikatoren anzeigen. (Standard)", "settingsPage.linearViewLabel": "Lineare Ansicht", - "settingsPage.linearViewDescription": "Ressourcen wie Stimmkraft mit traditionellen horizontalen Balken anzeigen." + "settingsPage.linearViewDescription": "Ressourcen wie Stimmkraft mit traditionellen horizontalen Balken anzeigen.", + "pageTitle.topHolders": "Principales Titulares", + "filters.filtersAndSearch": "Filtros y Búsqueda", + "filters.balance": "Saldo", + "filters.savings": "Ahorros", + "table.rank": "Rango", + "table.account": "Cuenta", + "table.balance": "Saldo", + "table.savings": "Ahorros", + "modal.loading": "Cargando...", + "modal.noHistory": "No hay historial disponible.", + "modal.error": "Error al cargar el historial de saldo.", + "modal.close": "Cerrar" } \ No newline at end of file diff --git a/i18n/fr.json b/i18n/fr.json index 387287396..d24122d25 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -903,5 +903,17 @@ "settingsPage.radialViewLabel": "Vue radiale", "settingsPage.radialViewDescription": "Afficher les ressources comme la puissance de vote avec des indicateurs circulaires. (Défaut)", "settingsPage.linearViewLabel": "Vue linéaire", - "settingsPage.linearViewDescription": "Afficher les ressources comme la puissance de vote avec des barres horizontales traditionnelles." + "settingsPage.linearViewDescription": "Afficher les ressources comme la puissance de vote avec des barres horizontales traditionnelles.", + "pageTitle.topHolders": "Principaux Détenteurs", + "filters.filtersAndSearch": "Filtres & Recherche", + "filters.balance": "Solde", + "filters.savings": "Économies", + "table.rank": "Rang", + "table.account": "Compte", + "table.balance": "Solde", + "table.savings": "Économies", + "modal.loading": "Chargement...", + "modal.noHistory": "Aucun historique disponible.", + "modal.error": "Erreur lors du chargement de l'historique du solde.", + "modal.close": "Fermer" } \ No newline at end of file diff --git a/i18n/it.json b/i18n/it.json index d35a91ab0..17e34969b 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -899,6 +899,18 @@ "settingsPage.radialViewLabel": "Vista radiale", "settingsPage.radialViewDescription": "Mostra risorse come il potere di voto usando indicatori circolari. (Predefinito)", "settingsPage.linearViewLabel": "Vista lineare", - "settingsPage.linearViewDescription": "Mostra risorse come il potere di voto usando le tradizionali barre orizzontali." + "settingsPage.linearViewDescription": "Mostra risorse come il potere di voto usando le tradizionali barre orizzontali.", + "pageTitle.topHolders": "Principali Detentori", + "filters.filtersAndSearch": "Filtri e Ricerca", + "filters.balance": "Saldo", + "filters.savings": "Risparmi", + "table.rank": "Posizione", + "table.account": "Account", + "table.balance": "Saldo", + "table.savings": "Risparmi", + "modal.loading": "Caricamento...", + "modal.noHistory": "Nessuna cronologia disponibile.", + "modal.error": "Errore durante il caricamento dello storico saldo.", + "modal.close": "Chiudi" } \ No newline at end of file diff --git a/i18n/ja.json b/i18n/ja.json index 912865c78..b1f430fcf 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -904,5 +904,17 @@ "settingsPage.radialViewLabel": "円形ビュー", "settingsPage.radialViewDescription": "投票パワーなどのリソースを円形のインジケーターで表示します。(デフォルト)", "settingsPage.linearViewLabel": "線形ビュー", - "settingsPage.linearViewDescription": "投票パワーなどのリソースを従来の横棒で表示します。" + "settingsPage.linearViewDescription": "投票パワーなどのリソースを従来の横棒で表示します。", + "pageTitle.topHolders": "トップ保有者", + "filters.filtersAndSearch": "フィルターと検索", + "filters.balance": "残高", + "filters.savings": "貯蓄", + "table.rank": "ランク", + "table.account": "アカウント", + "table.balance": "残高", + "table.savings": "貯蓄", + "modal.loading": "読み込み中...", + "modal.noHistory": "履歴はありません。", + "modal.error": "残高履歴の読み込み中にエラーが発生しました。", + "modal.close": "閉じる" } \ No newline at end of file diff --git a/i18n/ko.json b/i18n/ko.json index 756fb016e..2a7cbe613 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -905,4 +905,17 @@ "settingsPage.radialViewDescription": "보팅 파워와 같은 리소스를 원형 표시기로 표시합니다. (기본값)", "settingsPage.linearViewLabel": "선형 보기", "settingsPage.linearViewDescription": "보팅 파워와 같은 리소스를 기존의 수평 막대로 표시합니다." + , + "pageTitle.topHolders": "상위 보유자", + "filters.filtersAndSearch": "필터 및 검색", + "filters.balance": "잔액", + "filters.savings": "저축", + "table.rank": "순위", + "table.account": "계정", + "table.balance": "잔액", + "table.savings": "저축", + "modal.loading": "로딩 중...", + "modal.noHistory": "사용 가능한 기록이 없습니다.", + "modal.error": "잔액 기록을 불러오는 중 오류가 발생했습니다.", + "modal.close": "닫기" } \ No newline at end of file diff --git a/i18n/pl.json b/i18n/pl.json index 034c8c19b..961e0e81c 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -904,5 +904,17 @@ "settingsPage.radialViewLabel": "Widok radialny", "settingsPage.radialViewDescription": "Pokazuj zasoby, takie jak siła głosu, za pomocą wskaźników kołowych. (Domyślnie)", "settingsPage.linearViewLabel": "Widok liniowy", - "settingsPage.linearViewDescription": "Pokazuj zasoby, takie jak siła głosu, za pomocą tradycyjnych pasków poziomych." + "settingsPage.linearViewDescription": "Pokazuj zasoby, takie jak siła głosu, za pomocą tradycyjnych pasków poziomych.", + "pageTitle.topHolders": "Najwięksi Posiadacze", + "filters.filtersAndSearch": "Filtry i Wyszukiwanie", + "filters.balance": "Saldo", + "filters.savings": "Oszczędności", + "table.rank": "Pozycja", + "table.account": "Konto", + "table.balance": "Saldo", + "table.savings": "Oszczędności", + "modal.loading": "Ładowanie...", + "modal.noHistory": "Brak dostępnej historii.", + "modal.error": "Błąd podczas ładowania historii salda.", + "modal.close": "Zamknij" } \ No newline at end of file diff --git a/i18n/pt.json b/i18n/pt.json index 7539e1028..489da8c4d 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -904,7 +904,19 @@ "settingsPage.radialViewLabel": "Visualização Radial", "settingsPage.radialViewDescription": "Mostrar recursos como poder de voto usando indicadores circulares. (Padrão)", "settingsPage.linearViewLabel": "Visualização Linear", - "settingsPage.linearViewDescription": "Mostrar recursos como poder de voto usando barras horizontais tradicionais." + "settingsPage.linearViewDescription": "Mostrar recursos como poder de voto usando barras horizontais tradicionais.", + "pageTitle.topHolders": "Principais Detentores", + "filters.filtersAndSearch": "Filtros e Pesquisa", + "filters.balance": "Saldo", + "filters.savings": "Poupança", + "table.rank": "Classificação", + "table.account": "Conta", + "table.balance": "Saldo", + "table.savings": "Poupança", + "modal.loading": "Carregando...", + "modal.noHistory": "Nenhum histórico disponível.", + "modal.error": "Erro ao carregar histórico de saldo.", + "modal.close": "Fechar" } \ No newline at end of file diff --git a/i18n/ro.json b/i18n/ro.json index 88beeb4b4..7b561aa30 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -903,5 +903,17 @@ "settingsPage.radialViewLabel": "Vizualizare radială", "settingsPage.radialViewDescription": "Afișează resurse precum puterea de vot folosind indicatori circulari. (Implicit)", "settingsPage.linearViewLabel": "Vizualizare liniară", - "settingsPage.linearViewDescription": "Afișează resurse precum puterea de vot folosind bare orizontale tradiționale." + "settingsPage.linearViewDescription": "Afișează resurse precum puterea de vot folosind bare orizontale tradiționale.", + "pageTitle.topHolders": "Cei mai mari deținători", + "filters.filtersAndSearch": "Filtre și Căutare", + "filters.balance": "Sold", + "filters.savings": "Economii", + "table.rank": "Rang", + "table.account": "Cont", + "table.balance": "Sold", + "table.savings": "Economii", + "modal.loading": "Se încarcă...", + "modal.noHistory": "Nu există istoric disponibil.", + "modal.error": "Eroare la încărcarea istoricului soldului.", + "modal.close": "Închide" } \ No newline at end of file diff --git a/i18n/zh.json b/i18n/zh.json index 3d77ee330..8a2d40ace 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -905,5 +905,17 @@ "settingsPage.radialViewDescription": "使用环形指示器显示投票权等资源。(默认)", "settingsPage.linearViewLabel": "线性视图", "settingsPage.linearViewDescription": "使用传统的水平条显示投票权等资源。" + ,"pageTitle.topHolders": "持币最多者", + "filters.filtersAndSearch": "筛选与搜索", + "filters.balance": "余额", + "filters.savings": "储蓄", + "table.rank": "排名", + "table.account": "账户", + "table.balance": "余额", + "table.savings": "储蓄", + "modal.loading": "加载中...", + "modal.noHistory": "没有可用历史记录。", + "modal.error": "加载余额历史出错。", + "modal.close": "关闭" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 84d1a2ac3..42be30eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/react": "19.1.8", "@types/react-dom": "19.1.6", "autoprefixer": "10.4.14", + "chart.js": "^4.5.0", "child_process": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -43,6 +44,7 @@ "playwright-merge-html-reports": "^0.2.8", "postcss": "^8.4.31", "react": "18.3.1", + "react-chartjs-2": "^5.3.0", "react-country-flag": "^3.1.0", "react-day-picker": "^8.10.0", "react-dom": "18.3.1", @@ -706,6 +708,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "node_modules/@next/env": { "version": "15.3.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", @@ -3075,6 +3082,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", @@ -6605,6 +6623,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-country-flag": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", @@ -8485,6 +8512,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "@next/env": { "version": "15.3.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", @@ -9809,6 +9841,14 @@ "supports-color": "^7.1.0" } }, + "chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", @@ -12253,6 +12293,12 @@ "loose-envify": "^1.1.0" } }, + "react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "requires": {} + }, "react-country-flag": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", diff --git a/package.json b/package.json index 675a2354a..81f6015ac 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/react": "19.1.8", "@types/react-dom": "19.1.6", "autoprefixer": "10.4.14", + "chart.js": "^4.5.0", "child_process": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -53,6 +54,7 @@ "playwright-merge-html-reports": "^0.2.8", "postcss": "^8.4.31", "react": "18.3.1", + "react-chartjs-2": "^5.3.0", "react-country-flag": "^3.1.0", "react-day-picker": "^8.10.0", "react-dom": "18.3.1", diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx index ac6231bdf..36d9077d6 100644 --- a/pages/top-holders.tsx +++ b/pages/top-holders.tsx @@ -1,29 +1,33 @@ -// pages/top-holders.tsx import { useEffect, useState } from "react"; import { Card, CardHeader, CardTitle } from "@/components/ui/card"; -import { Select } from "@/components/ui/select"; -import { Button } from "@/components/ui/button"; import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; -import { Pagination } from "@/components/ui/pagination"; import PageTitle from "@/components/PageTitle"; import ErrorMessage from "@/components/ErrorMessage"; import NoResult from "@/components/NoResult"; import TopHoldersPagination from "@/components/ui/TopHoldersPagination"; +import SearchBar from "@/components/SearchBar"; +import BalanceHistoryModal from "@/components/Modal"; +import { useI18n } from "@/i18n/i18n"; interface Holder { + rank: number; account: string; - balance: string; + value: string; } export default function TopHoldersPage() { + const { t } = useI18n(); const [holders, setHolders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [page, setPage] = useState(1); - const [coinType, setCoinType] = useState("HIVE"); - const [balanceType, setBalanceType] = useState("balance"); + const [coinType, setCoinType] = useState<"HIVE" | "HBD" | "VESTS">("HIVE"); + const [balanceType, setBalanceType] = useState<"balance" | "savings_balance">("balance"); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedAccount, setSelectedAccount] = useState(null); useEffect(() => { + if (coinType === "VESTS" && balanceType !== "balance") setBalanceType("balance"); fetchHolders(); }, [page, coinType, balanceType]); @@ -36,7 +40,7 @@ export default function TopHoldersPage() { ); if (!res.ok) throw new Error(`API error: ${res.status}`); const data = await res.json(); - setHolders(data?.holders || []); + setHolders(Array.isArray(data) ? data : []); } catch (err: any) { setError(err.message); } finally { @@ -44,66 +48,108 @@ export default function TopHoldersPage() { } }; + const handleAccountClick = (account: string) => setSelectedAccount(account); + const closeModal = () => setSelectedAccount(null); + + const filteredHolders = holders.filter((holder) => + holder.account.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const formatValue = (value: string) => Number(value).toLocaleString(); + + const getRankStyle = (rank: number) => { + if (rank === 1) return "bg-yellow-300 font-bold"; + if (rank === 2) return "bg-gray-300 font-bold"; + if (rank === 3) return "bg-yellow-800 text-white font-bold"; + return ""; + }; + return (
    - + + {/* Filters */} - Filters + {t("filters.filtersAndSearch")} -
    - +
    + + + - +
    + setSearchTerm(value)} open={true} /> +
    + {/* Top Holders Table */} - Richlist + {t("pageTitle.topHolders")}
    - {loading &&

    Loading...

    } + {loading &&

    {t("modal.loading")}

    } {error && } - {!loading && !error && holders.length === 0 && } - {!loading && !error && holders.length > 0 && ( + {!loading && !error && filteredHolders.length === 0 && } + {!loading && !error && filteredHolders.length > 0 && ( - Rank - Account - Balance + {t("table.rank")} + {t("table.account")} + + {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} + - {holders.map((holder, idx) => ( - - {idx + 1 + (page - 1) * 50} - {holder.account} - {holder.balance} - - ))} + {filteredHolders.map((holder) => { + const displayRank = holder.rank + (page - 1) * 100; + return ( + handleAccountClick(holder.account)} + style={{ cursor: "pointer" }} + > + {displayRank} + {holder.account} + {formatValue(holder.value)} + + ); + })}
    )}
    - +
    + + {/* Modal */} + {selectedAccount && ( + + )}
    ); } -- GitLab From 340febf20f27595650330454527fed91354d2f73 Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Fri, 5 Sep 2025 17:05:22 +0300 Subject: [PATCH 3/9] myrichlist so far --- components/ExploreMenu.tsx | 4 +- components/footer.tsx | 4 + components/navbar.tsx | 5 ++ components/ui/TopHoldersPagination.tsx | 46 +++++++++++ pages/top-holders.tsx | 109 +++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 components/ui/TopHoldersPagination.tsx create mode 100644 pages/top-holders.tsx diff --git a/components/ExploreMenu.tsx b/components/ExploreMenu.tsx index 11e778b35..a1aeed5ea 100644 --- a/components/ExploreMenu.tsx +++ b/components/ExploreMenu.tsx @@ -1,9 +1,10 @@ // src/components/layout/ExploreMenu.tsx +// At the top with imports import React, { useState, useEffect, useRef } from "react"; import Link from "next/link"; import { useI18n } from "@/i18n/i18n"; -import { Users, Vote, Menu, UserCheck, SettingsIcon } from "lucide-react"; +import { Users, Vote, Menu, UserCheck, SettingsIcon,Award } from "lucide-react"; import { Popover, PopoverContent, @@ -55,6 +56,7 @@ export function ExploreMenu() { setIsOpen(false)} /> setIsOpen(false)} /> setIsOpen(false)} /> + setIsOpen(false)}/> setIsOpen(false)} />
    diff --git a/components/footer.tsx b/components/footer.tsx index 125d15fc5..3142c08fb 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -200,6 +200,10 @@ const Footer = () => { {t("footer.witnessSchedule")} +
  • + + Top Holders +
  • diff --git a/components/navbar.tsx b/components/navbar.tsx index a8264e309..033c4f4a2 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -99,6 +99,11 @@ export default function Navbar() { {t("navbar.witnessesTitle")} + setMenuOpen(false)}> + + Top Holders + + setMenuOpen(false)}> {t("navbar.additionalSettingsTitle")} diff --git a/components/ui/TopHoldersPagination.tsx b/components/ui/TopHoldersPagination.tsx new file mode 100644 index 000000000..b3c3efeaa --- /dev/null +++ b/components/ui/TopHoldersPagination.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { + Pagination, + PaginationItem, + PaginationLink, + PaginationPrevious, + PaginationNext, +} from "@/components/ui/pagination"; + +interface Props { + currentPage: number; + onPageChange: (page: number) => void; + totalPages: number; +} + +const TopHoldersPagination: React.FC = ({ currentPage, onPageChange, totalPages }) => { + const pagesToShow = 5; // Number of page buttons to display + const startPage = Math.max(1, currentPage - Math.floor(pagesToShow / 2)); + const endPage = Math.min(totalPages, startPage + pagesToShow - 1); + + const pageNumbers = []; + for (let i = startPage; i <= endPage; i++) pageNumbers.push(i); + + return ( + + onPageChange(Math.max(1, currentPage - 1))} + /> + {pageNumbers.map((page) => ( + + onPageChange(page)} + > + {page} + + + ))} + onPageChange(Math.min(totalPages, currentPage + 1))} + /> + + ); +}; + +export default TopHoldersPagination; diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx new file mode 100644 index 000000000..ac6231bdf --- /dev/null +++ b/pages/top-holders.tsx @@ -0,0 +1,109 @@ +// pages/top-holders.tsx +import { useEffect, useState } from "react"; +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; +import { Select } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; +import { Pagination } from "@/components/ui/pagination"; +import PageTitle from "@/components/PageTitle"; +import ErrorMessage from "@/components/ErrorMessage"; +import NoResult from "@/components/NoResult"; +import TopHoldersPagination from "@/components/ui/TopHoldersPagination"; + +interface Holder { + account: string; + balance: string; +} + +export default function TopHoldersPage() { + const [holders, setHolders] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const [coinType, setCoinType] = useState("HIVE"); + const [balanceType, setBalanceType] = useState("balance"); + + useEffect(() => { + fetchHolders(); + }, [page, coinType, balanceType]); + + const fetchHolders = async () => { + setLoading(true); + setError(null); + try { + const res = await fetch( + `https://api.syncad.com/balance-api/top-holders?coin-type=${coinType}&balance-type=${balanceType}&page=${page}` + ); + if (!res.ok) throw new Error(`API error: ${res.status}`); + const data = await res.json(); + setHolders(data?.holders || []); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
    + + + + + Filters + +
    + + + +
    +
    + + + + Richlist + +
    + {loading &&

    Loading...

    } + {error && } + {!loading && !error && holders.length === 0 && } + {!loading && !error && holders.length > 0 && ( + + + + Rank + Account + Balance + + + + {holders.map((holder, idx) => ( + + {idx + 1 + (page - 1) * 50} + {holder.account} + {holder.balance} + + ))} + +
    + )} + +
    + +
    +
    +
    +
    + ); +} -- GitLab From 0713d73379da2bb5551fe145158ab6c98f94a843 Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Mon, 8 Sep 2025 01:05:02 +0300 Subject: [PATCH 4/9] gar/#619_creating_richlist_page_progress --- components/ExploreMenu.tsx | 2 +- components/Modal.tsx | 69 +++++++++++++++++++++ components/footer.tsx | 2 +- components/navbar.tsx | 2 +- i18n/ar.json | 12 ++++ i18n/de.json | 12 ++++ i18n/en.json | 12 ++++ i18n/es.json | 12 ++++ i18n/fr.json | 12 ++++ i18n/it.json | 14 ++++- i18n/ja.json | 12 ++++ i18n/ko.json | 13 ++++ i18n/pl.json | 12 ++++ i18n/pt.json | 14 ++++- i18n/ro.json | 12 ++++ i18n/zh.json | 14 ++++- package-lock.json | 46 ++++++++++++++ package.json | 2 + pages/top-holders.tsx | 124 +++++++++++++++++++++++++------------ 19 files changed, 353 insertions(+), 45 deletions(-) create mode 100644 components/Modal.tsx diff --git a/components/ExploreMenu.tsx b/components/ExploreMenu.tsx index a1aeed5ea..4d69abcd6 100644 --- a/components/ExploreMenu.tsx +++ b/components/ExploreMenu.tsx @@ -56,7 +56,7 @@ export function ExploreMenu() { setIsOpen(false)} /> setIsOpen(false)} /> setIsOpen(false)} /> - setIsOpen(false)}/> + setIsOpen(false)}/> setIsOpen(false)} /> diff --git a/components/Modal.tsx b/components/Modal.tsx new file mode 100644 index 000000000..f1fbb2e17 --- /dev/null +++ b/components/Modal.tsx @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import BalanceHistoryChart from "@/components/balanceHistory/BalanceHistoryChart"; +import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory"; + +interface BalanceHistoryModalProps { + username: string; + coinType?: "HIVE" | "VESTS" | "HBD"; + onClose: () => void; +} + +const BalanceHistoryModal: React.FC = ({ + username, + coinType = "HIVE", + onClose, +}) => { + const [selectedCoinType, setSelectedCoinType] = useState<"HIVE" | "VESTS" | "HBD">(coinType); + + const { accountBalanceHistory, isAccountBalanceHistoryLoading, isAccountBalanceHistoryError } = + useBalanceHistory(username, selectedCoinType, 1, 100, "asc"); + + return ( +
    +
    +
    +

    {username} Balance History

    + +
    + + {/* Coin selection */} +
    + {["HIVE", "VESTS", "HBD"].map((coin) => ( + + ))} +
    + + {/* Content */} + {isAccountBalanceHistoryLoading ? ( +

    Loading balance history...

    + ) : isAccountBalanceHistoryError ? ( +

    Error loading balance history.

    + ) : !accountBalanceHistory?.operations_result?.length ? ( +

    No balance history available.

    + ) : ( +
    + +
    + )} +
    +
    + ); +}; + +export default BalanceHistoryModal; diff --git a/components/footer.tsx b/components/footer.tsx index 3142c08fb..4c6bf7b23 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -202,7 +202,7 @@ const Footer = () => {
  • - Top Holders + {t("pageTitle.topHolders")}
  • diff --git a/components/navbar.tsx b/components/navbar.tsx index 033c4f4a2..0eeb08287 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -101,7 +101,7 @@ export default function Navbar() { setMenuOpen(false)}> - Top Holders + {t("pageTitle.topHolders")} setMenuOpen(false)}> diff --git a/i18n/ar.json b/i18n/ar.json index fd3e40bdc..a36c3214b 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -912,4 +912,16 @@ "blockDetails.blockDetails": "تفاصيل_الكتلة", "commentsSearch.commentSearchResults": "نتائج_البحث_عن_التعليقات", "transactionPage.transactionDetailsRefferer": "تفاصيل_المعاملة" + ,"pageTitle.topHolders": "أعلى الحائزين", + "filters.filtersAndSearch": "المرشحات والبحث", + "filters.balance": "الرصيد", + "filters.savings": "المدخرات", + "table.rank": "الترتيب", + "table.account": "الحساب", + "table.balance": "الرصيد", + "table.savings": "المدخرات", + "modal.loading": "جار التحميل...", + "modal.noHistory": "لا يوجد تاريخ متاح.", + "modal.error": "خطأ في تحميل تاريخ الرصيد.", + "modal.close": "إغلاق" } \ No newline at end of file diff --git a/i18n/de.json b/i18n/de.json index 3be0514c2..4377cb2c2 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -912,4 +912,16 @@ "blockDetails.blockDetails": "block_details", "commentsSearch.commentSearchResults": "kommentar_suchergebnisse", "transactionPage.transactionDetailsRefferer": "transaktionsdetails" + , "pageTitle.topHolders": "Top-Inhaber", + "filters.filtersAndSearch": "Filter & Suche", + "filters.balance": "Kontostand", + "filters.savings": "Ersparnisse", + "table.rank": "Rang", + "table.account": "Konto", + "table.balance": "Kontostand", + "table.savings": "Ersparnisse", + "modal.loading": "Lädt...", + "modal.noHistory": "Keine Historie verfügbar.", + "modal.error": "Fehler beim Laden des Kontostandverlaufs.", + "modal.close": "Schließen" } \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index d0ed10cbd..ab43dfecc 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -912,4 +912,16 @@ "accountSearch.accountReactResults": "account_search_results", "commentsSearch.commentSearchResults": "comment_search_results", "blockDetails.blockDetails": "block_details" + ,"pageTitle.topHolders": "Top Holders", + "filters.filtersAndSearch": "Filters & Search", + "filters.balance": "Balance", + "filters.savings": "Savings", + "table.rank": "Rank", + "table.account": "Account", + "table.balance": "Balance", + "table.savings": "Savings", + "modal.loading": "Loading...", + "modal.noHistory": "No history available.", + "modal.error": "Error loading balance history.", + "modal.close": "Close" } \ No newline at end of file diff --git a/i18n/es.json b/i18n/es.json index 3b77dde13..7adf352cb 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -911,4 +911,16 @@ "blockDetails.blockDetails": "detalles_bloque", "commentsSearch.commentSearchResults": "resultados_busqueda_comentarios", "transactionPage.transactionDetailsRefferer": "detalles_transaccion" + ,"pageTitle.topHolders": "Principales Titulares", + "filters.filtersAndSearch": "Filtros y Búsqueda", + "filters.balance": "Saldo", + "filters.savings": "Ahorros", + "table.rank": "Rango", + "table.account": "Cuenta", + "table.balance": "Saldo", + "table.savings": "Ahorros", + "modal.loading": "Cargando...", + "modal.noHistory": "No hay historial disponible.", + "modal.error": "Error al cargar el historial de saldo.", + "modal.close": "Cerrar" } \ No newline at end of file diff --git a/i18n/fr.json b/i18n/fr.json index 31d426283..52e1e5697 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -911,4 +911,16 @@ "blockDetails.blockDetails": "details_bloc", "commentsSearch.commentSearchResults": "resultats_recherche_commentaires", "transactionPage.transactionDetailsRefferer": "details_transaction" +, "pageTitle.topHolders": "Principaux Détenteurs", + "filters.filtersAndSearch": "Filtres & Recherche", + "filters.balance": "Solde", + "filters.savings": "Économies", + "table.rank": "Rang", + "table.account": "Compte", + "table.balance": "Solde", + "table.savings": "Économies", + "modal.loading": "Chargement...", + "modal.noHistory": "Aucun historique disponible.", + "modal.error": "Erreur lors du chargement de l'historique du solde.", + "modal.close": "Fermer" } \ No newline at end of file diff --git a/i18n/it.json b/i18n/it.json index 0e5470f2e..596b7e6d3 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -903,7 +903,19 @@ "settingsPage.radialViewDescription": "Mostra risorse come il potere di voto usando indicatori circolari. (Predefinito)", "settingsPage.linearViewLabel": "Vista lineare", "settingsPage.linearViewDescription": "Mostra risorse come il potere di voto usando le tradizionali barre orizzontali.", - "accountSearch.accountReactResults": "risultati_ricerca_account", + "pageTitle.topHolders": "Principali Detentori", + "filters.filtersAndSearch": "Filtri e Ricerca", + "filters.balance": "Saldo", + "filters.savings": "Risparmi", + "table.rank": "Posizione", + "table.account": "Account", + "table.balance": "Saldo", + "table.savings": "Risparmi", + "modal.loading": "Caricamento...", + "modal.noHistory": "Nessuna cronologia disponibile.", + "modal.error": "Errore durante il caricamento dello storico saldo.", + "modal.close": "Chiudi" + ,"accountSearch.accountReactResults": "risultati_ricerca_account", "blockDetails.blockDetails": "dettagli_blocco", "commentsSearch.commentSearchResults": "risultati_ricerca_commenti", "transactionPage.transactionDetailsRefferer": "dettagli_transazione" diff --git a/i18n/ja.json b/i18n/ja.json index 93cb325bc..bf17dba3f 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -912,4 +912,16 @@ "blockDetails.blockDetails": "ブロック詳細", "commentsSearch.commentSearchResults": "コメント検索結果", "transactionPage.transactionDetailsRefferer": "取引詳細" +, "pageTitle.topHolders": "トップ保有者", + "filters.filtersAndSearch": "フィルターと検索", + "filters.balance": "残高", + "filters.savings": "貯蓄", + "table.rank": "ランク", + "table.account": "アカウント", + "table.balance": "残高", + "table.savings": "貯蓄", + "modal.loading": "読み込み中...", + "modal.noHistory": "履歴はありません。", + "modal.error": "残高履歴の読み込み中にエラーが発生しました。", + "modal.close": "閉じる" } \ No newline at end of file diff --git a/i18n/ko.json b/i18n/ko.json index 4158fc01d..1f4a9e234 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -912,4 +912,17 @@ "blockDetails.blockDetails": "블록_세부정보", "commentsSearch.commentSearchResults": "댓글_검색_결과", "transactionPage.transactionDetailsRefferer": "거래_세부정보" + , + "pageTitle.topHolders": "상위 보유자", + "filters.filtersAndSearch": "필터 및 검색", + "filters.balance": "잔액", + "filters.savings": "저축", + "table.rank": "순위", + "table.account": "계정", + "table.balance": "잔액", + "table.savings": "저축", + "modal.loading": "로딩 중...", + "modal.noHistory": "사용 가능한 기록이 없습니다.", + "modal.error": "잔액 기록을 불러오는 중 오류가 발생했습니다.", + "modal.close": "닫기" } \ No newline at end of file diff --git a/i18n/pl.json b/i18n/pl.json index 657ea7366..b90defc72 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -912,4 +912,16 @@ "blockDetails.blockDetails": "szczegoly_bloku", "commentsSearch.commentSearchResults": "wyniki_wyszukiwania_komentarzy", "transactionPage.transactionDetailsRefferer": "szczegoly_transakcji" +, "pageTitle.topHolders": "Najwięksi Posiadacze", + "filters.filtersAndSearch": "Filtry i Wyszukiwanie", + "filters.balance": "Saldo", + "filters.savings": "Oszczędności", + "table.rank": "Pozycja", + "table.account": "Konto", + "table.balance": "Saldo", + "table.savings": "Oszczędności", + "modal.loading": "Ładowanie...", + "modal.noHistory": "Brak dostępnej historii.", + "modal.error": "Błąd podczas ładowania historii salda.", + "modal.close": "Zamknij" } \ No newline at end of file diff --git a/i18n/pt.json b/i18n/pt.json index ca8c9f881..da92b1361 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -908,7 +908,19 @@ "settingsPage.radialViewDescription": "Mostrar recursos como poder de voto usando indicadores circulares. (Padrão)", "settingsPage.linearViewLabel": "Visualização Linear", "settingsPage.linearViewDescription": "Mostrar recursos como poder de voto usando barras horizontais tradicionais.", - "accountSearch.accountReactResults": "resultados_pesquisa_conta", + "pageTitle.topHolders": "Principais Detentores", + "filters.filtersAndSearch": "Filtros e Pesquisa", + "filters.balance": "Saldo", + "filters.savings": "Poupança", + "table.rank": "Classificação", + "table.account": "Conta", + "table.balance": "Saldo", + "table.savings": "Poupança", + "modal.loading": "Carregando...", + "modal.noHistory": "Nenhum histórico disponível.", + "modal.error": "Erro ao carregar histórico de saldo.", + "modal.close": "Fechar" + ,"accountSearch.accountReactResults": "resultados_pesquisa_conta", "blockDetails.blockDetails": "detalhes_bloco", "commentsSearch.commentSearchResults": "resultados_pesquisa_comentarios", "transactionPage.transactionDetailsRefferer": "detalhes_transacao" diff --git a/i18n/ro.json b/i18n/ro.json index 7dccfe0ab..0d0a6ebef 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -911,4 +911,16 @@ "blockDetails.blockDetails": "detalii_bloc", "commentsSearch.commentSearchResults": "rezultate_cautare_comentarii", "transactionPage.transactionDetailsRefferer": "detalii_tranzactie" +, "pageTitle.topHolders": "Cei mai mari deținători", + "filters.filtersAndSearch": "Filtre și Căutare", + "filters.balance": "Sold", + "filters.savings": "Economii", + "table.rank": "Rang", + "table.account": "Cont", + "table.balance": "Sold", + "table.savings": "Economii", + "modal.loading": "Se încarcă...", + "modal.noHistory": "Nu există istoric disponibil.", + "modal.error": "Eroare la încărcarea istoricului soldului.", + "modal.close": "Închide" } \ No newline at end of file diff --git a/i18n/zh.json b/i18n/zh.json index 8944827dc..dbf5c2914 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -911,5 +911,17 @@ "accountSearch.accountReactResults": "账户搜索结果", "blockDetails.blockDetails": "区块详情", "commentsSearch.commentSearchResults": "评论搜索结果", - "transactionPage.transactionDetailsRefferer": "交易详情" + "transactionPage.transactionDetailsRefferer": "交易详情" ,"pageTitle.topHolders": "持币最多者", + "filters.filtersAndSearch": "筛选与搜索", + "filters.balance": "余额", + "filters.savings": "储蓄", + "table.rank": "排名", + "table.account": "账户", + "table.balance": "余额", + "table.savings": "储蓄", + "modal.loading": "加载中...", + "modal.noHistory": "没有可用历史记录。", + "modal.error": "加载余额历史出错。", + "modal.close": "关闭" + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 84d1a2ac3..42be30eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/react": "19.1.8", "@types/react-dom": "19.1.6", "autoprefixer": "10.4.14", + "chart.js": "^4.5.0", "child_process": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -43,6 +44,7 @@ "playwright-merge-html-reports": "^0.2.8", "postcss": "^8.4.31", "react": "18.3.1", + "react-chartjs-2": "^5.3.0", "react-country-flag": "^3.1.0", "react-day-picker": "^8.10.0", "react-dom": "18.3.1", @@ -706,6 +708,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "node_modules/@next/env": { "version": "15.3.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", @@ -3075,6 +3082,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", @@ -6605,6 +6623,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-country-flag": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", @@ -8485,6 +8512,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "@next/env": { "version": "15.3.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", @@ -9809,6 +9841,14 @@ "supports-color": "^7.1.0" } }, + "chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", @@ -12253,6 +12293,12 @@ "loose-envify": "^1.1.0" } }, + "react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "requires": {} + }, "react-country-flag": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", diff --git a/package.json b/package.json index 675a2354a..81f6015ac 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/react": "19.1.8", "@types/react-dom": "19.1.6", "autoprefixer": "10.4.14", + "chart.js": "^4.5.0", "child_process": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -53,6 +54,7 @@ "playwright-merge-html-reports": "^0.2.8", "postcss": "^8.4.31", "react": "18.3.1", + "react-chartjs-2": "^5.3.0", "react-country-flag": "^3.1.0", "react-day-picker": "^8.10.0", "react-dom": "18.3.1", diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx index ac6231bdf..36d9077d6 100644 --- a/pages/top-holders.tsx +++ b/pages/top-holders.tsx @@ -1,29 +1,33 @@ -// pages/top-holders.tsx import { useEffect, useState } from "react"; import { Card, CardHeader, CardTitle } from "@/components/ui/card"; -import { Select } from "@/components/ui/select"; -import { Button } from "@/components/ui/button"; import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; -import { Pagination } from "@/components/ui/pagination"; import PageTitle from "@/components/PageTitle"; import ErrorMessage from "@/components/ErrorMessage"; import NoResult from "@/components/NoResult"; import TopHoldersPagination from "@/components/ui/TopHoldersPagination"; +import SearchBar from "@/components/SearchBar"; +import BalanceHistoryModal from "@/components/Modal"; +import { useI18n } from "@/i18n/i18n"; interface Holder { + rank: number; account: string; - balance: string; + value: string; } export default function TopHoldersPage() { + const { t } = useI18n(); const [holders, setHolders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [page, setPage] = useState(1); - const [coinType, setCoinType] = useState("HIVE"); - const [balanceType, setBalanceType] = useState("balance"); + const [coinType, setCoinType] = useState<"HIVE" | "HBD" | "VESTS">("HIVE"); + const [balanceType, setBalanceType] = useState<"balance" | "savings_balance">("balance"); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedAccount, setSelectedAccount] = useState(null); useEffect(() => { + if (coinType === "VESTS" && balanceType !== "balance") setBalanceType("balance"); fetchHolders(); }, [page, coinType, balanceType]); @@ -36,7 +40,7 @@ export default function TopHoldersPage() { ); if (!res.ok) throw new Error(`API error: ${res.status}`); const data = await res.json(); - setHolders(data?.holders || []); + setHolders(Array.isArray(data) ? data : []); } catch (err: any) { setError(err.message); } finally { @@ -44,66 +48,108 @@ export default function TopHoldersPage() { } }; + const handleAccountClick = (account: string) => setSelectedAccount(account); + const closeModal = () => setSelectedAccount(null); + + const filteredHolders = holders.filter((holder) => + holder.account.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const formatValue = (value: string) => Number(value).toLocaleString(); + + const getRankStyle = (rank: number) => { + if (rank === 1) return "bg-yellow-300 font-bold"; + if (rank === 2) return "bg-gray-300 font-bold"; + if (rank === 3) return "bg-yellow-800 text-white font-bold"; + return ""; + }; + return (
    - + + {/* Filters */} - Filters + {t("filters.filtersAndSearch")} -
    - +
    + + + - +
    + setSearchTerm(value)} open={true} /> +
    + {/* Top Holders Table */} - Richlist + {t("pageTitle.topHolders")}
    - {loading &&

    Loading...

    } + {loading &&

    {t("modal.loading")}

    } {error && } - {!loading && !error && holders.length === 0 && } - {!loading && !error && holders.length > 0 && ( + {!loading && !error && filteredHolders.length === 0 && } + {!loading && !error && filteredHolders.length > 0 && ( - Rank - Account - Balance + {t("table.rank")} + {t("table.account")} + + {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} + - {holders.map((holder, idx) => ( - - {idx + 1 + (page - 1) * 50} - {holder.account} - {holder.balance} - - ))} + {filteredHolders.map((holder) => { + const displayRank = holder.rank + (page - 1) * 100; + return ( + handleAccountClick(holder.account)} + style={{ cursor: "pointer" }} + > + {displayRank} + {holder.account} + {formatValue(holder.value)} + + ); + })}
    )}
    - +
    + + {/* Modal */} + {selectedAccount && ( + + )}
    ); } -- GitLab From 536a6b97c3aed1af4153fb4ce187d54fca7c4666 Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Tue, 9 Sep 2025 09:24:03 +0300 Subject: [PATCH 5/9] gar/#619 created richlist page,added the filtering and search, the balance hostory currently appearing in hive blog api only as the hive node,and made it responiive in mobile view --- components/Modal.tsx | 25 +++- i18n/en.json | 15 ++- pages/top-holders.tsx | 257 ++++++++++++++++++++++++++++++------------ 3 files changed, 222 insertions(+), 75 deletions(-) diff --git a/components/Modal.tsx b/components/Modal.tsx index f1fbb2e17..b6f0368e2 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import BalanceHistoryChart from "@/components/balanceHistory/BalanceHistoryChart"; import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory"; @@ -18,6 +18,21 @@ const BalanceHistoryModal: React.FC = ({ const { accountBalanceHistory, isAccountBalanceHistoryLoading, isAccountBalanceHistoryError } = useBalanceHistory(username, selectedCoinType, 1, 100, "asc"); + // Map API result to chart-compatible format + const parsedOperations = useMemo(() => { + return accountBalanceHistory?.operations_result?.map((op: any) => ({ + timestamp: op.timestamp, + balance: Number(op.balance || 0), + balance_change: Number(op.balance_change || 0), + savings_balance: op.savings_balance !== undefined ? Number(op.savings_balance) : undefined, + savings_balance_change: + op.savings_balance_change !== undefined ? Number(op.savings_balance_change) : undefined, + hivePrice: op.hivePrice || "0", + dollarValue: op.dollarValue !== undefined ? Number(op.dollarValue) : undefined, + convertedHive: op.convertedHive !== undefined ? Number(op.convertedHive) : undefined, + })); + }, [accountBalanceHistory]); + return (
    @@ -48,14 +63,14 @@ const BalanceHistoryModal: React.FC = ({

    Loading balance history...

    ) : isAccountBalanceHistoryError ? (

    Error loading balance history.

    - ) : !accountBalanceHistory?.operations_result?.length ? ( + ) : !parsedOperations?.length ? (

    No balance history available.

    ) : (
    diff --git a/i18n/en.json b/i18n/en.json index ecbc19f7e..72f9a8a9f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -911,6 +911,19 @@ "transactionPage.transactionDetailsRefferer": "transaction_details", "accountSearch.accountReactResults": "account_search_results", "commentsSearch.commentSearchResults": "comment_search_results", - "blockDetails.blockDetails": "block_details" + "blockDetails.blockDetails": "block_details", + "pageTitle.topHolders": "Top Holders", +"filters.filtersAndSearch": "Filters & Search", +"filters.balance": "Balance", +"filters.savings": "Savings", +"table.rank": "Rank", +"table.account": "Account", +"table.balance": "Balance", +"table.savings": "Savings", +"modal.loading": "Loading...", +"modal.noHistory": "No history available.", +"modal.error": "Error loading balance history.", +"modal.close": "Close" + } \ No newline at end of file diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx index 36d9077d6..222bd9a21 100644 --- a/pages/top-holders.tsx +++ b/pages/top-holders.tsx @@ -1,13 +1,22 @@ import { useEffect, useState } from "react"; -import { Card, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card } from "@/components/ui/card"; import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; import PageTitle from "@/components/PageTitle"; import ErrorMessage from "@/components/ErrorMessage"; import NoResult from "@/components/NoResult"; -import TopHoldersPagination from "@/components/ui/TopHoldersPagination"; import SearchBar from "@/components/SearchBar"; import BalanceHistoryModal from "@/components/Modal"; +import FilterSectionToggle from "@/components/account/FilterSectionToggle"; import { useI18n } from "@/i18n/i18n"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationPrevious, + PaginationNext, +} from "@/components/ui/pagination"; +import DataExport from "@/components/DataExport"; interface Holder { rank: number; @@ -16,7 +25,7 @@ interface Holder { } export default function TopHoldersPage() { - const { t } = useI18n(); + const { t } = useI18n(); const [holders, setHolders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -25,6 +34,7 @@ export default function TopHoldersPage() { const [balanceType, setBalanceType] = useState<"balance" | "savings_balance">("balance"); const [searchTerm, setSearchTerm] = useState(""); const [selectedAccount, setSelectedAccount] = useState(null); + const [isFiltersVisible, setIsFiltersVisible] = useState(false); useEffect(() => { if (coinType === "VESTS" && balanceType !== "balance") setBalanceType("balance"); @@ -57,98 +67,207 @@ export default function TopHoldersPage() { const formatValue = (value: string) => Number(value).toLocaleString(); - const getRankStyle = (rank: number) => { - if (rank === 1) return "bg-yellow-300 font-bold"; - if (rank === 2) return "bg-gray-300 font-bold"; - if (rank === 3) return "bg-yellow-800 text-white font-bold"; - return ""; - }; - return ( -
    - - - {/* Filters */} - - - {t("filters.filtersAndSearch")} - -
    - - - - -
    - setSearchTerm(value)} open={true} /> +
    + {/* Header: outer Card*/} + + {/* Desktop header*/} +
    + + setIsFiltersVisible(!isFiltersVisible)} + /> +
    + + {/* Mobile header */} +
    +
    + +
    +
    + setIsFiltersVisible(!isFiltersVisible)} + />
    - {/* Top Holders Table */} - - - {t("pageTitle.topHolders")} - + {/* Filters/Search*/} + {isFiltersVisible && ( + +
    + + + + +
    + setSearchTerm(value)} open={true} /> +
    +
    +
    + )} + + {/* Export button: */} +
    + {!loading && !error && filteredHolders.length > 0 && ( + ({ + Rank: holder.rank + (page - 1) * 100, + Account: holder.account, + [balanceType === "savings_balance" ? "Savings" : "Balance"]: holder.value, + }))} + filename={`top_holders_${coinType.toLowerCase()}.csv`} + skipColumnSelection={true} + className="h-10 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded px-4" + /> + )} +
    + + {/* Table Card */} +
    {loading &&

    {t("modal.loading")}

    } {error && } {!loading && !error && filteredHolders.length === 0 && } + {!loading && !error && filteredHolders.length > 0 && ( - - - - {t("table.rank")} - {t("table.account")} - - {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} - - - - + <> + {/* Desktop table */} +
    +
    +
    + + + {t("table.rank")} + {t("table.account")} + + {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} + + + + + + {filteredHolders.map((holder) => { + const displayRank = holder.rank + (page - 1) * 100; + return ( + handleAccountClick(holder.account)} + className="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer text-gray-900 dark:text-gray-100" + > + {displayRank} + {holder.account} + {formatValue(holder.value)} + + ); + })} + +
    +
    +
    + + {/* Mobile compact list */} +
    {filteredHolders.map((holder) => { const displayRank = holder.rank + (page - 1) * 100; return ( - handleAccountClick(holder.account)} - style={{ cursor: "pointer" }} + className="border rounded p-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 cursor-pointer" > - {displayRank} - {holder.account} - {formatValue(holder.value)} - +
    + {t("table.rank")} + {displayRank} +
    +
    + {t("table.account")} + {holder.account} +
    +
    + + {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} + + {formatValue(holder.value)} +
    +
    ); })} - - +
    + )} -
    - + {/* Pagination: */} +
    + + setPage((prev) => Math.max(prev - 1, 1))} /> + + {Array.from({ length: 5 }).map((_, i) => { + const pageNumber = i + 1; + return ( + + setPage(pageNumber)} + > + {pageNumber} + + + ); + })} + + setPage((prev) => Math.min(prev + 1, 5))} /> +
    + {/* Mobile pagination */} +
    +
    + + setPage((prev) => Math.max(prev - 1, 1))} /> + + {Array.from({ length: 5 }).map((_, i) => { + const pageNumber = i + 1; + return ( + + setPage(pageNumber)} + > + {pageNumber} + + + ); + })} + + setPage((prev) => Math.min(prev + 1, 5))} /> + +
    +
    + {/* Modal */} {selectedAccount && ( - + )}
    ); -- GitLab From 2cb7fe80d51601fd72ae6f0ccbb03fe55de93268 Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Thu, 11 Sep 2025 21:24:11 +0300 Subject: [PATCH 6/9] worked on the notes for richlist page --- components/TableSearchBar.tsx | 27 +++ hooks/common/useTopHolders.ts | 48 ++++++ i18n/ar.json | 6 +- i18n/de.json | 7 +- i18n/en.json | 28 +-- i18n/es.json | 6 +- i18n/fr.json | 7 +- i18n/it.json | 8 +- i18n/ja.json | 8 +- i18n/pl.json | 8 +- i18n/pt.json | 8 +- i18n/ro.json | 8 +- i18n/zh.json | 6 +- pages/top-holders.tsx | 313 +++++++++++++++++----------------- 14 files changed, 308 insertions(+), 180 deletions(-) create mode 100644 components/TableSearchBar.tsx create mode 100644 hooks/common/useTopHolders.ts diff --git a/components/TableSearchBar.tsx b/components/TableSearchBar.tsx new file mode 100644 index 000000000..ed8dcaff3 --- /dev/null +++ b/components/TableSearchBar.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { Search } from "lucide-react"; +import { useI18n } from "@/i18n/i18n"; + +interface TableSearchBarProps { + value: string; + onChange: (value: string) => void; +} + +const TableSearchBar: React.FC = ({ value, onChange }) => { + const { t } = useI18n(); + + return ( +
    + onChange(e.target.value)} + placeholder={t("filters.searchUser")} + className="w-full border rounded pl-10 pr-3 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-400" + /> + +
    + ); +}; + +export default TableSearchBar; diff --git a/hooks/common/useTopHolders.ts b/hooks/common/useTopHolders.ts new file mode 100644 index 000000000..c6ab5a5d7 --- /dev/null +++ b/hooks/common/useTopHolders.ts @@ -0,0 +1,48 @@ +// hooks/common/useTopHolders.ts +import { useState, useEffect } from "react"; + +export type CoinType = "HIVE" | "HBD" | "VESTS"; +export type BalanceType = "balance" | "savings_balance"; + +export interface Holder { + rank: number; + account: string; + value: string; +} + +interface UseTopHoldersProps { + page: number; + coinType: CoinType; + balanceType: BalanceType; +} + +export default function useTopHolders({ page, coinType, balanceType }: UseTopHoldersProps) { + const [holders, setHolders] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (coinType === "VESTS" && balanceType !== "balance") return; + + const fetchHolders = async () => { + setLoading(true); + setError(null); + try { + const res = await fetch( + `https://api.syncad.com/balance-api/top-holders?coin-type=${coinType}&balance-type=${balanceType}&page=${page}` + ); + if (!res.ok) throw new Error(`API error: ${res.status}`); + const data = await res.json(); + setHolders(Array.isArray(data) ? data : []); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchHolders(); + }, [page, coinType, balanceType]); + + return { holders, loading, error }; +} diff --git a/i18n/ar.json b/i18n/ar.json index 7e22879dd..b204f0efe 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -923,6 +923,10 @@ "modal.loading": "جار التحميل...", "modal.noHistory": "لا يوجد تاريخ متاح.", "modal.error": "خطأ في تحميل تاريخ الرصيد.", - "modal.close": "إغلاق" + "modal.close": "إغلاق", + "topHoldersPage.infoDescription": "تُظهر هذه الصفحة أكبر الحائزين لكل عملة. استخدم الفلاتر للبحث، والفرز، وتحديد الأرصدة.", +"filters.ascending": "تصاعدي", +"filters.descending": "تنازلي", +"filters.searchUser":"البحث عن مستخدم" } \ No newline at end of file diff --git a/i18n/de.json b/i18n/de.json index 4377cb2c2..92c0401c4 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -923,5 +923,10 @@ "modal.loading": "Lädt...", "modal.noHistory": "Keine Historie verfügbar.", "modal.error": "Fehler beim Laden des Kontostandverlaufs.", - "modal.close": "Schließen" + "modal.close": "Schließen", + "topHoldersPage.infoDescription": "Diese Seite zeigt die Top-Inhaber jeder Münze. Verwenden Sie Filter, um zu suchen, zu sortieren und Guthaben auszuwählen.", +"filters.ascending": "Aufsteigend", +"filters.descending": "Absteigend", +"filters.searchUser": "Nach Benutzer suchen" + } \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 72f9a8a9f..d52a64315 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -913,17 +913,23 @@ "commentsSearch.commentSearchResults": "comment_search_results", "blockDetails.blockDetails": "block_details", "pageTitle.topHolders": "Top Holders", -"filters.filtersAndSearch": "Filters & Search", -"filters.balance": "Balance", -"filters.savings": "Savings", -"table.rank": "Rank", -"table.account": "Account", -"table.balance": "Balance", -"table.savings": "Savings", -"modal.loading": "Loading...", -"modal.noHistory": "No history available.", -"modal.error": "Error loading balance history.", -"modal.close": "Close" + "filters.ascending":"Ascending", + "filters.descending":"Descending", + "topHoldersPage.infoDescription": "This page shows the top holders for each coin. Use filters to search, sort, and select balances.", + + "filters.filtersAndSearch": "Filters & Search", + "filters.balance": "Balance", + "filters.savings": "Savings", + "table.rank": "Rank", + "table.account": "Account", + "table.balance": "Balance", + "table.savings": "Savings", + "modal.loading": "Loading...", + "modal.noHistory": "No history available.", + "modal.error": "Error loading balance history.", + "modal.close": "Close", + "filters.searchUser": "Search by user" + } \ No newline at end of file diff --git a/i18n/es.json b/i18n/es.json index 7adf352cb..cea823578 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -922,5 +922,9 @@ "modal.loading": "Cargando...", "modal.noHistory": "No hay historial disponible.", "modal.error": "Error al cargar el historial de saldo.", - "modal.close": "Cerrar" + "modal.close": "Cerrar", + "topHoldersPage.infoDescription": "Esta página muestra los principales poseedores de cada moneda. Usa los filtros para buscar, ordenar y seleccionar saldos.", +"filters.ascending": "Ascendente", +"filters.descending": "Descendente", +"filters.searchUser": "Buscar por usuario" } \ No newline at end of file diff --git a/i18n/fr.json b/i18n/fr.json index 52e1e5697..8b16910cf 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -922,5 +922,10 @@ "modal.loading": "Chargement...", "modal.noHistory": "Aucun historique disponible.", "modal.error": "Erreur lors du chargement de l'historique du solde.", - "modal.close": "Fermer" + "modal.close": "Fermer", + "topHoldersPage.infoDescription": "Cette page affiche les principaux détenteurs de chaque crypto-monnaie. Utilisez les filtres pour rechercher, trier et sélectionner les soldes.", +"filters.ascending": "Croissant", +"filters.descending": "Décroissant", +"filters.searchUser": "Rechercher un utilisateur" + } \ No newline at end of file diff --git a/i18n/it.json b/i18n/it.json index 491f9bd50..2e2349347 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -919,5 +919,11 @@ ,"accountSearch.accountReactResults": "risultati_ricerca_account", "blockDetails.blockDetails": "dettagli_blocco", "commentsSearch.commentSearchResults": "risultati_ricerca_commenti", - "transactionPage.transactionDetailsRefferer": "dettagli_transazione" + "transactionPage.transactionDetailsRefferer": "dettagli_transazione", + "topHoldersPage.infoDescription": "Questa pagina mostra i principali detentori di ogni moneta. Usa i filtri per cercare, ordinare e selezionare i saldi.", +"filters.ascending": "Crescente", +"filters.descending": "Decrescente", +"filters.searchUser": "Cerca per utente" + + } \ No newline at end of file diff --git a/i18n/ja.json b/i18n/ja.json index bf17dba3f..17f08101d 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -923,5 +923,11 @@ "modal.loading": "読み込み中...", "modal.noHistory": "履歴はありません。", "modal.error": "残高履歴の読み込み中にエラーが発生しました。", - "modal.close": "閉じる" + "modal.close": "閉じる", + "topHoldersPage.infoDescription": "このページでは、各コインの上位保有者を表示します。フィルターを使用して検索、並べ替え、残高を選択してください。", +"filters.ascending": "昇順", +"filters.descending": "降順", +"filters.searchUser": "ユーザーで検索" + + } \ No newline at end of file diff --git a/i18n/pl.json b/i18n/pl.json index b90defc72..1be96d30a 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -923,5 +923,11 @@ "modal.loading": "Ładowanie...", "modal.noHistory": "Brak dostępnej historii.", "modal.error": "Błąd podczas ładowania historii salda.", - "modal.close": "Zamknij" + "modal.close": "Zamknij", + "topHoldersPage.infoDescription": "Ta strona pokazuje największych posiadaczy każdej monety. Użyj filtrów, aby wyszukiwać, sortować i wybierać salda.", +"filters.ascending": "Rosnąco", +"filters.descending": "Malejąco", +"filters.searchUser": "Szukaj użytkownika" + + } \ No newline at end of file diff --git a/i18n/pt.json b/i18n/pt.json index 14bf4a12a..7faf9a559 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -924,5 +924,11 @@ ,"accountSearch.accountReactResults": "resultados_pesquisa_conta", "blockDetails.blockDetails": "detalhes_bloco", "commentsSearch.commentSearchResults": "resultados_pesquisa_comentarios", - "transactionPage.transactionDetailsRefferer": "detalhes_transacao" + "transactionPage.transactionDetailsRefferer": "detalhes_transacao", + "topHoldersPage.infoDescription": "Esta página mostra os principais detentores de cada moeda. Use os filtros para pesquisar, classificar e selecionar saldos.", +"filters.ascending": "Ascendente", +"filters.descending": "Descendente", +"filters.searchUser": "Pesquisar por usuário" + + } \ No newline at end of file diff --git a/i18n/ro.json b/i18n/ro.json index 0d0a6ebef..8113beed0 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -922,5 +922,11 @@ "modal.loading": "Se încarcă...", "modal.noHistory": "Nu există istoric disponibil.", "modal.error": "Eroare la încărcarea istoricului soldului.", - "modal.close": "Închide" + "modal.close": "Închide", + "topHoldersPage.infoDescription": "Această pagină arată cei mai importanți deținători pentru fiecare monedă. Folosiți filtrele pentru a căuta, sorta și selecta soldurile.", +"filters.ascending": "Ascendent", +"filters.descending": "Descendent", +"filters.searchUser": "Căutați după utilizator" + + } \ No newline at end of file diff --git a/i18n/zh.json b/i18n/zh.json index 8a5c1c592..796f2a59d 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -922,7 +922,11 @@ "modal.loading": "加载中...", "modal.noHistory": "没有可用历史记录。", "modal.error": "加载余额历史出错。", - "modal.close": "关闭" + "modal.close": "关闭", + "topHoldersPage.infoDescription": "此页面显示每种货币的顶级持有者。使用筛选器进行搜索、排序和选择余额。", +"filters.ascending": "升序", +"filters.descending": "降序", +"filters.searchUser": "按用户搜索" diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx index 222bd9a21..fd5ac69fc 100644 --- a/pages/top-holders.tsx +++ b/pages/top-holders.tsx @@ -1,106 +1,135 @@ -import { useEffect, useState } from "react"; +// pages/TopHoldersPage.tsx +import { useState } from "react"; import { Card } from "@/components/ui/card"; import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; import PageTitle from "@/components/PageTitle"; import ErrorMessage from "@/components/ErrorMessage"; import NoResult from "@/components/NoResult"; -import SearchBar from "@/components/SearchBar"; import BalanceHistoryModal from "@/components/Modal"; import FilterSectionToggle from "@/components/account/FilterSectionToggle"; +import { Info } from "lucide-react"; import { useI18n } from "@/i18n/i18n"; -import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationPrevious, - PaginationNext, -} from "@/components/ui/pagination"; import DataExport from "@/components/DataExport"; - -interface Holder { - rank: number; - account: string; - value: string; -} +import TableSearchBar from "@/components/TableSearchBar"; +import CustomPagination from "@/components/CustomPagination"; +import useTopHolders, { CoinType, BalanceType } from "@/hooks/common/useTopHolders"; export default function TopHoldersPage() { const { t } = useI18n(); - const [holders, setHolders] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [page, setPage] = useState(1); - const [coinType, setCoinType] = useState<"HIVE" | "HBD" | "VESTS">("HIVE"); - const [balanceType, setBalanceType] = useState<"balance" | "savings_balance">("balance"); + const [coinType, setCoinType] = useState("HIVE"); + const [balanceType, setBalanceType] = useState("balance"); const [searchTerm, setSearchTerm] = useState(""); const [selectedAccount, setSelectedAccount] = useState(null); const [isFiltersVisible, setIsFiltersVisible] = useState(false); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); - useEffect(() => { - if (coinType === "VESTS" && balanceType !== "balance") setBalanceType("balance"); - fetchHolders(); - }, [page, coinType, balanceType]); + const totalCount = 500; - const fetchHolders = async () => { - setLoading(true); - setError(null); - try { - const res = await fetch( - `https://api.syncad.com/balance-api/top-holders?coin-type=${coinType}&balance-type=${balanceType}&page=${page}` - ); - if (!res.ok) throw new Error(`API error: ${res.status}`); - const data = await res.json(); - setHolders(Array.isArray(data) ? data : []); - } catch (err: any) { - setError(err.message); - } finally { - setLoading(false); - } - }; + const { holders, loading, error } = useTopHolders({ page, coinType, balanceType }); + + const defaultCoinType: CoinType = "HIVE"; + const defaultBalanceType: BalanceType = "balance"; + const defaultSortOrder: "asc" | "desc" = "desc"; + + const filtersChanged = + coinType !== defaultCoinType || balanceType !== defaultBalanceType || sortOrder !== defaultSortOrder; const handleAccountClick = (account: string) => setSelectedAccount(account); const closeModal = () => setSelectedAccount(null); - const filteredHolders = holders.filter((holder) => - holder.account.toLowerCase().includes(searchTerm.toLowerCase()) - ); + const filteredHolders = holders + .filter((holder) => holder.account.toLowerCase().includes(searchTerm.toLowerCase())) + .sort((a, b) => { + const valA = parseFloat(a.value); + const valB = parseFloat(b.value); + if (isNaN(valA) || isNaN(valB)) return 0; + return sortOrder === "asc" ? valA - valB : valB - valA; + }); + + const formatValue = (value: string, coinType: CoinType) => { + let num = parseFloat(value); + if (isNaN(num)) return value; + num = num / 1000; + const decimals = coinType === "VESTS" ? 6 : 3; + return parseFloat(num.toFixed(decimals)).toLocaleString(undefined, { maximumFractionDigits: decimals }); + }; + +const prepareExportData = () => { + return filteredHolders.map((holder) => ({ + [t("table.rank")]: holder.rank + (page - 1) * 100, + [t("table.account")]: holder.account, + [balanceType === "savings_balance" + ? t("table.savings") + : t("table.balance")]: holder.value, + })); +}; - const formatValue = (value: string) => Number(value).toLocaleString(); + const exportFileName = `top_holders_${coinType.toLowerCase()}.csv`; return ( -
    - {/* Header: outer Card*/} - - {/* Desktop header*/} -
    - +
    + {/* Top Section */} + +
    +
    + +
    + +
    + {t("topHoldersPage.infoDescription")} +
    +
    +
    setIsFiltersVisible(!isFiltersVisible)} />
    - {/* Mobile header */} -
    -
    - -
    -
    - setIsFiltersVisible(!isFiltersVisible)} - /> +
    +
    + +
    + +
    + {t("topHoldersPage.infoDescription")} +
    +
    + setIsFiltersVisible(!isFiltersVisible)} + />
    - {/* Filters/Search*/} + {/* Filter Section */} {isFiltersVisible && ( - +
    setBalanceType(e.target.value as "balance" | "savings_balance")} + onChange={(e) => setBalanceType(e.target.value as BalanceType)} disabled={coinType === "VESTS"} className="border rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 w-full sm:w-auto" > @@ -118,31 +147,36 @@ export default function TopHoldersPage() { + +
    - setSearchTerm(value)} open={true} /> +
    )} - {/* Export button: */} -
    + {/* Export Button */} +
    {!loading && !error && filteredHolders.length > 0 && ( ({ - Rank: holder.rank + (page - 1) * 100, - Account: holder.account, - [balanceType === "savings_balance" ? "Savings" : "Balance"]: holder.value, - }))} - filename={`top_holders_${coinType.toLowerCase()}.csv`} + data={prepareExportData()} + filename={exportFileName} skipColumnSelection={true} className="h-10 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded px-4" /> )}
    - {/* Table Card */} - + {/* Table */} +
    {loading &&

    {t("modal.loading")}

    } {error && } @@ -150,41 +184,38 @@ export default function TopHoldersPage() { {!loading && !error && filteredHolders.length > 0 && ( <> - {/* Desktop table */} -
    -
    - - - - {t("table.rank")} - {t("table.account")} - - {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} - - - - - - {filteredHolders.map((holder) => { - const displayRank = holder.rank + (page - 1) * 100; - return ( - handleAccountClick(holder.account)} - className="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer text-gray-900 dark:text-gray-100" - > - {displayRank} - {holder.account} - {formatValue(holder.value)} - - ); - })} - -
    -
    + {/* Desktop Table */} +
    + + + + {t("table.rank")} + {t("table.account")} + + {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} + + + + + {filteredHolders.map((holder) => { + const displayRank = holder.rank + (page - 1) * 100; + return ( + handleAccountClick(holder.account)} + className="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer text-gray-900 dark:text-gray-100" + > + {displayRank} + {holder.account} + {formatValue(holder.value, coinType)} + + ); + })} + +
    - {/* Mobile compact list */} + {/* Mobile Table */}
    {filteredHolders.map((holder) => { const displayRank = holder.rank + (page - 1) * 100; @@ -206,65 +237,29 @@ export default function TopHoldersPage() { {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} - {formatValue(holder.value)} + {formatValue(holder.value, coinType)}
    ); })}
    + + {/* Pagination */} +
    + +
    )} - - {/* Pagination: */} -
    - - setPage((prev) => Math.max(prev - 1, 1))} /> - - {Array.from({ length: 5 }).map((_, i) => { - const pageNumber = i + 1; - return ( - - setPage(pageNumber)} - > - {pageNumber} - - - ); - })} - - setPage((prev) => Math.min(prev + 1, 5))} /> - -
    - {/* Mobile pagination */} -
    -
    - - setPage((prev) => Math.max(prev - 1, 1))} /> - - {Array.from({ length: 5 }).map((_, i) => { - const pageNumber = i + 1; - return ( - - setPage(pageNumber)} - > - {pageNumber} - - - ); - })} - - setPage((prev) => Math.min(prev + 1, 5))} /> - -
    -
    - {/* Modal */} {selectedAccount && ( -- GitLab From 6ab21b036f6eee0dbac79539a1fce2b558a40776 Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Mon, 15 Sep 2025 20:07:36 +0300 Subject: [PATCH 7/9] fixing richlist --- Config.ts | 4 + .../BalanceHistoryModal.tsx} | 0 .../TopHoldersSearchBar.tsx} | 0 components/ui/TopHoldersPagination.tsx | 46 --- hooks/common/useTopHolders.ts | 67 ++-- pages/top-holders.tsx | 287 ++++++++---------- services/FetchingService.ts | 14 + types/Hive.ts | 13 + types/Rest.ts | 9 + utils/PageTitlesInfo.tsx | 92 +++++- 10 files changed, 284 insertions(+), 248 deletions(-) rename components/{Modal.tsx => top-holders/BalanceHistoryModal.tsx} (100%) rename components/{TableSearchBar.tsx => top-holders/TopHoldersSearchBar.tsx} (100%) delete mode 100644 components/ui/TopHoldersPagination.tsx diff --git a/Config.ts b/Config.ts index 6445c9a73..119e9d457 100644 --- a/Config.ts +++ b/Config.ts @@ -14,6 +14,10 @@ export const config = { gitHash: process.env.NEXT_PUBLIC_COMMIT_HASH, get lastCommitHashRepoUrl() { return `https://gitlab.syncad.com/hive/block_explorer_ui/-/commit/${this.gitHash}`; + }, + topHolders: { + totalCount: 500, // Total number of top holders + pageSize: 100, // Page size for pagination }, opsBodyLimit: 100000, commentOperationsTypeIds: [0, 1, 17, 19, 51, 53, 61, 63, 72, 73], diff --git a/components/Modal.tsx b/components/top-holders/BalanceHistoryModal.tsx similarity index 100% rename from components/Modal.tsx rename to components/top-holders/BalanceHistoryModal.tsx diff --git a/components/TableSearchBar.tsx b/components/top-holders/TopHoldersSearchBar.tsx similarity index 100% rename from components/TableSearchBar.tsx rename to components/top-holders/TopHoldersSearchBar.tsx diff --git a/components/ui/TopHoldersPagination.tsx b/components/ui/TopHoldersPagination.tsx deleted file mode 100644 index b3c3efeaa..000000000 --- a/components/ui/TopHoldersPagination.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; -import { - Pagination, - PaginationItem, - PaginationLink, - PaginationPrevious, - PaginationNext, -} from "@/components/ui/pagination"; - -interface Props { - currentPage: number; - onPageChange: (page: number) => void; - totalPages: number; -} - -const TopHoldersPagination: React.FC = ({ currentPage, onPageChange, totalPages }) => { - const pagesToShow = 5; // Number of page buttons to display - const startPage = Math.max(1, currentPage - Math.floor(pagesToShow / 2)); - const endPage = Math.min(totalPages, startPage + pagesToShow - 1); - - const pageNumbers = []; - for (let i = startPage; i <= endPage; i++) pageNumbers.push(i); - - return ( - - onPageChange(Math.max(1, currentPage - 1))} - /> - {pageNumbers.map((page) => ( - - onPageChange(page)} - > - {page} - - - ))} - onPageChange(Math.min(totalPages, currentPage + 1))} - /> - - ); -}; - -export default TopHoldersPagination; diff --git a/hooks/common/useTopHolders.ts b/hooks/common/useTopHolders.ts index c6ab5a5d7..fe80e7f51 100644 --- a/hooks/common/useTopHolders.ts +++ b/hooks/common/useTopHolders.ts @@ -1,48 +1,27 @@ -// hooks/common/useTopHolders.ts -import { useState, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import fetchingService from "@/services/FetchingService"; +import Hive from "@/types/Hive"; export type CoinType = "HIVE" | "HBD" | "VESTS"; export type BalanceType = "balance" | "savings_balance"; -export interface Holder { - rank: number; - account: string; - value: string; -} - -interface UseTopHoldersProps { - page: number; - coinType: CoinType; - balanceType: BalanceType; -} - -export default function useTopHolders({ page, coinType, balanceType }: UseTopHoldersProps) { - const [holders, setHolders] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (coinType === "VESTS" && balanceType !== "balance") return; - - const fetchHolders = async () => { - setLoading(true); - setError(null); - try { - const res = await fetch( - `https://api.syncad.com/balance-api/top-holders?coin-type=${coinType}&balance-type=${balanceType}&page=${page}` - ); - if (!res.ok) throw new Error(`API error: ${res.status}`); - const data = await res.json(); - setHolders(Array.isArray(data) ? data : []); - } catch (err: any) { - setError(err.message); - } finally { - setLoading(false); - } - }; - - fetchHolders(); - }, [page, coinType, balanceType]); - - return { holders, loading, error }; -} +const useTopHolders = (coinType: CoinType, balanceType: BalanceType, page: number) => { + const { + data: holdersData, + isLoading: isTopHoldersLoading, + error: isTopHoldersError, + } = useQuery({ + queryKey: ["topHolders", coinType, balanceType, page], + queryFn: () => fetchingService.getTopHolders(coinType, balanceType, page), + enabled: !(coinType === "VESTS" && balanceType !== "balance"), + refetchOnWindowFocus: false, + }); + + return { + holdersData: holdersData ?? [], + isTopHoldersLoading, + isTopHoldersError: isTopHoldersError as Error | null, + }; +}; + +export default useTopHolders; diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx index fd5ac69fc..d53f5eeb1 100644 --- a/pages/top-holders.tsx +++ b/pages/top-holders.tsx @@ -1,132 +1,146 @@ -// pages/TopHoldersPage.tsx import { useState } from "react"; +import { useRouter } from "next/router"; import { Card } from "@/components/ui/card"; import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; import PageTitle from "@/components/PageTitle"; import ErrorMessage from "@/components/ErrorMessage"; import NoResult from "@/components/NoResult"; -import BalanceHistoryModal from "@/components/Modal"; import FilterSectionToggle from "@/components/account/FilterSectionToggle"; -import { Info } from "lucide-react"; import { useI18n } from "@/i18n/i18n"; import DataExport from "@/components/DataExport"; -import TableSearchBar from "@/components/TableSearchBar"; import CustomPagination from "@/components/CustomPagination"; import useTopHolders, { CoinType, BalanceType } from "@/hooks/common/useTopHolders"; +import { config } from "@/Config"; +import { formatNumber } from "@/lib/utils"; +import { ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; export default function TopHoldersPage() { const { t } = useI18n(); + const router = useRouter(); const [page, setPage] = useState(1); const [coinType, setCoinType] = useState("HIVE"); const [balanceType, setBalanceType] = useState("balance"); - const [searchTerm, setSearchTerm] = useState(""); - const [selectedAccount, setSelectedAccount] = useState(null); const [isFiltersVisible, setIsFiltersVisible] = useState(false); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); - const totalCount = 500; + const totalCount = config.topHolders.totalCount; - const { holders, loading, error } = useTopHolders({ page, coinType, balanceType }); + const { holdersData, isTopHoldersLoading, isTopHoldersError } = useTopHolders( + coinType, + balanceType, + page + ); const defaultCoinType: CoinType = "HIVE"; const defaultBalanceType: BalanceType = "balance"; const defaultSortOrder: "asc" | "desc" = "desc"; const filtersChanged = - coinType !== defaultCoinType || balanceType !== defaultBalanceType || sortOrder !== defaultSortOrder; - - const handleAccountClick = (account: string) => setSelectedAccount(account); - const closeModal = () => setSelectedAccount(null); + coinType !== defaultCoinType || + balanceType !== defaultBalanceType || + sortOrder !== defaultSortOrder; - const filteredHolders = holders - .filter((holder) => holder.account.toLowerCase().includes(searchTerm.toLowerCase())) - .sort((a, b) => { - const valA = parseFloat(a.value); - const valB = parseFloat(b.value); - if (isNaN(valA) || isNaN(valB)) return 0; - return sortOrder === "asc" ? valA - valB : valB - valA; - }); + // Navigate to user's profile page on click + const handleAccountClick = (account: string) => { + router.push(`/@${account}`); + }; - const formatValue = (value: string, coinType: CoinType) => { - let num = parseFloat(value); - if (isNaN(num)) return value; - num = num / 1000; - const decimals = coinType === "VESTS" ? 6 : 3; - return parseFloat(num.toFixed(decimals)).toLocaleString(undefined, { maximumFractionDigits: decimals }); + // Sort holders based on value and sortOrder + const filteredHolders = holdersData.sort((a, b) => { + const valA = parseFloat(a.value); + const valB = parseFloat(b.value); + if (isNaN(valA) || isNaN(valB)) return 0; + return sortOrder === "asc" ? valA - valB : valB - valA; + }); + + const formatValueForDisplay = (value: string, coinType: CoinType) => { + const isVest = coinType === "VESTS"; + return formatNumber(value, isVest); }; -const prepareExportData = () => { - return filteredHolders.map((holder) => ({ - [t("table.rank")]: holder.rank + (page - 1) * 100, - [t("table.account")]: holder.account, - [balanceType === "savings_balance" - ? t("table.savings") - : t("table.balance")]: holder.value, - })); -}; + const prepareExportData = () => + filteredHolders.map((holder) => ({ + [t("table.rank")]: holder.rank + (page - 1) * 100, + [t("table.account")]: holder.account, + [balanceType === "savings_balance" ? t("table.savings") : t("table.balance")]: holder.value, + })); const exportFileName = `top_holders_${coinType.toLowerCase()}.csv`; + const HolderRow = ({ + rank, + account, + value, + }: { + rank: number; + account: string; + value: string; + }) => ( + handleAccountClick(account)} + className="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer text-gray-900 dark:text-gray-100" + data-testid="top-holders-table-row" + > + {rank} + {account} + {formatValueForDisplay(value, coinType)} + + ); + + // Table header with sortable arrows + const TableHeaderRow = () => ( + + + setSortOrder(sortOrder === "asc" ? "desc" : "asc")} + > +
    + {t("table.rank")} + {sortOrder === "asc" ? ( + + ) : ( + + )} +
    +
    + {t("table.account")} + setSortOrder(sortOrder === "asc" ? "desc" : "asc")} + > +
    + {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} + +
    +
    +
    +
    + ); + return ( -
    +
    {/* Top Section */} - -
    -
    - -
    - -
    - {t("topHoldersPage.infoDescription")} -
    -
    + +
    +
    +
    - setIsFiltersVisible(!isFiltersVisible)} - /> -
    - -
    -
    - -
    - -
    - {t("topHoldersPage.infoDescription")} -
    -
    +
    + setIsFiltersVisible(!isFiltersVisible)} + />
    - setIsFiltersVisible(!isFiltersVisible)} - />
    {/* Filter Section */} {isFiltersVisible && ( - -
    + +
    - - - -
    - -
    )} {/* Export Button */} -
    - {!loading && !error && filteredHolders.length > 0 && ( +
    + {!isTopHoldersLoading && !isTopHoldersError && filteredHolders.length > 0 && ( )} @@ -177,38 +177,27 @@ const prepareExportData = () => { {/* Table */} -
    - {loading &&

    {t("modal.loading")}

    } - {error && } - {!loading && !error && filteredHolders.length === 0 && } +
    + {isTopHoldersLoading &&

    {t("modal.loading")}

    } + {isTopHoldersError && } + {!isTopHoldersLoading && !isTopHoldersError && filteredHolders.length === 0 && } - {!loading && !error && filteredHolders.length > 0 && ( + {!isTopHoldersLoading && !isTopHoldersError && filteredHolders.length > 0 && ( <> {/* Desktop Table */}
    - - - {t("table.rank")} - {t("table.account")} - - {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} - - - - + + {filteredHolders.map((holder) => { const displayRank = holder.rank + (page - 1) * 100; return ( - handleAccountClick(holder.account)} - className="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer text-gray-900 dark:text-gray-100" - > - {displayRank} - {holder.account} - {formatValue(holder.value, coinType)} - + rank={displayRank} + account={holder.account} + value={holder.value} + /> ); })} @@ -216,32 +205,23 @@ const prepareExportData = () => { {/* Mobile Table */} -
    - {filteredHolders.map((holder) => { - const displayRank = holder.rank + (page - 1) * 100; - return ( -
    handleAccountClick(holder.account)} - className="border rounded p-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 cursor-pointer" - > -
    - {t("table.rank")} - {displayRank} -
    -
    - {t("table.account")} - {holder.account} -
    -
    - - {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")} - - {formatValue(holder.value, coinType)} -
    -
    - ); - })} +
    +
    + + + {filteredHolders.map((holder) => { + const displayRank = holder.rank + (page - 1) * 100; + return ( + + ); + })} + +
    {/* Pagination */} @@ -251,19 +231,12 @@ const prepareExportData = () => { onPageChange={setPage} pageSize={100} totalCount={totalCount} - className="rounded" - isMirrored={false} />
    )}
    - - {/* Modal */} - {selectedAccount && ( - - )}
    ); } diff --git a/services/FetchingService.ts b/services/FetchingService.ts index 463c52b60..2bde9a8c9 100644 --- a/services/FetchingService.ts +++ b/services/FetchingService.ts @@ -769,6 +769,20 @@ class FetchingService { async getBlockChainProps(): Promise { return await this.extendedHiveChain!.api.condenser_api.get_chain_properties([]); } + async getTopHolders( + coinType: "HIVE" | "HBD" | "VESTS", + balanceType: "balance" | "savings_balance", + page: number +): Promise { + return await this.extendedHiveChain!.restApi["balance-api"].topHolders({ + "coin-type": coinType, + "balance-type": balanceType, + page, + + }); +} + + } const fetchingService = new FetchingService(); diff --git a/types/Hive.ts b/types/Hive.ts index 95b2b4560..f6d3de261 100644 --- a/types/Hive.ts +++ b/types/Hive.ts @@ -1094,6 +1094,19 @@ namespace Hive { account_subsidy_limit!: number; } + export class GetTopHoldersParams { + "coin-type"!: string; + "balance-type"!: string; + page!: number; + "page-size"?: number; +} + +export class TopHolder { + rank!: number; + account!: string; + value!: string; +} + } export default Hive; diff --git a/types/Rest.ts b/types/Rest.ts index 8bda3399d..abb7e89a5 100644 --- a/types/Rest.ts +++ b/types/Rest.ts @@ -171,5 +171,14 @@ export const extendedRest = { result: Hive.AccountRecurrentBalanceTransfersResponse, urlPath: "accounts/{accountName}/recurrent-transfers", }, + topHolders: { + params: Hive.GetTopHoldersParams, + result: Hive.TopHolder, + urlPath: "top-holders", + responseArray: true, + }, + + }, + }; diff --git a/utils/PageTitlesInfo.tsx b/utils/PageTitlesInfo.tsx index 23eea1b1a..84614ac18 100644 --- a/utils/PageTitlesInfo.tsx +++ b/utils/PageTitlesInfo.tsx @@ -68,6 +68,84 @@ const TransactionDetailsInfoEn: React.FC = () => (
  • You can change the setting from Data View in the main menu to view data in other formats
  • ); +//top holders section +// --- English (en) --- +const TopHoldersInfoEn: React.FC = () => ( +
      +
    • This page shows the top holders for each coin. Use filters to search, sort, and select balances.
    • +
    +); + +// --- Spanish (es) --- +const TopHoldersInfoEs: React.FC = () => ( +
      +
    • Esta página muestra los principales poseedores de cada moneda. Usa filtros para buscar, ordenar y seleccionar saldos.
    • +
    +); + +// --- Italian (it) --- +const TopHoldersInfoIt: React.FC = () => ( +
      +
    • Questa pagina mostra i principali detentori per ogni moneta. Usa i filtri per cercare, ordinare e selezionare i saldi.
    • +
    +); + +// --- German (de) --- +const TopHoldersInfoDe: React.FC = () => ( +
      +
    • Diese Seite zeigt die größten Inhaber jeder Münze. Verwenden Sie Filter, um Salden zu suchen, zu sortieren und auszuwählen.
    • +
    +); + +// --- Portuguese (pt) --- +const TopHoldersInfoPt: React.FC = () => ( +
      +
    • Esta página mostra os principais detentores de cada moeda. Use filtros para pesquisar, classificar e selecionar saldos.
    • +
    +); + +// --- French (fr) --- +const TopHoldersInfoFr: React.FC = () => ( +
      +
    • Cette page affiche les principaux détenteurs de chaque monnaie. Utilisez des filtres pour rechercher, trier et sélectionner les soldes.
    • +
    +); + +// --- Polish (pl) --- +const TopHoldersInfoPl: React.FC = () => ( +
      +
    • Ta strona pokazuje największych posiadaczy każdej monety. Użyj filtrów, aby wyszukać, posortować i wybrać salda.
    • +
    +); + +// --- Chinese (Simplified) (zh) --- +const TopHoldersInfoZh: React.FC = () => ( +
      +
    • 此页面显示每种代币的最大持有者。使用筛选器来搜索、排序和选择余额。
    • +
    +); + +// --- Japanese (ja) --- +const TopHoldersInfoJa: React.FC = () => ( +
      +
    • このページでは、各コインの最大保有者が表示されます。フィルターを使って検索、並べ替え、残高の選択ができます。
    • +
    +); + +// --- Romanian (ro) --- +const TopHoldersInfoRo: React.FC = () => ( +
      +
    • Această pagină afișează principalii deținători pentru fiecare monedă. Folosiți filtre pentru a căuta, sorta și selecta soldurile.
    • +
    +); + +// --- Arabic (ar) --- +const TopHoldersInfoAr: React.FC = () => ( +
      +
    • تعرض هذه الصفحة كبار الحائزين لكل عملة. استخدم الفلاتر للبحث والفرز وتحديد الأرصدة.
    • +
    +); + // src/components/info/ProposalsInfo.tsx const ProposalsInfoEn: React.FC = () => ( @@ -804,8 +882,20 @@ const pageTitlesInfo: InfoContent = { ro: , ar: }, + "pageTitle.topHolders": { + en: , + es: , + it: , + de: , + pt: , + fr: , + pl: , + zh: , + ja: , + ro: , + ar: +}, - }; export default pageTitlesInfo; \ No newline at end of file -- GitLab From 7754f722521ce29a3cb479d7d202ccf25a823c6b Mon Sep 17 00:00:00 2001 From: Dima Rifai Date: Mon, 15 Sep 2025 21:33:57 +0300 Subject: [PATCH 8/9] Peer Notes --- pages/top-holders.tsx | 273 +++++++++++++++++++++++++++++------------- types/Hive.ts | 19 ++- 2 files changed, 200 insertions(+), 92 deletions(-) diff --git a/pages/top-holders.tsx b/pages/top-holders.tsx index d53f5eeb1..b12516fd5 100644 --- a/pages/top-holders.tsx +++ b/pages/top-holders.tsx @@ -1,7 +1,7 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { Card } from "@/components/ui/card"; -import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table"; +import { Table, TableHeader, TableBody, TableRow, TableCell, TableHead } from "@/components/ui/table"; import PageTitle from "@/components/PageTitle"; import ErrorMessage from "@/components/ErrorMessage"; import NoResult from "@/components/NoResult"; @@ -12,7 +12,16 @@ import CustomPagination from "@/components/CustomPagination"; import useTopHolders, { CoinType, BalanceType } from "@/hooks/common/useTopHolders"; import { config } from "@/Config"; import { formatNumber } from "@/lib/utils"; -import { ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; +import { ChevronDown, ChevronUp, ChevronsUpDown, Loader2 } from "lucide-react"; +import { getHiveAvatarUrl } from "@/utils/HiveBlogUtils"; +import Image from "next/image"; +import Link from "next/link"; +import useDynamicGlobal from "@/hooks/api/homePage/useDynamicGlobal"; +import { convertVestsToHP } from "@/utils/Calculations"; +import { useHiveChainContext } from "@/contexts/HiveChainContext"; +import Hive from "@/types/Hive"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; export default function TopHoldersPage() { const { t } = useI18n(); @@ -41,10 +50,6 @@ export default function TopHoldersPage() { balanceType !== defaultBalanceType || sortOrder !== defaultSortOrder; - // Navigate to user's profile page on click - const handleAccountClick = (account: string) => { - router.push(`/@${account}`); - }; // Sort holders based on value and sortOrder const filteredHolders = holdersData.sort((a, b) => { @@ -65,6 +70,39 @@ export default function TopHoldersPage() { [t("table.account")]: holder.account, [balanceType === "savings_balance" ? t("table.savings") : t("table.balance")]: holder.value, })); + const { dynamicGlobalData } = useDynamicGlobal() as any; + const [totalVestingShares, setTotalVestingShares] = useState( + dynamicGlobalData?.headBlockDetails.rawTotalVestingShares + ); + const [totalVestingFundHive, setTotalVestingFundHive] = useState( + dynamicGlobalData?.headBlockDetails.rawTotalVestingFundHive + ); + + const { hiveChain } = useHiveChainContext(); + + useEffect(() => { + if (dynamicGlobalData?.headBlockDetails) { + setTotalVestingShares( + dynamicGlobalData.headBlockDetails.rawTotalVestingShares + ); + setTotalVestingFundHive( + dynamicGlobalData.headBlockDetails.rawTotalVestingFundHive + ); + } + }, [dynamicGlobalData]); + + const fetchHivePower = (value: string, isHP: boolean): string => { + if (isHP) { + if (!hiveChain) return ""; + return convertVestsToHP( + hiveChain, + value, + totalVestingFundHive, + totalVestingShares + ); + } + return `${formatNumber(value, true)} VESTS`; + }; const exportFileName = `top_holders_${coinType.toLowerCase()}.csv`; @@ -79,12 +117,31 @@ export default function TopHoldersPage() { }) => ( handleAccountClick(account)} - className="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer text-gray-900 dark:text-gray-100" + //onClick={() => handleAccountClick(account)} + className="hover:bg-rowHover cursor-pointer text-sm" data-testid="top-holders-table-row" > {rank} - {account} + +
    + {`${account}'s + + +
    + + + + + {account}
    {formatValueForDisplay(value, coinType)}
    ); @@ -93,7 +150,7 @@ export default function TopHoldersPage() { const TableHeaderRow = () => ( - setSortOrder(sortOrder === "asc" ? "desc" : "asc")} > @@ -105,9 +162,9 @@ export default function TopHoldersPage() { )}
    - - {t("table.account")} - + {t("table.account")} + setSortOrder(sortOrder === "asc" ? "desc" : "asc")} > @@ -115,15 +172,15 @@ export default function TopHoldersPage() { {balanceType === "savings_balance" ? t("table.savings") : t("table.balance")}
    - + ); return ( -
    +
    {/* Top Section */} - +
    @@ -139,7 +196,7 @@ export default function TopHoldersPage() { {/* Filter Section */} {isFiltersVisible && ( - +
    setCoinType(e.target.value as CoinType)} - className="border rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 w-full sm:w-auto" - > + - setBalanceType(e.target.value as BalanceType)} disabled={coinType === "VESTS"} className="border rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 w-full sm:w-auto"> @@ -221,130 +182,49 @@ export default function TopHoldersPage() { )} - {/* Export Button */} -
    - -
    - -
    - -
    - - - //setUnit(checked ? "hp" : "vests") - - /> - -
    + {/* Pagination */} +
    +
    - {/* }
    - {!isTopHoldersLoading && - !isTopHoldersError && - filteredHolders.length > 0 && ( - + {/* Export + Unit Toggle */} +
    +
    + {coinType === "VESTS" && ( +
    + + setUnit(checked ? "hp" : "vests")} /> + +
    )} -
    */} + +
    +
    {/* Table */} -
    - {isTopHoldersLoading && ( + {isTopHoldersLoading && (
    )} {!isTopHoldersLoading && isTopHoldersError && (

    - +

    )} - - - - {/* {isTopHoldersLoading &&

    {t("modal.loading")}

    } - {isTopHoldersError && ( - - )} - {!isTopHoldersLoading && - !isTopHoldersError && - filteredHolders.length === 0 && 0 && ( - <> - {/* Desktop Table */} -
    - - - - {filteredHolders.map((holder) => { - const displayRank = holder.rank + (page - 1) * 100; - return ( - - ); - })} - -
    -
    - - {/* Mobile Table */} - {/*
    - - - - {filteredHolders.map((holder) => { - const displayRank = holder.rank + (page - 1) * 100; - return ( - - ); - })} - -
    -
    */} - - {/* Pagination */} - - - )} -
    + {!isTopHoldersLoading && !isTopHoldersError && filteredHolders.length === 0 && } + {!isTopHoldersLoading && !isTopHoldersError && filteredHolders.length > 0 && ( + + + + {filteredHolders.map((holder) => { + const displayRank = holder.rank + (page - 1) * 100; + return ; + })} + +
    + )}
    ); -- GitLab