Adding my old frontpage UI to this project

This commit is contained in:
ditatompel 2024-05-04 15:32:42 +07:00
parent 6caec5718d
commit 92acb52aac
No known key found for this signature in database
GPG key ID: 31D3D06D77950979
25 changed files with 1078 additions and 7 deletions

View file

@ -0,0 +1,43 @@
<script>
import { IcnGitHub } from '$lib/components/svg';
</script>
<div class="flex w-full items-end border-t border-surface-500/10 bg-surface-50 dark:bg-surface-900">
<footer class="w-full">
<div
class="md:flex-no-wrap container mx-auto flex max-w-screen-xl flex-col flex-wrap px-5 py-10 md:flex-row md:items-center lg:items-start"
>
<div class="mx-auto w-64 flex-shrink-0 text-center lg:text-left">
<a
href="/"
class="title-font flex items-center justify-center font-medium md:justify-start"
>
<span class="txt-logo-gradient text-5xl font-bold">XMR Nodes</span>
</a>
<p class="mt-2">
By <a href="https://www.ditatompel.com">ditatompel</a>,
<a href="https://github.com/ditatompel/xmr-nodes" target="_blank" rel="noopener"
>source code</a
>.
</p>
<div class="mt-4">
<span class="mt-2 inline-flex justify-center sm:ml-auto sm:mt-0 sm:justify-start">
<a
href="https://github.com/ditatompel/xmr-nodes"
target="_blank"
class="cursor-pointer text-gray-500 hover:text-gray-700"
aria-label="Link to ditatompel's GitHub profile"
>
<IcnGitHub class="h-5 w-5" />
</a>
</span>
</div>
</div>
</div>
<div class="bg-surface-500/5">
<div class="container mx-auto px-5 py-4">
<p class="text-center text-sm">XMR Nodes</p>
</div>
</div>
</footer>
</div>

View file

@ -0,0 +1,72 @@
<script>
import { page } from '$app/stores';
import { LightSwitch, getDrawerStore } from '@skeletonlabs/skeleton';
const drawerStore = getDrawerStore();
function drawerOpen() {
drawerStore.open({});
}
</script>
<nav class="fixed w-full z-20 top-0 start-0 bg-surface-100-800-token shadow-2xl">
<div class="mx-auto flex max-w-screen-xl flex-wrap items-center justify-between px-4 py-1">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse" aria-label="ditatompel">
<span
class="txt-logo-gradient hidden self-center whitespace-nowrap text-2xl font-semibold lg:block"
>XMR Nodes</span
>
</a>
<div class="flex items-center space-x-1 md:order-2 md:space-x-0 rtl:space-x-reverse">
<LightSwitch />
<button
class="btn btn-sm mr-4 md:hidden"
aria-label="Mobile Drawer Button"
on:click={drawerOpen}
>
<span>
<svg viewBox="0 0 100 80" class="fill-token h-4 w-4">
<rect width="100" height="20" />
<rect y="30" width="100" height="20" />
<rect y="60" width="100" height="20" />
</svg>
</span>
</button>
</div>
<div class="hidden w-full items-center justify-between md:order-1 md:flex md:w-auto">
<ul
class="flex flex-row space-x-1 rounded-lg bg-white p-0 dark:bg-gray-900 rtl:space-x-reverse"
>
<li>
<a
href="/"
class={$page.url.pathname === '/' ? 'active' : 'nav-link'}
aria-current={$page.url.pathname === '/' ? 'page' : undefined}>Home</a
>
</li>
<li>
<a
href="/remote-nodes"
class={$page.url.pathname.startsWith('/remote-nodes') ? 'active' : 'nav-link'}
>Remote Nodes</a
>
</li>
<li>
<a
href="/add-node"
class={$page.url.pathname.startsWith('/add-node') ? 'active' : 'nav-link'}>Add Node</a
>
</li>
</ul>
</div>
</div>
</nav>
<style lang="postcss">
.active {
@apply block rounded bg-primary-500 p-2 text-black;
}
.nav-link {
@apply block rounded hover:bg-secondary-500 md:p-2 hover:text-black;
}
</style>

View file

