diff --git a/internal/handler/response.go b/internal/handler/response.go
index cfa506f..6024a93 100644
--- a/internal/handler/response.go
+++ b/internal/handler/response.go
@@ -7,6 +7,7 @@ import (
"github.com/a-h/templ"
"github.com/ditatompel/xmr-remote-nodes/internal/handler/views"
"github.com/ditatompel/xmr-remote-nodes/internal/monero"
+ "github.com/ditatompel/xmr-remote-nodes/internal/paging"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
@@ -30,24 +31,6 @@ func (s *fiberServer) homeHandler(c *fiber.Ctx) error {
return handler(c)
}
-// Render Remote Nodes Page
-func (s *fiberServer) remoteNodesHandler(c *fiber.Ctx) error {
- p := views.Meta{
- Title: "Public Monero Remote Nodes List",
- Description: "Although it's possible to use these existing public Monero nodes, you're MUST RUN AND USE YOUR OWN NODE!",
- Keywords: "monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc",
- Robots: "INDEX,FOLLOW",
- Permalink: "https://xmr.ditatompel.com/remote-nodes",
- Identifier: "/remote-nodes",
- }
-
- c.Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, p.Permalink))
- home := views.BaseLayout(p, views.RemoteNodes())
- handler := adaptor.HTTPHandler(templ.Handler(home))
-
- return handler(c)
-}
-
// Render Add Node Page
func (s *fiberServer) addNodeHandler(c *fiber.Ctx) error {
p := views.Meta{
@@ -102,20 +85,78 @@ func Node(c *fiber.Ctx) error {
})
}
-// Returns a list of nodes
+// Render Remote Nodes Page
+func (s *fiberServer) remoteNodesHandler(c *fiber.Ctx) error {
+ p := views.Meta{
+ Title: "Public Monero Remote Nodes List",
+ Description: "Although it's possible to use these existing public Monero nodes, you're MUST RUN AND USE YOUR OWN NODE!",
+ Keywords: "monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc",
+ Robots: "INDEX,FOLLOW",
+ Permalink: "https://xmr.ditatompel.com/remote-nodes",
+ Identifier: "/remote-nodes",
+ }
+
+ moneroRepo := monero.New()
+ query := monero.QueryNodes{
+ Paging: paging.Paging{
+ Limit: c.QueryInt("limit", 10), // rows per page
+ Page: c.QueryInt("page", 1),
+ SortBy: c.Query("sort_by", "id"),
+ SortDir: c.Query("sort_dir", "desc"),
+ SortDirection: c.Query("sort_direction", "desc"), // deprecated
+ Refresh: c.QueryInt("refresh", 0),
+ },
+ Host: c.Query("host"),
+ Nettype: c.Query("nettype", "any"),
+ Protocol: c.Query("protocol", "any"),
+ CC: c.Query("cc", "any"),
+ Status: c.QueryInt("status", -1),
+ CORS: c.QueryInt("cors", -1),
+ }
+
+ nodes, err := moneroRepo.Nodes(query)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "status": "error",
+ "message": err.Error(),
+ "data": nil,
+ })
+ }
+
+ pagination := paging.NewPagination(query.Page, nodes.TotalPages)
+
+ // handle request from HTMX
+ if c.Get("HX-Target") == "tbl_nodes" {
+ cmp := views.BlankLayout(views.TableNodes(nodes, query, pagination))
+ handler := adaptor.HTTPHandler(templ.Handler(cmp))
+ return handler(c)
+ }
+
+ c.Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, p.Permalink))
+ home := views.BaseLayout(p, views.RemoteNodes(nodes, query, pagination))
+ handler := adaptor.HTTPHandler(templ.Handler(home))
+
+ return handler(c)
+}
+
+// Returns a list of nodes (API)
func Nodes(c *fiber.Ctx) error {
moneroRepo := monero.New()
query := monero.QueryNodes{
- RowsPerPage: c.QueryInt("limit", 10),
- Page: c.QueryInt("page", 1),
- SortBy: c.Query("sort_by", "id"),
- SortDirection: c.Query("sort_direction", "desc"),
- Host: c.Query("host"),
- Nettype: c.Query("nettype", "any"),
- Protocol: c.Query("protocol", "any"),
- CC: c.Query("cc", "any"),
- Status: c.QueryInt("status", -1),
- CORS: c.QueryInt("cors", -1),
+ Paging: paging.Paging{
+ Limit: c.QueryInt("limit", 10), // rows per page
+ Page: c.QueryInt("page", 1),
+ SortBy: c.Query("sort_by", "id"),
+ SortDir: c.Query("sort_dir", "desc"),
+ SortDirection: c.Query("sort_direction", "desc"), // deprecated
+ Refresh: c.QueryInt("refresh", 0),
+ },
+ Host: c.Query("host"),
+ Nettype: c.Query("nettype", "any"),
+ Protocol: c.Query("protocol", "any"),
+ CC: c.Query("cc", "any"),
+ Status: c.QueryInt("status", -1),
+ CORS: c.QueryInt("cors", -1),
}
nodes, err := moneroRepo.Nodes(query)
diff --git a/internal/handler/views/partial_datatable.templ b/internal/handler/views/partial_datatable.templ
new file mode 100644
index 0000000..b07e93f
--- /dev/null
+++ b/internal/handler/views/partial_datatable.templ
@@ -0,0 +1,70 @@
+package views
+
+import (
+ "fmt"
+ "github.com/ditatompel/xmr-remote-nodes/internal/paging"
+)
+
+var availablePages = []int{5, 10, 20, 50, 100}
+
+templ DtRowPerPage(url, hxTarget string, rowsPerPage int, q interface{}) {
+
+
+ CHOOSE
+ for _, page := range availablePages {
+ { fmt.Sprintf("%d", page) }
+ }
+
+
+}
+
+templ DtRowCount(currentPage, rowsPerPage, totalRows int) {
+
+
+ if totalRows <= 0 {
+ No entries found
+ } else {
+ { fmt.Sprintf("%d", (rowsPerPage * currentPage) - rowsPerPage + 1) }
+ if rowsPerPage * currentPage > totalRows {
+ - { fmt.Sprintf("%d", totalRows) }
+ } else {
+ - { fmt.Sprintf("%d", rowsPerPage * currentPage) }
+ }
+ / { fmt.Sprintf("%d", totalRows) }
+ }
+
+
+}
+
+templ DtPagination(url, hxTarget string, q interface{}, p paging.Pagination) {
+
+
+
+}
diff --git a/internal/handler/views/partial_datatable_templ.go b/internal/handler/views/partial_datatable_templ.go
new file mode 100644
index 0000000..2cd1341
--- /dev/null
+++ b/internal/handler/views/partial_datatable_templ.go
@@ -0,0 +1,333 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.778
+package views
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "fmt"
+ "github.com/ditatompel/xmr-remote-nodes/internal/paging"
+)
+
+var availablePages = []int{5, 10, 20, 50, 100}
+
+func DtRowPerPage(url, hxTarget string, rowsPerPage int, q interface{}) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("CHOOSE ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, page := range availablePages {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", page))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 27, Col: 30}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func DtRowCount(currentPage, rowsPerPage, totalRows int) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var6 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var6 == nil {
+ templ_7745c5c3_Var6 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if totalRows <= 0 {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("No entries found")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (rowsPerPage*currentPage)-rowsPerPage+1))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 39, Col: 73}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if rowsPerPage*currentPage > totalRows {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalRows))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 41, Col: 40}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", rowsPerPage*currentPage))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 43, Col: 56}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" / ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalRows))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 45, Col: 39}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func DtPagination(url, hxTarget string, q interface{}, p paging.Pagination) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var11 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var11 == nil {
+ templ_7745c5c3_Var11 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/internal/handler/views/remote_nodes.templ b/internal/handler/views/remote_nodes.templ
index 943f3c4..6225f9d 100644
--- a/internal/handler/views/remote_nodes.templ
+++ b/internal/handler/views/remote_nodes.templ
@@ -1,6 +1,13 @@
package views
-templ RemoteNodes() {
+import (
+ "fmt"
+ "github.com/ditatompel/xmr-remote-nodes/internal/monero"
+ "github.com/ditatompel/xmr-remote-nodes/internal/paging"
+ "time"
+)
+
+templ RemoteNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) {
@@ -30,4 +37,49 @@ templ RemoteNodes() {
+
+
+ @TableNodes(data, q, p)
+
+
+}
+
+templ TableNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) {
+
+
+ @DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q)
+
+
+
+
+
+ Host:Port
+ Nettype
+ Protocol
+ Country
+ Status
+ Estimate Fee
+ Uptime
+ Check
+
+
+
+ for _, row := range data.Items {
+
+ { fmt.Sprintf("%s:%d", row.Hostname, row.Port) }
+ { row.Nettype } { fmt.Sprintf("%d", row.Height) }
+ { row.Protocol }
+ { row.CountryCode }
+ { fmt.Sprintf("%d", row.EstimateFee) }
+ { time.Unix(row.LastChecked, 0).Format("2006-01-02 15:04") }
+
+ }
+
+
+
+
+ @DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows)
+ @DtPagination("/remote-nodes", "#tbl_nodes", q, p)
+
+
}
diff --git a/internal/handler/views/remote_nodes_templ.go b/internal/handler/views/remote_nodes_templ.go
index a70436c..3698a26 100644
--- a/internal/handler/views/remote_nodes_templ.go
+++ b/internal/handler/views/remote_nodes_templ.go
@@ -8,7 +8,14 @@ package views
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
-func RemoteNodes() templ.Component {
+import (
+ "fmt"
+ "github.com/ditatompel/xmr-remote-nodes/internal/monero"
+ "github.com/ditatompel/xmr-remote-nodes/internal/paging"
+ "time"
+)
+
+func RemoteNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -29,7 +36,165 @@ func RemoteNodes() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Public Monero Remote Nodes List Monero remote node is a device on the internet running the Monero software with full copy of the Monero blockchain that doesn't run on the same local machine where the Monero wallet is located.
Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.
Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the Monero community suggests to to obtain the maximum possible privacy and to help decentralize the network.
")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Public Monero Remote Nodes List Monero remote node is a device on the internet running the Monero software with full copy of the Monero blockchain that doesn't run on the same local machine where the Monero wallet is located.
Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.
Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the Monero community suggests to to obtain the maximum possible privacy and to help decentralize the network.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = TableNodes(data, q, p).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func TableNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var2 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var2 == nil {
+ templ_7745c5c3_Var2 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Host:Port Nettype Protocol Country Status Estimate Fee Uptime Check ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, row := range data.Items {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s:%d", row.Hostname, row.Port))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 69, Col: 57}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(row.Nettype)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 70, Col: 24}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", row.Height))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 70, Col: 62}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(row.Protocol)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 71, Col: 25}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(row.CountryCode)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 72, Col: 28}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", row.EstimateFee))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 73, Col: 47}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(time.Unix(row.LastChecked, 0).Format("2006-01-02 15:04"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 74, Col: 69}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = DtPagination("/remote-nodes", "#tbl_nodes", q, p).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/handler/views/src/css/main.css b/internal/handler/views/src/css/main.css
index 6aed813..d8681a6 100644
--- a/internal/handler/views/src/css/main.css
+++ b/internal/handler/views/src/css/main.css
@@ -25,3 +25,11 @@ a.btn-link {
button.copy-input {
@apply px-2 shrink-0 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-e-md border border-transparent bg-orange-600 text-white hover:brightness-125 focus:outline-none focus:bg-orange-700 disabled:opacity-50 disabled:pointer-events-none;
}
+
+/* pagination */
+nav.pagination button.active {
+ @apply py-1.5 px-2 inline-flex items-center gap-x-2 text-sm font-bold rounded-lg border border-orange-500 bg-orange-500 text-white shadow-sm hover:brightness-125 disabled:opacity-90 disabled:pointer-events-none;
+}
+nav.pagination button {
+ @apply py-1.5 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg bg-neutral-800 border border-neutral-700 text-white shadow-sm hover:brightness-125 disabled:opacity-50 disabled:pointer-events-none;
+}
diff --git a/internal/monero/monero.go b/internal/monero/monero.go
index daec765..9f69e99 100644
--- a/internal/monero/monero.go
+++ b/internal/monero/monero.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log/slog"
+ "math"
"net"
"slices"
"strings"
@@ -13,6 +14,7 @@ import (
"github.com/ditatompel/xmr-remote-nodes/internal/database"
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
+ "github.com/ditatompel/xmr-remote-nodes/internal/paging"
"github.com/jmoiron/sqlx/types"
)
@@ -74,18 +76,13 @@ func (r *moneroRepo) Node(id int) (Node, error) {
// QueryNodes represents database query parameters
type QueryNodes struct {
- Host string
+ paging.Paging
+ Host string `url:"host,omitempty"`
Nettype string // Can be "any", mainnet, stagenet, testnet. Default: "any"
Protocol string // Can be "any", tor, http, https. Default: "any"
- CC string // 2 letter country code
+ CC string `url:"cc,omitempty"` // 2 letter country code
Status int
CORS int
-
- // pagination
- RowsPerPage int
- Page int
- SortBy string
- SortDirection string
}
// toSQL generates SQL query from query parameters
@@ -133,8 +130,14 @@ func (q *QueryNodes) toSQL() (args []interface{}, where string) {
if !slices.Contains([]string{"last_checked", "uptime"}, q.SortBy) {
q.SortBy = "last_checked"
}
+
+ // deprecated: Use SortDir instead
if q.SortDirection != "asc" {
- q.SortDirection = "DESC"
+ q.SortDir = "DESC"
+ }
+
+ if q.SortDir != "asc" {
+ q.SortDir = "DESC"
}
return args, where
@@ -143,6 +146,7 @@ func (q *QueryNodes) toSQL() (args []interface{}, where string) {
// Nodes represents a list of nodes
type Nodes struct {
TotalRows int `json:"total_rows"`
+ TotalPages int `json:"total_pages"` // total pages
RowsPerPage int `json:"rows_per_page"`
Items []*Node `json:"items"`
}
@@ -153,7 +157,7 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) {
var nodes Nodes
- nodes.RowsPerPage = q.RowsPerPage
+ nodes.RowsPerPage = q.Limit
qTotal := fmt.Sprintf(`
SELECT
@@ -166,7 +170,8 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) {
if err != nil {
return nodes, err
}
- args = append(args, q.RowsPerPage, (q.Page-1)*q.RowsPerPage)
+ nodes.TotalPages = int(math.Ceil(float64(nodes.TotalRows) / float64(q.Limit)))
+ args = append(args, q.Limit, (q.Page-1)*q.Limit)
query := fmt.Sprintf(`
SELECT
@@ -178,7 +183,7 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) {
%s
%s
LIMIT ?
- OFFSET ?`, where, q.SortBy, q.SortDirection)
+ OFFSET ?`, where, q.SortBy, q.SortDir)
err = r.db.Select(&nodes.Items, query, args...)
return nodes, err
diff --git a/internal/monero/monero_test.go b/internal/monero/monero_test.go
index b9cddf5..ad0625c 100644
--- a/internal/monero/monero_test.go
+++ b/internal/monero/monero_test.go
@@ -8,6 +8,7 @@ import (
"github.com/ditatompel/xmr-remote-nodes/internal/config"
"github.com/ditatompel/xmr-remote-nodes/internal/database"
+ "github.com/ditatompel/xmr-remote-nodes/internal/paging"
)
var testMySQL = true
@@ -39,50 +40,56 @@ func init() {
// go test -race ./internal/monero -run=TestQueryNodes_toSQL -v
func TestQueryNodes_toSQL(t *testing.T) {
tests := []struct {
- name string
- query QueryNodes
- wantArgs []interface{}
- wantWhere string
- wantSortBy string
- wantSortDirection string
+ name string
+ query QueryNodes
+ wantArgs []interface{}
+ wantWhere string
+ wantSortBy string
+ wantSortDir string
}{
{
name: "Default query",
query: QueryNodes{
- Host: "",
- Nettype: "any",
- Protocol: "any",
- CC: "any",
- Status: -1,
- CORS: -1,
- RowsPerPage: 10,
- Page: 1,
- SortBy: "last_checked",
- SortDirection: "desc",
+ Paging: paging.Paging{
+ Limit: 10,
+ Page: 1,
+ SortBy: "last_checked",
+ SortDir: "desc",
+ SortDirection: "desc", // deprecated
+ },
+ Host: "",
+ Nettype: "any",
+ Protocol: "any",
+ CC: "any",
+ Status: -1,
+ CORS: -1,
},
- wantArgs: []interface{}{},
- wantWhere: "",
- wantSortBy: "last_checked",
- wantSortDirection: "DESC",
+ wantArgs: []interface{}{},
+ wantWhere: "",
+ wantSortBy: "last_checked",
+ wantSortDir: "DESC",
},
{
name: "With host query",
query: QueryNodes{
- Host: "test",
- Nettype: "any",
- Protocol: "any",
- CC: "any",
- Status: -1,
- CORS: -1,
- RowsPerPage: 10,
- Page: 1,
- SortBy: "last_checked",
- SortDirection: "desc",
+ Paging: paging.Paging{
+ Limit: 10,
+ Page: 1,
+ SortBy: "last_checked",
+ SortDir: "desc",
+ SortDirection: "desc", // deprecated
+ },
+ Host: "test",
+ Nettype: "any",
+ Protocol: "any",
+ CC: "any",
+ Status: -1,
+ CORS: -1,
},
- wantArgs: []interface{}{"%test%", "%test%"},
- wantWhere: "WHERE (hostname LIKE ? OR ip_addr LIKE ?)",
- wantSortBy: "last_checked",
- wantSortDirection: "DESC",
+ wantArgs: []interface{}{"%test%", "%test%"},
+ wantWhere: "WHERE (hostname LIKE ? OR ip_addr LIKE ?)",
+ wantSortBy: "last_checked",
+ wantSortDir: "DESC",
},
}
for _, tt := range tests {
@@ -97,8 +104,8 @@ func TestQueryNodes_toSQL(t *testing.T) {
if tt.query.SortBy != tt.wantSortBy {
t.Errorf("QueryNodes.toSQL() gotSortBy = %v, want %v", tt.query.SortBy, tt.wantSortBy)
}
- if tt.query.SortDirection != tt.wantSortDirection {
- t.Errorf("QueryNodes.toSQL() gotSortDirection = %v, want %v", tt.query.SortDirection, tt.wantSortDirection)
+ if tt.query.SortDir != tt.wantSortDir {
+ t.Errorf("QueryNodes.toSQL() gotSortDir = %v, want %v", tt.query.SortDir, tt.wantSortDir)
}
})
}
@@ -108,16 +115,19 @@ func TestQueryNodes_toSQL(t *testing.T) {
// go test ./internal/monero -bench QueryNodes_toSQL -benchmem -run=^$ -v
func Benchmark_QueryNodes_toSQL(b *testing.B) {
q := QueryNodes{
- Host: "test",
- Nettype: "any",
- Protocol: "any",
- CC: "any",
- Status: -1,
- CORS: -1,
- RowsPerPage: 10,
- Page: 1,
- SortBy: "last_checked",
- SortDirection: "desc",
+ Paging: paging.Paging{
+ Limit: 10,
+ Page: 1,
+ SortBy: "last_checked",
+ SortDir: "desc",
+ SortDirection: "desc", // deprecated
+ },
+ Host: "test",
+ Nettype: "any",
+ Protocol: "any",
+ CC: "any",
+ Status: -1,
+ CORS: -1,
}
for i := 0; i < b.N; i++ {
_, _ = q.toSQL()