feat!: Added base datatable functionality

Deprecated: `SortDirection` is deprecated, use `SortDir` instead
This commit is contained in:
Cristian Ditaputratama 2024-10-31 22:45:26 +07:00
parent ca3ca881fd
commit 10182d9dbc
Signed by: ditatompel
GPG key ID: 31D3D06D77950979
8 changed files with 774 additions and 90 deletions

View file

@ -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,14 +85,72 @@ 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),
Paging: paging.Paging{
Limit: c.QueryInt("limit", 10), // rows per page
Page: c.QueryInt("page", 1),
SortBy: c.Query("sort_by", "id"),
SortDirection: c.Query("sort_direction", "desc"),
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"),

View file

@ -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{}) {
<div class="max-w-sm space-y-3">
<select
name="limit"
id="dt_limit"
class="py-2 px-3 pe-9 block bg-neutral-900 border-neutral-700 rounded-lg text-sm focus:border-orange-400 focus:ring-orange-400"
hx-get={ fmt.Sprintf("%s?%s", url, paging.EncodedQuery(q, []string{"limit"})) }
hx-trigger="change"
hx-push-url="true"
hx-target={ hxTarget }
hx-swap="outerHTML"
>
<option disabled>CHOOSE</option>
for _, page := range availablePages {
<option
value={ fmt.Sprintf("%d", page) }
selected?={ page == rowsPerPage }
>{ fmt.Sprintf("%d", page) }</option>
}
</select>
</div>
}
templ DtRowCount(currentPage, rowsPerPage, totalRows int) {
<div>
<p class="text-sm">
if totalRows <= 0 {
No entries found
} else {
<b>{ fmt.Sprintf("%d", (rowsPerPage * currentPage) - rowsPerPage + 1) }</b>
if rowsPerPage * currentPage > totalRows {
- <b>{ fmt.Sprintf("%d", totalRows) }</b>
} else {
- <b>{ fmt.Sprintf("%d", rowsPerPage * currentPage) }</b>
}
<b>/ { fmt.Sprintf("%d", totalRows) }</b>
}
</p>
</div>
}
templ DtPagination(url, hxTarget string, q interface{}, p paging.Pagination) {
<div>
<nav class="pagination inline-flex gap-x-2">
for _, page := range p.Pages {
if page == -1 {
<button class="cursor-not-allowed" disabled>...</button>
} else if page == p.CurrentPage {
<button class="active" disabled>{ fmt.Sprintf("%d", page) }</button>
} else {
<button
hx-get={ fmt.Sprintf("%s?%s&page=%d", url, paging.EncodedQuery(q, []string{"page"}), page) }
hx-push-url="true"
hx-target={ hxTarget }
hx-swap="outerHTML"
>{ fmt.Sprintf("%d", page) }</button>
}
}
</nav>
</div>
}

View file

@ -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("<div class=\"max-w-sm space-y-3\"><select name=\"limit\" id=\"dt_limit\" class=\"py-2 px-3 pe-9 block bg-neutral-900 border-neutral-700 rounded-lg text-sm focus:border-orange-400 focus:ring-orange-400\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?%s", url, paging.EncodedQuery(q, []string{"limit"})))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 16, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-trigger=\"change\" hx-push-url=\"true\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(hxTarget)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 19, Col: 23}
}
_, 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("\" hx-swap=\"outerHTML\"><option disabled>CHOOSE</option> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, page := range availablePages {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, 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: 25, Col: 36}
}
_, 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
}
if page == rowsPerPage {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
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", 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("</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div>")
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("<div><p class=\"text-sm\">")
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("<b>")
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("</b> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if rowsPerPage*currentPage > totalRows {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- <b>")
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("</b>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- <b>")
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("</b>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <b>/ ")
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("</b>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div>")
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("<div><nav class=\"pagination inline-flex gap-x-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, page := range p.Pages {
if page == -1 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"cursor-not-allowed\" disabled>...</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if page == p.CurrentPage {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"active\" disabled>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, 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: 58, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?%s&page=%d", url, paging.EncodedQuery(q, []string{"page"}), page))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 61, Col: 96}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-push-url=\"true\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(hxTarget)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 63, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, 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: 65, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</nav></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View file

@ -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) {
<!-- Hero -->
<section class="relative overflow-hidden pt-6">
<!-- Gradients -->
@ -30,4 +37,49 @@ templ RemoteNodes() {
</div>
</section>
<!-- End Hero -->
<div class="flex flex-col max-w-6xl mx-auto mb-10">
<div class="min-w-full inline-block align-middle">
@TableNodes(data, q, p)
</div>
</div>
}
templ TableNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) {
<div id="tbl_nodes" class="bg-neutral-800 border border-neutral-700 rounded-xl shadow-sm overflow-hidden">
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-neutral-700">
@DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q)
</div>
<div class="overflow-x-auto">
<table class="table-striped table-hover">
<thead>
<tr>
<th scope="col">Host:Port</th>
<th scope="col">Nettype</th>
<th scope="col">Protocol</th>
<th scope="col">Country</th>
<th scope="col">Status</th>
<th scope="col">Estimate Fee</th>
<th scope="col">Uptime</th>
<th scope="col">Check</th>
</tr>
</thead>
<tbody>
for _, row := range data.Items {
<tr>
<td>{ fmt.Sprintf("%s:%d", row.Hostname, row.Port) }</td>
<td>{ row.Nettype }<br/>{ fmt.Sprintf("%d", row.Height) } </td>
<td>{ row.Protocol }</td>
<td>{ row.CountryCode }</td>
<td>{ fmt.Sprintf("%d", row.EstimateFee) }</td>
<td>{ time.Unix(row.LastChecked, 0).Format("2006-01-02 15:04") }</td>
</tr>
}
</tbody>
</table>
</div>
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-neutral-700">
@DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows)
@DtPagination("/remote-nodes", "#tbl_nodes", q, p)
</div>
</div>
}