@ -0,0 +1,28 @@
<script>
import { page } from '$app/stores';
import { getDrawerStore } from '@skeletonlabs/skeleton';
const drawerStore = getDrawerStore();
function drawerClose() {
drawerStore.close();
}
$: classesActive = (/** @type {string} */ href) =>
$page.url.pathname.startsWith(href) ? 'bg-primary-500' : '';
</script>
<nav class="list-nav p-4">
<ul>
<li>
<a href="/" class={$page.url.pathname === '/' ? 'bg-primary-500' : ''} on:click={drawerClose}
>Home</a
>
</li>
<li><a href="/dib" class={classesActive('/dib')} on:click={drawerClose}>Data</a></li>
<li><a href="/tools" class={classesActive('/tools')} on:click={drawerClose}>Tools</a></li>
<li><a href="/asn" class={classesActive('/asn')} on:click={drawerClose}>ASN</a></li>
<li><a href="/proxy" class={classesActive('/proxy')} on:click={drawerClose}>Proxy</a></li>
<li><a href="/monero" class={classesActive('/monero')} on:click={drawerClose}>Monero</a></li>
</ul>
</nav>

View file

@ -1,3 +1,5 @@
export { default as MainNav } from './MainNav.svelte';
export { default as MobileDrawer } from './MobileDrawer.svelte';
export { default as AdminNav } from './AdminNav.svelte';
export { default as AdminSidebar } from './AdminSidebar.svelte';
export { default as AdminMobileDrawer } from './AdminMobileDrawer.svelte';

View file

@ -0,0 +1,17 @@
export const getDistinct = (items) => {
return Array.from(getCounter(items).keys());
};
export const getDuplicates = (items) => {
return Array.from(getCounter(items).entries())
.filter(([, count]) => count !== 1)
.map(([key]) => key);
};
export const getCounter = (items) => {
const result = new Map();
items.forEach((item) => {
result.set(item, (result.get(item) ?? 0) + 1);
});
return result;
};

View file

