xmr-remote-nodes/internal/handler/views/remote_nodes.templ
2024-11-04 18:02:24 +07:00

334 lines
11 KiB
Text

package views
import (
"fmt"
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
"github.com/ditatompel/xmr-remote-nodes/internal/monero"
"github.com/ditatompel/xmr-remote-nodes/internal/paging"
"strings"
"time"
)
var nettypes = []string{"mainnet", "stagenet", "testnet"}
var protocols = []string{"tor", "http", "https"}
type nodeStatus struct {
Code int
Text string
}
var nodeStatuses = []nodeStatus{
{-1, "ANY"},
{1, "Online"},
{0, "Offline"},
}
templ RemoteNodes(data monero.Nodes, countries []monero.Countries, q monero.QueryNodes, p paging.Pagination) {
<!-- Hero -->
<section class="relative overflow-hidden pt-6">
@heroGradient()
<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">
@TableNodes(data, countries, q, p)
</div>
</div>
}
templ TableNodes(data monero.Nodes, countries []monero.Countries, 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>
@DtRefreshInterval("/remote-nodes", "#tbl_nodes", q.Refresh, q)
</div>
@DtReload("/remote-nodes", "#tbl_nodes", q)
</div>
<div class="overflow-x-auto">
<table class="dt">
<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>
@DtThSort("/remote-nodes", "#tbl_nodes", "Uptime", "uptime", q.SortBy, q.SortDirection, q)
@DtThSort("/remote-nodes", "#tbl_nodes", "Check", "last_checked", q.SortBy, q.SortDirection, q)
</tr>
<tr>
<td>
<input
type="text"
id="host"
name="host"
value={ fmt.Sprintf("%s", q.Host) }
autocomplete="off"
class="th-filter"
placeholder="Filter Host / IP"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"host"})) }
hx-push-url="true"
hx-trigger="keyup changed delay:0.4s"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
/>
</td>
<td>
<select
id="nettype"
name="nettype"
class="th-filter"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"nettype"})) }
hx-trigger="change"
hx-push-url="true"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
<option value="">ANY</option>
for _, nettype := range nettypes {
<option value={ fmt.Sprintf("%s", nettype) } selected?={ nettype == q.Nettype }>{ nettype }</option>
}
</select>
</td>
<td>
<select
id="protocol"
name="protocol"
class="th-filter"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"protocol"})) }
hx-trigger="change"
hx-push-url="true"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
<option value="">ANY</option>
for _, protocol := range protocols {
<option value={ fmt.Sprintf("%s", protocol) } selected?={ protocol == q.Protocol }>{ protocol }</option>
}
</select>
</td>
<td>
<select
id="cc"
name="cc"
class="th-filter"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"cc"})) }
hx-trigger="change"
hx-push-url="true"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
<option value="any">ANY</option>
for _, country := range countries {
if country.CC == "" {
<option value="UNKNOWN" selected?={ q.CC== "UNKNOWN" }>{ fmt.Sprintf("UNKNOWN (%d)", country.TotalNodes ) }</option>
} else {
<option value={ fmt.Sprintf("%s", country.CC) } selected?={ country.CC == q.CC }>{ fmt.Sprintf("%s (%d)", country.Name, country.TotalNodes ) }</option>
}
}
</select>
</td>
<td colspan="2">
<select
id="status"
name="status"
class="th-filter"
autocomplete="off"
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"status"})) }
hx-trigger="change"
hx-push-url="true"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
>
for _, status := range nodeStatuses {
<option value={ fmt.Sprintf("%d", status.Code) } selected?={ status.Code == q.Status }>{ status.Text }</option>
}
</select>
</td>
<td colspan="2">
<div class="flex justify-center">
<input
type="checkbox"
id="cors"
name="cors"
autocomplete="off"
checked?={ q.CORS == "on" }
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"cors"})) }
hx-trigger="change"
hx-push-url="true"
hx-target="#tbl_nodes"
hx-swap="outerHTML"
class="shrink-0 mt-0.5 text-orange-400 bg-neutral-800 border-neutral-700 rounded focus:ring-0 checked:bg-orange-400 checked:border-orange-400 focus:ring-offset-orange-500"
/>
<label for="cors" class="text-sm ms-3 text-neutral-400">CORS</label>
</div>
</td>
</tr>
</thead>
<tbody>
for _, row := range data.Items {
<tr>
<td>
@cellHostPort(row.ID, row.Port, row.Hostname, row.IPAddresses, row.IsTor, row.IPv6Only)
</td>
<td>
@cellNettype(row.Nettype, row.Height)
</td>
<td>
@cellProtocol(row.Protocol, row.CORSCapable)
</td>
<td>
@cellCountry(row.CountryCode, row.CountryName, row.City, row.ASNName, row.ASN)
</td>
<td>
@cellStatuses(row.IsAvailable, monero.ParseNodeStatuses(row.LastCheckStatus))
</td>
<td>{ fmt.Sprintf("%d", row.EstimateFee) }</td>
<td class="text-right">
@cellUptime(row.Uptime)
</td>
<td title={ time.Unix(row.LastChecked, 0).UTC().Format("Jan 2, 2006 15:04 MST") }>{ timeSince(row.LastChecked) }</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>
}
templ Node(data monero.Node) {
<p>{ fmt.Sprintf("%s:%d", data.Hostname, data.Port) }</p>
}
templ cellHostPort(id, port uint, hostname, ips string, isTor, ipv6Only bool) {
if isTor {
<button
class="max-w-40 truncate text-orange-400"
hx-get={ fmt.Sprintf("/remote-nodes/id/%d", id) }
hx-push-url="false"
hx-target="#modal-section"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="modal-section"
data-hs-overlay="#modal-section"
>
👁 { hostname }
</button>
<br/>
.onion:<span class="text-indigo-400">{ fmt.Sprintf("%d", port) }</span>
<span class="text-neutral-400">(TOR)</span>
} else {
{ ip.FormatHostname(hostname) }:<span class="text-indigo-400">{ fmt.Sprintf("%d", port) }</span>
<br/>
<div class="max-w-40 text-ellipsis overflow-x-auto md:overflow-hidden hover:overflow-visible">
<span class="whitespace-break-spaces text-gray-400">{ strings.ReplaceAll(ips, ",", " ") }</span>
if ipv6Only {
<span class="text-rose-400">(IPv6 only)</span>
}
</div>
}
}
templ cellNettype(nettype string, height uint) {
switch nettype {
case "stagenet":
<span class="font-semibold uppercase text-sky-500">{ nettype }</span>
case "testnet":
<span class="font-semibold uppercase text-rose-500">{ nettype }</span>
default:
<span class="font-semibold uppercase text-green-500">{ nettype }</span>
}
<br/>
{ fmt.Sprintf("%d", height) }
}
templ cellProtocol(protocol string, cors bool) {
switch protocol {
case "http":
<span class="font-semibold uppercase text-sky-500">{ protocol }</span>
default:
<span class="font-semibold uppercase text-green-500">{ protocol }</span>
}
if cors {
<br/>
(CORS 💪)
}
}
templ cellCountry(cc, countryName, city, asnName string, asn uint) {
if cc != "" {
if city != "" {
{ city },
}
{ countryName }
<img class="inline-block" src={ fmt.Sprintf("/assets/img/cf/%s.svg", strings.ToLower(cc)) } alt={ fmt.Sprintf("%s Flag", cc) } width="22px"/>
}
if asn != 0 {
<br/>
<a
class="external font-semibold underline !text-purple-400"
href={ templ.URL(fmt.Sprintf("https://www.ditatompel.com/asn/%d", asn)) }
target="_blank"
rel="noopener"
>{ fmt.Sprintf("AS%d", asn) }</a>
(<span class="font-semibold text-green-500">{ asnName }</span>)
}
}
templ cellStatuses(isAvailable bool, statuses [5]int) {
if isAvailable {
<span class="font-semibold text-green-500">Online</span>
} else {
<span class="text-rose-400">Offline</span>
}
<br/>
for _, status := range statuses {
if status == 1 {
<span class="text-green-400 mr-1">•</span>
} else if status == 0 {
<span class="text-red-400 mr-1">•</span>
} else {
<span class="text-neutral-600 mr-1">•</span>
}
}
}
templ cellUptime(uptime float64) {
if uptime >= 98 {
<span class="text-green-500">{ formatFloat(uptime) }%</span>
} else if uptime < 98 && uptime >= 80 {
<span class="text-sky-500">{ formatFloat(uptime) }%</span>
} else if uptime < 80 && uptime > 75 {
<span class="text-orange-500">{ formatFloat(uptime) }%</span>
} else {
<span class="text-rose-500">{ formatFloat(uptime) }%</span>
}
}