View file

@ -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("<!-- Hero --><section class=\"relative overflow-hidden pt-6\"><!-- Gradients --><div aria-hidden=\"true\" class=\"flex absolute -top-96 start-1/2 transform -translate-x-1/2\"><div class=\"bg-gradient-to-r blur-3xl w-[25rem] h-[44rem] rotate-[-60deg] transform -translate-x-[10rem] from-amber-800/30 to-orange-800/40\"></div><div class=\"bg-gradient-to-tl blur-3xl w-[90rem] h-[50rem] rounded-fulls origin-top-left -rotate-12 -translate-x-[15rem] from-orange-900/60 via-orange-900/40 to-amber-900/80\"></div></div><!-- End Gradients --><div class=\"relative z-10\"><div class=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 lg:py-16\"><div class=\"text-center\"><!-- Title --><div class=\"mt-5\"><h1 class=\"block font-extrabold text-4xl md:text-5xl lg:text-6xl text-neutral-200\">Public Monero Remote Nodes List</h1></div><!-- End Title --><div class=\"mt-5\"><p class=\"text-lg text-neutral-300\"><strong>Monero remote node</strong> 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.</p></div><hr class=\"mt-6\"></div><div class=\"max-w-3xl text-center mx-auto mt-8 prose prose-invert\"><p>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.</p><p>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 <strong>Monero community suggests to <span class=\"font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500\">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p></div></div></div></section><!-- End Hero -->")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!-- Hero --><section class=\"relative overflow-hidden pt-6\"><!-- Gradients --><div aria-hidden=\"true\" class=\"flex absolute -top-96 start-1/2 transform -translate-x-1/2\"><div class=\"bg-gradient-to-r blur-3xl w-[25rem] h-[44rem] rotate-[-60deg] transform -translate-x-[10rem] from-amber-800/30 to-orange-800/40\"></div><div class=\"bg-gradient-to-tl blur-3xl w-[90rem] h-[50rem] rounded-fulls origin-top-left -rotate-12 -translate-x-[15rem] from-orange-900/60 via-orange-900/40 to-amber-900/80\"></div></div><!-- End Gradients --><div class=\"relative z-10\"><div class=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 lg:py-16\"><div class=\"text-center\"><!-- Title --><div class=\"mt-5\"><h1 class=\"block font-extrabold text-4xl md:text-5xl lg:text-6xl text-neutral-200\">Public Monero Remote Nodes List</h1></div><!-- End Title --><div class=\"mt-5\"><p class=\"text-lg text-neutral-300\"><strong>Monero remote node</strong> 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.</p></div><hr class=\"mt-6\"></div><div class=\"max-w-3xl text-center mx-auto mt-8 prose prose-invert\"><p>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.</p><p>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 <strong>Monero community suggests to <span class=\"font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500\">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p></div></div></div></section><!-- End Hero --><div class=\"flex flex-col max-w-6xl mx-auto mb-10\"><div class=\"min-w-full inline-block align-middle\">")
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("</div></div>")
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("<div id=\"tbl_nodes\" class=\"bg-neutral-800 border border-neutral-700 rounded-xl shadow-sm overflow-hidden\"><div class=\"px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-neutral-700\">")
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("</div><div class=\"overflow-x-auto\"><table class=\"table-striped table-hover\"><thead><tr><th scope=\"col\">Host:Port</th><th scope=\"col\">Nettype</th><th scope=\"col\">Protocol</th><th scope=\"col\">Country</th><th scope=\"col\">Status</th><th scope=\"col\">Estimate Fee</th><th scope=\"col\">Uptime</th><th scope=\"col\">Check</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, row := range data.Items {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td>")
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("</td><td>")
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("<br>")
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("</td><td>")
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("</td><td>")
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("</td><td>")
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("</td><td>")
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("</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</tbody></table></div><div class=\"px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-neutral-700\">")
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("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -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;
}

View file

@ -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

View file

@ -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
@ -44,45 +45,51 @@ func TestQueryNodes_toSQL(t *testing.T) {
wantArgs []interface{}
wantWhere string
wantSortBy string
wantSortDirection string
wantSortDir string
}{
{
name: "Default query",
query: QueryNodes{
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,
RowsPerPage: 10,
Page: 1,
SortBy: "last_checked",
SortDirection: "desc",
},
wantArgs: []interface{}{},
wantWhere: "",
wantSortBy: "last_checked",
wantSortDirection: "DESC",
wantSortDir: "DESC",
},
{
name: "With host query",
query: QueryNodes{
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,
RowsPerPage: 10,
Page: 1,
SortBy: "last_checked",
SortDirection: "desc",
},
wantArgs: []interface{}{"%test%", "%test%"},
wantWhere: "WHERE (hostname LIKE ? OR ip_addr LIKE ?)",
wantSortBy: "last_checked",
wantSortDirection: "DESC",
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{
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,
RowsPerPage: 10,
Page: 1,
SortBy: "last_checked",
SortDirection: "desc",
}
for i := 0; i < b.N; i++ {
_, _ = q.toSQL()