@ -0,0 +1,210 @@
<script>
// import { writable } from 'svelte/store';
import { page } from '$app/stores';
import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
import { beforeNavigate, afterNavigate } from '$app/navigation';
import '../../app.css';
import {
Toast,
Modal,
Drawer,
initializeStores,
getToastStore,
storePopup // PopUps
} from '@skeletonlabs/skeleton';
import { dev, browser } from '$app/environment';
import { MainNav, MobileDrawer } from '$lib/components/navigation';
import Footer from '$lib/components/Footer.svelte';
initializeStores();
const toastStore = getToastStore();
// popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
/* prettier-ignore */
const metaDefaults = {
title: 'Monero is private, decentralized cryptocurrency that keeps your finances confidential and secure.',
description: '',
keywords: '',
image:
'https://vcl-og-img.ditatompel.com/' +
encodeURIComponent('Monero is private, decentralized cryptocurrency that keeps your finances confidential and secure.') +
'.png?md=0'
};
const meta = {
title: metaDefaults.title,
description: metaDefaults.description,
keywords: metaDefaults.keywords,
image: metaDefaults.image,
article: { publishTime: '', modifiedTime: '', author: '' },
// Twitter
twitter: {
title: metaDefaults.title,
description: metaDefaults.description,
image: metaDefaults.image
}
};
page.subscribe((page) => {
// Restore Page Defaults
meta.title = metaDefaults.title;
meta.description = metaDefaults.description;
meta.keywords = metaDefaults.keywords;
meta.image = metaDefaults.image;
// Restore Twitter Defaults
meta.twitter.title = metaDefaults.title;
meta.twitter.description = metaDefaults.description;
meta.twitter.image = metaDefaults.image;
if (typeof page.data.meta === 'object') {
meta.title = page.data.meta.title ?? metaDefaults.title;
meta.description = page.data.meta.description ?? metaDefaults.description;
meta.keywords = page.data.meta.keywords ?? metaDefaults.description;
meta.image = page.data.meta.image ?? metaDefaults.image;
// Restore Twitter Defaults
meta.twitter.title = page.data.meta.title ?? metaDefaults.title;
meta.twitter.description = page.data.meta.description ?? metaDefaults.description;
meta.twitter.image = page.data.meta.image ?? metaDefaults.description;
if (typeof page.data.meta.article === 'object') {
meta.article.author = page.data.meta.article.author ?? '';
}
// if (!dev) {
// promotionEnabled.set(page.data.promotionEnabled ?? false);
// }
}
});
let isLoading = false;
// progress bar show
beforeNavigate(() => (isLoading = true));
// scroll to top after nafigation and progress bar
afterNavigate((/* params */) => {
isLoading = false;
// scroll to top when navigate
// const isNewPage = params.from?.url.pathname !== params.to?.url.pathname;
// const elemPage = document.querySelector('#page');
// if (isNewPage && elemPage !== null) {
// elemPage.scrollTop = 0;
// }
});
if (browser) {
/* Service Worker */
/** @type {any} */
let newWorker;
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js', {
type: dev ? 'module' : 'classic'
})
.then((reg) => {
reg.addEventListener('updatefound', () => {
console.log('SW Update found');
// An updated service worker has appeared in reg.installing!
newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
// Has service worker state changed?
switch (newWorker.state) {
case 'installed':
// There is a new service worker available, show the notification
if (navigator.serviceWorker.controller) {
const notifUpdateSw = {
message: 'New version avaiable for this site is available.',
autohide: false,
action: {
label: 'Reload',
response: () => window.location.reload()
}
};
toastStore.trigger(notifUpdateSw);
// localStorage.clear();
// sessionStorage.clear();
newWorker.postMessage({ action: 'skipWaiting' });
}
break;
}
});
});
})
.catch((err) => {
console.log('error with service worker', err);
});
/** @type {any} */
let refreshing;
// The event listener that is fired when the service worker updates
// Here we reload the page
navigator.serviceWorker.addEventListener('controllerchange', function () {
if (refreshing) {
// console.log('refreshing');
return;
}
// window.location.reload();
refreshing = true;
});
}
}
</script>
<svelte:head>
<title>{meta.title} — xmr.ditatompel.com</title>
<!-- Meta Tags -->
<meta name="title" content="{meta.title} — ditatompel.com" />
<meta name="description" content={meta.description} />
<meta name="keywords" content={meta.keywords} />
<meta name="theme-color" content="#272b31" />
<meta name="author" content="ditatompel" />
<!-- Open Graph - https://ogp.me/ -->
<meta property="og:site_name" content="ditatompel.com" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.ditatompel.com{$page.url.pathname}" />
<meta property="og:locale" content="en_US" />
<meta property="og:title" content="{meta.title} — ditatompel.com" />
<meta property="og:description" content={meta.description} />
<meta property="og:image" content={meta.image} />
<meta property="og:image:secure_url" content={meta.image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="2048" />
<meta property="og:image:height" content="1170" />
<!-- Open Graph: Twitter -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@ditatompel" />
<meta name="twitter:creator" content="@ditatompel" />
<meta name="twitter:title" content="{meta.twitter.title} — ditatompel.com" />
<meta name="twitter:description" content={meta.twitter.description} />
<meta name="twitter:image" content={meta.twitter.image} />
</svelte:head>
<Modal />
<Toast />
<Drawer>
<h2 class="p-4">Navigation</h2>
<hr />
<MobileDrawer />
<hr />
</Drawer>
<!-- <AppShell slotSidebarLeft="bg-surface-500/5 w-0 lg:w-64"> -->
<!-- <svelte:fragment slot="header"> -->
<!-- <MainNav /> -->
<!-- </svelte:fragment> -->
<!-- <slot /> -->
<!-- <svelte:fragment slot="pageFooter"> -->
<!-- <Footer /> -->
<!-- </svelte:fragment> -->
<!-- </AppShell> -->
<MainNav />
<div class="pt-10 md:pt-12">
<slot />
</div>
<Footer />

View file

@ -147,7 +147,7 @@
<div class="section-container text-token grid grid-cols-1 gap-2 md:grid-cols-3">
<a
class="card {currentVariant} card-hover overflow-hidden py-2 text-center"
href="?remote-node"
href="/remote-nodes"
>
<h2 class="h2 font-bold">Remote Nodes</h2>
<div class="space-y-4 p-4">
@ -159,7 +159,7 @@
</div>
</a>
<a class="card {currentVariant} card-hover overflow-hidden py-2 text-center" href="?add-node">
<a class="card {currentVariant} card-hover overflow-hidden py-2 text-center" href="/add-node">
<h2 class="h2 font-bold">Add Node</h2>
<div class="space-y-4 p-4">
<p>

View file

@ -0,0 +1,27 @@
/** @type {import('./$types').PageLoad} */
export async function load({ data }) {
/* prettier-ignore */
const metaDefaults = {
title: 'Add Monero Node',
description: 'You can use this page to add known remote node to the system so my bots can monitor it.',
keywords: 'monero, monero node, monero public node, monero wallet'
};
return {
meta: {
title: metaDefaults.title,
description: metaDefaults.description,
keywords: metaDefaults.keywords,
image:
'https://vcl-og-img.ditatompel.com/' + encodeURIComponent(metaDefaults.title) + '.png?md=0',
// Article
article: { publishTime: '', modifiedTime: '', author: '' },
// Twitter
twitter: {
title: metaDefaults.title,
description: metaDefaults.description,
image: metaDefaults.image
}
}
};
}

View file

@ -0,0 +1,115 @@
<script>
import { applyAction, enhance } from '$app/forms';
import { slide } from 'svelte/transition';
import { ProgressBar } from '@skeletonlabs/skeleton';
/** @type {import('./$types').PageData} */
export let data;
/** @type {import('./$types').ActionData} */
export let form;
let isProcessing = false;
/** @type {import('./$types').SubmitFunction} */
const handleForm = async () => {
isProcessing = true;
return async ({ result }) => {
isProcessing = false;
if (result.type === 'success' || result.type === 'redirect') {
close();
}
await applyAction(result);
};
};
</script>
<header id="hero" class="hero-gradient py-7">
<div class="section-container text-center">
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
<p>{data.meta.description}</p>
</div>
<div class="mx-auto w-full max-w-3xl px-20">
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
</div>
</header>
<section id="form-add-monero-node">
<div class="section-container text-center">
<p>Enter your Monero node information below (IPv4 host only):</p>
<form class="mx-auto w-full max-w-3xl py-2" method="POST" use:enhance={handleForm}>
<div class="grid grid-cols-1 gap-4 py-6 md:grid-cols-4">
<label class="label">
<span>Protocol *</span>
<select name="protocol" class="select variant-form-material" disabled={isProcessing}>
<option value="http">HTTP / TOR</option>
<option value="https">HTTPS</option>
</select>
</label>
<label class="label md:col-span-2">
<span>Host / IP *</span>
<input
class="input variant-form-material"
name="host"
type="text"
placeholder="Eg: node.example.com or 172.16.17.18"
disabled={isProcessing}
/>
</label>
<label class="label">
<span>Port *</span>
<input
class="input variant-form-material"
name="port"
type="number"
placeholder="Eg: 18081"
disabled={isProcessing}
/>
</label>
</div>
<button class="variant-filled-success btn" disabled={isProcessing}
>{isProcessing ? 'Processing...' : 'Submit'}</button
>
</form>
<div class="mx-auto w-full max-w-3xl py-2">
{#if !isProcessing}
{#if form?.status === 'error'}
<div class="alert variant-ghost-error" transition:slide={{ duration: 500 }}>
<div class="alert-message">
<h3 class="h3">Error!</h3>
<p>{form.message}!</p>
</div>
</div>
{/if}
{#if form?.status === 'ok'}
<div class="alert variant-ghost-success" transition:slide={{ duration: 500 }}>
<div class="alert-message">
<h3 class="h3">Success!</h3>
<p>{form.message}!</p>
</div>
</div>
{/if}
{:else}
<ProgressBar meter="bg-secondary-500" track="bg-secondary-500/30" value={undefined} />
{/if}
</div>
<p>
Here you can find list of <a class="anchor" href="/remote-nodes">Monero Remote Node</a>.
</p>
</div>
</section>
<style lang="postcss">
.section-container {
@apply mx-auto w-full max-w-7xl p-4;
}
/* Hero Gradient */
/* prettier-ignore */
.hero-gradient {
background-image:
radial-gradient(at 0% 0%, rgba(242, 104, 34, .4) 0px, transparent 50%),
radial-gradient(at 98% 1%, rgba(var(--color-warning-900) / 0.33) 0px, transparent 50%);
}
</style>

View file

@ -0,0 +1,27 @@
/** @type {import('./$types').PageLoad} */
export async function load({ data }) {
/* prettier-ignore */
const metaDefaults = {
title: 'Monero (XMR)',
description: 'Monero is private, decentralized cryptocurrency that keeps your finances confidential and secure.',
keywords: 'monero,xmr,monero node,xmrnode,cryptocurrency'
};
return {
meta: {
title: metaDefaults.title,
description: metaDefaults.description,
keywords: metaDefaults.keywords,
image:
'https://vcl-og-img.ditatompel.com/' + encodeURIComponent(metaDefaults.title) + '.png?md=0',
// Article
article: { publishTime: '', modifiedTime: '', author: '' },
// Twitter
twitter: {
title: metaDefaults.title,
description: metaDefaults.description,
image: metaDefaults.image
}
}
};
}

View file

@ -0,0 +1,275 @@
<script>
export let data;
</script>
<header id="hero" class="hero-gradient py-7">
<div class="section-container text-center">
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
<p class="mx-auto max-w-3xl">
<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>
<div class="mx-auto w-full max-w-3xl px-20">
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
</div>
</header>
<section id="introduction ">
<div class="section-container text-center !max-w-4xl">
<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 always run your own node</strong> to
obtain the maximum possible privacy and to help decentralize the network.
</p>
</div>
</section>
<section id="monero-remote-node">
<div class="section-container">
<div class="space-y-2 overflow-x-auto">
<div class="flex justify-between">
<!-- <DtSrRowsPerPage {handler} /> -->
</div>
<!--
<table class="table table-hover table-compact w-full table-auto">
<thead>
<tr>
<th>Host:Port</th>
<th><label for="fNettype">Nettype</label></th>
<th><label for="fProtocol">Protocol</label></th>
<th><label for="fCc">Country</label></th>
<th><label for="fStatus">Status</label></th>
<th>Est. Fee</th>
<DtSrThSort {handler} orderBy="uptime">Uptime</DtSrThSort>
<DtSrThSort {handler} orderBy="last_checked">Check</DtSrThSort>
</tr>
<tr>
<DtSrThFilter {handler} filterBy="host" placeholder="Filter Host / IP" />
<th>
<select
id="fNettype"
name="fNettype"
class="select variant-form-material"
bind:value={filterNettype}
on:change={() => {
handler.filter(filterNettype, 'nettype');
handler.invalidate();
}}
>
<option value="any">Any</option>
<option value="mainnet">MAINNET</option>
<option value="stagenet">STAGENET</option>
<option value="testnet">TESTNET</option>
</select>
</th>
<th>
<select
id="fProtocol"
name="fProtocol"
class="select variant-form-material"
bind:value={filterProtocol}
on:change={() => {
handler.filter(filterProtocol, 'protocol');
handler.invalidate();
}}
>
<option value="any">Any</option>
<option value="tor">TOR</option>
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
</select>
</th>
<th>
<select
id="fCc"
name="fCc"
class="select variant-form-material"
bind:value={filterCc}
on:change={() => {
handler.filter(filterCc, 'country');
handler.invalidate();
}}
>
<option value="any">Any</option>
{#each data.countries as country}
{#if country.cc === ''}
<option value="UNKNOWN">UNKNOWN ({country.total_nodes})</option>
{:else}
<option value={country.cc}
>{country.name === '' ? country.cc : country.name} ({country.total_nodes})</option
>
{/if}
{/each}
</select>
</th>
<th colspan="2">
<select
id="fStatus"
name="fStatus"
class="select variant-form-material"
bind:value={filterStatus}
on:change={() => {
handler.filter(filterStatus, 'status');
handler.invalidate();
}}
>
<option value={-1}>Any</option>
<option value="0">Offline</option>
<option value="1">Online</option>
</select>
</th>
<th colspan="2">
<label for="fCors" class="flex items-center justify-center space-x-2">
<input
id="fCors"
class="checkbox"
type="checkbox"
name="fCors"
bind:checked={checkboxCors}
on:change={() => {
handler.filter(checkboxCors === true ? 1 : -1, 'cors');
handler.invalidate();
}}
/>
<p>CORS</p>
</label>
</th>
</tr>
</thead>
<tbody>
{#each $rows as row}
<tr>
<td
><HostPortCell
ip={row.ip}
is_tor={row.is_tor}
hostname={row.hostname}
port={row.port}
/></td
>
<td><NetTypeCell nettype={row.nettype} height={row.last_height} /></td>
<td><ProtocolCell protocol={row.protocol} cors={row.cors} /></td>
<td
><CountryCellWithAsn
cc={row.cc}
country_name={row.country_name}
city={row.city}
asn={row.asn}
asn_name={row.asn_name}
/></td
>
<td
><StatusCell
is_available={row.is_available}
statuses={row.last_check_statuses}
/></td
>
<td
><EstimateFeeCell
estimate_fee={row.estimate_fee}
majority_fee={netFees[row.nettype]}
/></td
>
<td><UptimeCell uptime={row.uptime} /></td>
<td>{format(row.last_checked * 1000, 'PP HH:mm')}</td>
</tr>
{/each}
</tbody>
</table>
<div class="flex justify-between">
<DtSrRowCount {handler} />
<DtSrPagination {handler} />
</div>
-->
</div>
</div>
</section>
<section id="page-info" class="mx-auto w-full max-w-4xl px-4 pb-7">
<div class="alert card shadow-xl">
<div class="alert-message">
<h2 class="h3">Info</h2>
<ul class="list-inside list-disc">
<li>
Uptime percentage calculated is the <strong>last 1 month</strong> uptime.
</li>
<li>
<strong>Est. Fee</strong> here is just fee estimation / byte from
<code class="code text-rose-900 font-bold">get_fee_estimate</code> RPC call method.
</li>
<li>
Malicious actors who running remote nodes <a
class="external"
href="https://w3-s3-jkt1.ditatompel.com/pub/assets/img/site-contents/monero-tx-fee-node.jpg"
target="_blank"
rel="noopener">still can return high fee only if you about to create a transactions</a
>.
</li>
<li><strong>The best and safest way is running your own node</strong>!</li>
<li>
Nodes with 0% uptime within 1 month with more than 300 check attempt will be removed. You
can always add your node again latter.
</li>
<li>
You can filter remote node by selecting on <strong>nettype</strong>,
<strong>protocol</strong>, <strong>country</strong>,
<strong>tor</strong>, and <strong>online status</strong> option.
</li>
<li>
If you know one or more remote node that we don't currently monitor, please add them using <a
href="/add-node">this form</a
>.
</li>
<li>
I deliberately cut the long Tor addresses, click the <span
class="text-orange-800 dark:text-orange-300">👁 torhostname...</span
> to see the full Tor address.
</li>
<li>
You can found larger remote nodes database from <a
class="external"
href="https://monero.fail/"
role="button"
target="_blank"
rel="noopener">monero.fail</a
>.
</li>
<li>
If you are developer or power user who like to fetch Monero remote node above in JSON
format, you can read <a
class="external"
href="https://insights.ditatompel.com/en/blog/2022/01/public-api-monero-remote-node-list/"
>Public API Monero Remote Node List</a
> blog post for more detailed information.
</li>
</ul>
</div>
</div>
</section>
<style lang="postcss">
.section-container {
@apply mx-auto w-full max-w-7xl p-4;
}
/* Hero Gradient */
/* prettier-ignore */
.hero-gradient {
background-image:
radial-gradient(at 0% 0%, rgba(242, 104, 34, .4) 0px, transparent 50%),
radial-gradient(at 98% 1%, rgba(var(--color-warning-900) / 0.33) 0px, transparent 50%);
}
/*
td:nth-child(1) {
@apply max-w-20;
}
*/
</style>

View file

@ -0,0 +1,22 @@
import { PUBLIC_API_ENDPOINT } from '$env/static/public';
/** @param {import('@vincjo/datatables/remote/state')} state */
export async function loadApiData(state) {
const response = await fetch(`${PUBLIC_API_ENDPOINT}/monero/remote-node-dt?${getParams(state)}`);
const json = await response.json();
state.setTotalRows(json.data.total ?? 0);
return json.data.nodes ?? [];
}
const getParams = ({ pageNumber, offset, rowsPerPage, sort, filters }) => {
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
if (sort) {
params += `&sort=${sort.orderBy}&dir=${sort.direction}`;
}
if (filters) {
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
}
return params;
};

View file

@ -0,0 +1,38 @@
<script lang="ts">
export let cc: string;
export let country_name: string;
export let city: string;
export let asn: number;
export let asn_name: string;
$: lowerCc = cc.toLowerCase();
</script>
{#if cc != ''}
{#if city !== ''}
{city},
{/if}
{country_name}
<img
class="inline-block"
src="https://edge.ditatompel.com/assets/img/cf/svg/{lowerCc}.svg"
alt="{cc} Flag"
width="22px"
/>
{/if}
{#if asn !== 0}
<br /><a class="asn" href="/asn/{asn}">AS{asn}</a> (<span class="asn-name">{asn_name}</span>)
{/if}
<style lang="postcss">
a {
@apply font-semibold text-sky-800 underline dark:text-sky-500;
}
a.asn {
@apply !text-purple-800 dark:!text-purple-400;
}
span.asn-name {
@apply font-semibold text-green-800 dark:text-green-500;
}
</style>

View file

@ -0,0 +1,10 @@
<script lang="ts">
export let estimate_fee: number;
export let majority_fee: number;
</script>
{#if estimate_fee !== majority_fee}
<span class="text-orange-800 dark:text-orange-300">{estimate_fee}<br />(CAUTION!)</span>
{:else}
{estimate_fee}
{/if}

View file

@ -0,0 +1,47 @@
<script>
import { getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
/** @type {string} */
export let ip;
/** @type {boolean} */
export let is_tor;
/** @type {string} */
export let hostname;
/** @type {number} */
export let port;
// if (is_tor) {
// hostname = hostname.substring(0, 8) + '[...].onion';
// }
/**
* @param {string} onionAddr
* @param {number} port
*/
function modalAlert(onionAddr, port) {
/** @typedef {import('@skeletonlabs/skeleton').ModalSettings} ModalSettings */
/** @type {ModalSettings} */
const modal = {
type: 'alert',
title: 'Hostname:',
body: '<code class="code">' + onionAddr + ':' + port + '</code>'
};
modalStore.trigger(modal);
}
</script>
{#if is_tor}
<button
class="max-w-32 truncate text-orange-800 dark:text-orange-300"
on:click={() => modalAlert(hostname, port)}
>
👁 {hostname}
</button><br />.onion:<span class="text-indigo-800 dark:text-indigo-400">{port}</span>
<span class="text-gray-700 dark:text-gray-400">(TOR)</span>
{:else}
{hostname}:<span class="text-indigo-800 dark:text-indigo-400">{port}</span>
{#if ip !== ''}
<br /><span class="text-gray-700 dark:text-gray-400">{ip}</span>
{/if}
{/if}

View file

@ -0,0 +1,15 @@
<script>
/** @type {string} */
export let nettype;
/** @type {number} */
export let height;
</script>
{#if nettype === 'stagenet'}
<span class="font-semibold uppercase text-sky-800 dark:text-sky-500">{nettype}</span>
{:else if nettype === 'testnet'}
<span class="font-semibold uppercase text-rose-800 dark:text-rose-400">{nettype}</span>
{:else}
<span class="font-semibold uppercase text-green-800 dark:text-green-500">{nettype}</span>
{/if}
<br />{height}

View file

@ -0,0 +1,16 @@
<script>
/** @type {string} */
export let protocol;
/** @type {boolean} */
export let cors;
</script>
{#if protocol === 'http'}
<span class="font-semibold uppercase text-sky-800 dark:text-sky-500">{protocol}</span>
{:else}
<span class="font-semibold uppercase text-green-800 dark:text-green-500">{protocol}</span>
{/if}
{#if cors}
<br />(CORS 💪)
{/if}

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { getDistinct } from '$lib/utils/arrays';
export let filterValue; //: Writable<string>;
export let preFilteredValues; //: Readable<unknown[]>;
$: uniqueValues = getDistinct($preFilteredValues);
</script>
<div class="pt-2">
<select name="filterAnonymity" class="select" bind:value={$filterValue} on:click|stopPropagation>
<option value={undefined}>All</option>
{#each uniqueValues as value}
{#if value === true}
<option {value}>TOR</option>
{:else}
<option {value}>CLEARNET</option>
{/if}
{/each}
</select>
</div>

View file

@ -0,0 +1,22 @@
<script>
import { getDistinct } from '$lib/utils/arrays';
/** @type {string} */
export let filterName;
export let filterValue; //: Writable<string>;
export let preFilteredValues; //: Readable<unknown[]>;
$: uniqueValues = getDistinct($preFilteredValues);
</script>
<div class="pt-2">
<select name={filterName} class="select" bind:value={$filterValue} on:click|stopPropagation>
<option value={undefined}>All</option>
{#each uniqueValues as value}
{#if value === ''}
<option {value}>UNKNOWN</option>
{:else}
<option {value}>{value.toUpperCase()}</option>
{/if}
{/each}
</select>
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { getDistinct } from '$lib/utils/arrays';
export let filterValue; //: Writable<string>;
export let preFilteredValues; //: Readable<unknown[]>;
$: uniqueValues = getDistinct($preFilteredValues);
</script>
<div class="pt-2">
<select name="filterStatus" class="select" bind:value={$filterValue} on:click|stopPropagation>
<option value={undefined}>All</option>
{#each uniqueValues as value}
{#if value === true}
<option {value}>ONLINE</option>
{:else}
<option {value}>OFFLINE</option>
{/if}
{/each}
</select>
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
export let is_available: boolean;
export let statuses: number[];
</script>
{#if is_available}
<span class="font-semibold text-green-800 dark:text-green-500">Online</span>
{:else}
<span class="text-rose-800 dark:text-rose-400">Offline</span>
{/if}
<br />
{#each statuses as status}
{#if status === 1}
<span class="text-success-700 dark:text-success-400 mr-1"></span>
{:else if status === 0}
<span class="text-error-700 dark:text-error-400 mr-1"></span>
{:else}
<span class="text-surface-400 dark:text-surface-600 mr-1"></span>
{/if}
{/each}

View file

@ -0,0 +1,13 @@
<script lang="ts">
export let uptime: number;
</script>
{#if uptime > 98}
<span class="text-green-800 dark:text-green-500">{uptime}%</span>
{:else if uptime < 98 && uptime > 80}
<span class="text-sky-800 dark:text-sky-500">{uptime}%</span>
{:else if uptime < 80 && uptime > 75}
<span class="text-orange-800 dark:text-orange-300">{uptime}%</span>
{:else}
<span class="text-rose-800 dark:text-rose-400">{uptime}%</span>
{/if}

View file

@ -0,0 +1,10 @@
export { default as CountryCellWithAsn } from './CountryCellWithAsn.svelte';
export { default as EstimateFeeCell } from './EstimateFeeCell.svelte';
export { default as HostPortCell } from './HostPortCell.svelte';
export { default as NetTypeCell } from './NetTypeCell.svelte';
export { default as ProtocolCell } from './ProtocolCell.svelte';
export { default as SelectAnonymityFilter } from './SelectAnonymityFilter.svelte';
export { default as SelectFilter } from './SelectFilter.svelte';
export { default as SelectStatusFilter } from './SelectStatusFilter.svelte';
export { default as StatusCell } from './StatusCell.svelte';
export { default as UptimeCell } from './UptimeCell.svelte';

View file

@ -1,20 +1,20 @@
<script>
// import { base } from '$app/paths';
import '../app.css';
import { beforeNavigate, afterNavigate } from '$app/navigation';
import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom'
import {
import { beforeNavigate, afterNavigate } from '$app/navigation';
import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
import {
ProgressBar,
initializeStores,
storePopup // PopUps
} from '@skeletonlabs/skeleton';
initializeStores();
initializeStores();
// popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
let isLoading = false;
let isLoading = false;
// progress bar show
beforeNavigate(() => (isLoading = true));
@ -23,6 +23,7 @@
isLoading = false;
});
</script>
{#if isLoading}
<ProgressBar
class="fixed top-0 z-50"
@ -32,3 +33,4 @@
/>
{/if}
<slot />