dskvr
2 years ago
32 changed files with 897 additions and 1147 deletions
@ -1,104 +0,0 @@ |
|||
<template> |
|||
<Disclosure as="nav" class="bg-white mb-5" v-slot="{ open }"> |
|||
<div class="mx-auto max-w-7xl px-0"> |
|||
<div class="flex h-12 justify-between"> |
|||
<div class="flex px-2 lg:px-0"> |
|||
<div class="hidden lg:flex lg:space-x-2"> |
|||
<a v-for="item in store.layout.getNavGroup(this.navSlug)" |
|||
:key="`subnav-${item.slug}`" |
|||
:href="item.href" |
|||
@click="setActiveContent(item.slug)" |
|||
:class="[isActive(item) ? 'bg-slate-500 text-white' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900', 'group flex items-center px-3 py-2 text-sm font-medium']" |
|||
class="inline-flex items-center pt-1.5 text-sm font-medium"> |
|||
{{ item.name }} |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end"> |
|||
<ToolFilter /> |
|||
</div> |
|||
<div class="flex items-center lg:hidden"> |
|||
<!-- Mobile menu button --> |
|||
<DisclosureButton class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"> |
|||
<span class="sr-only">Open main menu</span> |
|||
<Bars3Icon v-if="!open" class="block h-6 w-6" aria-hidden="true" /> |
|||
<XMarkIcon v-else class="block h-6 w-6" aria-hidden="true" /> |
|||
</DisclosureButton> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<DisclosurePanel class="lg:hidden"> |
|||
<div class="space-y-1 pt-2 pb-3"> |
|||
<!-- Current: "bg-indigo-50 border-indigo-500 text-indigo-700", Default: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800" --> |
|||
<DisclosureButton |
|||
v-for="item in store.layout.getNavGroup(this.navSlug)" |
|||
:key="`subnav-${item.slug}`" |
|||
@click="setActiveContent(item.slug)" |
|||
:class="[isActive(item) ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900', 'group flex items-center px-3 py-2 text-sm font-medium']" |
|||
as="a" |
|||
:href="item.href" |
|||
class="block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700"> |
|||
{{ item.name }} |
|||
</DisclosureButton> |
|||
</div> |
|||
</DisclosurePanel> |
|||
</Disclosure> |
|||
</template> |
|||
|
|||
<script> |
|||
//vue |
|||
import { defineComponent } from 'vue' |
|||
//pinia |
|||
import { setupStore } from '@/store' |
|||
//components |
|||
import ToolFilter from '@/components/relays/ToolFilter.vue' |
|||
//nav conf |
|||
import { items } from './config/find-pagenav.yaml' |
|||
//shared methods |
|||
import RelaysLib from '@/shared/relays-lib.js' |
|||
//hash router |
|||
import { setupNavData, mountNav, setActiveContent, loadNavContent, routeValid, parseHash, contentIsActive } from '@/shared/hash-router.js' |
|||
//tailwind |
|||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' |
|||
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/outline' |
|||
|
|||
export default defineComponent({ |
|||
title: "nostr.watch registry & network status", |
|||
name: 'FindRelaysSubnav', |
|||
components: { |
|||
ToolFilter, |
|||
Disclosure, DisclosureButton, DisclosurePanel, |
|||
Bars3Icon, XMarkIcon, |
|||
// PreferencesComponent, |
|||
// AuthComponent |
|||
}, |
|||
props: {}, |
|||
data(){ |
|||
return setupNavData('relays/find') |
|||
}, |
|||
setup(){ |
|||
return { |
|||
store : setupStore() |
|||
} |
|||
}, |
|||
updated(){ |
|||
|
|||
}, |
|||
created(){ |
|||
this.mountNav('subsection', items) |
|||
}, |
|||
mounted(){ |
|||
|
|||
}, |
|||
methods: Object.assign(RelaysLib, { mountNav, setActiveContent, loadNavContent}), |
|||
|
|||
computed: { |
|||
isActive(){ |
|||
return (item) => item.slug==this.navActiveContent |
|||
}, |
|||
contentIsActive, |
|||
routeValid, |
|||
parseHash |
|||
}, |
|||
}); |
|||
</script> |
@ -1,23 +0,0 @@ |
|||
<script> |
|||
console.log(window.nostr) |
|||
// func make_like_event(pubkey, privkey: String, liked: NostrEvent) => { |
|||
// { |
|||
// content: "+", |
|||
// pubkey: pubkey, |
|||
// kind: 7, |
|||
// tags: tags |
|||
// } |
|||
|
|||
// } |
|||
// NostrEvent { |
|||
// var tags: [[String]] = liked.tags.filter { |
|||
// tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") |
|||
// } |
|||
// tags.append(["e", liked.id]) |
|||
// tags.append(["p", liked.pubkey]) |
|||
// let ev = NostrEvent(content: "+", pubkey: pubkey, kind: 7, tags: tags) |
|||
// ev.calculate_id() |
|||
// ev.sign(privkey: privkey) |
|||
// return ev |
|||
// } |
|||
</script> |
@ -1,189 +0,0 @@ |
|||
<template> |
|||
<div :class="mapToggleClass()" class="relative min-h-screen"> |
|||
<l-map |
|||
ref="map" |
|||
v-model:zoom="zoom" |
|||
:center="[34.41322, -1.219482]" |
|||
:minZoom="minZoom" |
|||
:maxZoom="maxZoom" |
|||
> |
|||
<!-- :maxBounds="[34.41322, -1.219482]" |
|||
:maxBoundsViscosity="1.0" --> |
|||
|
|||
<l-tile-layer |
|||
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" |
|||
layer-type="base" |
|||
name="OpenStreetMap" |
|||
attribution='<a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' |
|||
/> |
|||
|
|||
|
|||
<!-- <l-marker v-for="([relay, result]) in Object.entries(geo)" :lat-lng="getLatLng(result)" :key="relay"> |
|||
<l-popup> |
|||
{{ relay }} |
|||
</l-popup> |
|||
</l-marker> --> |
|||
|
|||
<l-marker |
|||
v-for="relay in getRelaysWithGeo" |
|||
:lat-lng="getLatLng(relay)" |
|||
:key="relay" |
|||
:radius="2" |
|||
:weight="4" |
|||
:color="getCircleColor(relay)" |
|||
:fillOpacity="1" > |
|||
<l-popup> |
|||
{{ relay }} |
|||
</l-popup> |
|||
</l-marker> |
|||
</l-map> |
|||
</div> |
|||
|
|||
|
|||
|
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent, toRefs } from 'vue' |
|||
import "leaflet/dist/leaflet.css" |
|||
import { LMap, LTileLayer, LMarker, LPopup } from "@vue-leaflet/vue-leaflet" |
|||
import { setupStore } from '@/store' |
|||
import RelaysLib from '@/shared/relays-lib.js' |
|||
|
|||
export default defineComponent({ |
|||
name: "MapInteractive", |
|||
components: { |
|||
LMap, |
|||
LTileLayer, |
|||
LMarker, |
|||
LPopup, |
|||
}, |
|||
data() { |
|||
return { |
|||
minZoom: 4, |
|||
maxZoom: 7, |
|||
center: [70.41322, -1.219482], |
|||
expanded: false, |
|||
relays: [], |
|||
}; |
|||
}, |
|||
setup(props){ |
|||
const {activePageItemProp: activePageItem} = toRefs(props) |
|||
const {resultsProp: results} = toRefs(props) |
|||
return { |
|||
store : setupStore(), |
|||
results: results, |
|||
activePageItem: activePageItem |
|||
} |
|||
}, |
|||
mounted() { |
|||
console.log('results', this.results) |
|||
this.geo = this.store.relays.geo |
|||
}, |
|||
updated(){}, |
|||
unmounted(){ |
|||
console.log('unmounted', '$refs', this.$refs) |
|||
delete this.$refs.map |
|||
}, |
|||
props: { |
|||
resultsProp: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
activePageItemProp: { |
|||
type: String, |
|||
default(){ |
|||
return "" |
|||
} |
|||
}, |
|||
}, |
|||
computed: { |
|||
getCircleClass(relay){ |
|||
console.log('the relay', relay) |
|||
return (relay) => { |
|||
return { |
|||
visible: this.isRelayInActiveSubsection(relay), |
|||
hidden: !this.isRelayInActiveSubsection(relay), |
|||
[relay]: true |
|||
} |
|||
} |
|||
}, |
|||
getRelaysWithGeo(){ |
|||
return this.store.relays.getAll.filter( relay => this.geo?.[relay] instanceof Object ) |
|||
}, |
|||
}, |
|||
methods: Object.assign(RelaysLib, { |
|||
mapHeight(){ |
|||
return this.expanded ? "500px" : "250px" |
|||
}, |
|||
getLatLng(relay){ |
|||
// console.log('geo', relay, [this.geo.lat, this.geo.lon]) |
|||
return [this.geo[relay].lat, this.geo[relay].lon] |
|||
}, |
|||
getCircleColor(relay){ |
|||
if(!this.isRelayInActiveSubsection(relay)) |
|||
return 'transparent' |
|||
|
|||
if(this.results[relay]?.aggregate == 'public') |
|||
return '#00AA00' |
|||
|
|||
if(this.results[relay]?.aggregate == 'restricted') |
|||
return '#FFA500' |
|||
|
|||
if(this.results[relay]?.aggregate == 'offline') |
|||
return '#FF0000' |
|||
|
|||
}, |
|||
|
|||
isRelayInActiveSubsection(relay){ |
|||
// console.log(this.store.relays.getRelays(this.activePageItem).length, this.activePageItem, relay, this.store.relays.getRelays(this.activePageItem).includes(relay)) |
|||
return this.store.relays.getRelays(this.activePageItem, this.results).includes(relay) |
|||
}, |
|||
toggleMap(){ |
|||
this.expanded = !this.expanded |
|||
setTimeout(() => { this.resetMapSize() }, 1) |
|||
}, |
|||
mapToggleClass(){ |
|||
return { |
|||
expanded: this.expanded |
|||
} |
|||
}, |
|||
resetMapSize(){ |
|||
if (this.$refs.map.ready) |
|||
this.$refs.map.leafletObject.invalidateSize() |
|||
} |
|||
}), |
|||
|
|||
}); |
|||
|
|||
|
|||
</script> |
|||
|
|||
<style> |
|||
/* :root { |
|||
--map-tiles-filter: brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7); |
|||
} |
|||
|
|||
@media (prefers-color-scheme: dark) { |
|||
.leaflet-tile { |
|||
filter:var(--map-tiles-filter, none); |
|||
} |
|||
} */ |
|||
</style> |
|||
|
|||
<style scoped> |
|||
.leaflet-container { |
|||
position:absolute; |
|||
z-index:900; |
|||
margin:0; |
|||
padding:0; |
|||
top:0; |
|||
bottom:0; |
|||
width:100%; |
|||
height:100% !important; |
|||
} |
|||
</style> |
|||
|
@ -1,34 +0,0 @@ |
|||
<template> |
|||
<div class="flex bg-slate-600 text-right flex-row-reverse align-middle "> |
|||
<div class="flex-none h-8 align-middle "> |
|||
<RefreshComponent |
|||
v-bind:resultsProp="results" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import RefreshComponent from '@/components/relays/RefreshComponent.vue' |
|||
import { defineComponent, toRefs } from 'vue' |
|||
import { setupStore } from '@/store' |
|||
|
|||
export default defineComponent({ |
|||
name: 'RelayControlComponent', |
|||
components: {RefreshComponent}, |
|||
setup(props){ |
|||
const {resultsProp: results} = toRefs(props) |
|||
return { |
|||
store : setupStore(), |
|||
results: results |
|||
} |
|||
}, |
|||
props: { |
|||
resultsProp: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
}, |
|||
}) |
|||
</script> |
@ -1,124 +0,0 @@ |
|||
<template> |
|||
<div class="pt-5 px-1 sm:px-6 lg:px-8"> |
|||
<div class="sm:flex sm:items-center"> |
|||
<div class="sm:flex-auto text-left"> |
|||
<h1 class="text-4xl capitalize font-semibold text-gray-900"> |
|||
<span class="inline-flex rounded bg-green-800 text-sm px-2 py-1 text-white relative -top-2"> |
|||
{{ getRelaysCount(activeSubsection) }} |
|||
</span> |
|||
{{ activeSubsection }} Relays |
|||
</h1> |
|||
<p class="mt-2 text-xl text-gray-700"> |
|||
<!-- {{ store.layout.getActiveItem('relays/find') }} --> |
|||
{{ store.layout.getActiveItem('relays/find')?.description }} |
|||
</p> |
|||
</div> |
|||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> |
|||
<button |
|||
@click="$router.push('/relays/add')" |
|||
type="button" |
|||
class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-m font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"> |
|||
Add Relay |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="mt-8 flex flex-col"> |
|||
<FindRelaysSubnav /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
v-for="subsection in navSubsection" |
|||
:key="subsection.slug" > |
|||
|
|||
<!-- <div v-if="section.slug == activeSubsection"> --> |
|||
<div v-if="subsection.slug == activeSubsection"> |
|||
<ListClearnet |
|||
:resultsProp="results" |
|||
:subsectionProp="subsection.slug" |
|||
v-bind:relaysCountProp="relaysCount" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script> |
|||
//vue |
|||
import { defineComponent, toRefs } from 'vue' |
|||
//pinia |
|||
import { setupStore } from '@/store' |
|||
//shared methods |
|||
import RelaysLib from '@/shared/relays-lib.js' |
|||
import { parseHash } from '@/shared/hash-router.js' |
|||
//components |
|||
import FindRelaysSubnav from '@/components/relays/FindRelaysSubnav.vue' |
|||
import ListClearnet from '@/components/relays/ListClearnet.vue' |
|||
|
|||
const localMethods = {} |
|||
|
|||
export default defineComponent({ |
|||
name: 'HomePage', |
|||
|
|||
components: { |
|||
ListClearnet, |
|||
FindRelaysSubnav, |
|||
}, |
|||
|
|||
setup(props){ |
|||
const {resultsProp: results} = toRefs(props) |
|||
return { |
|||
store : setupStore(), |
|||
results: results |
|||
} |
|||
}, |
|||
|
|||
props: { |
|||
resultsProp: { |
|||
type: Object, |
|||
default: () => {} |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
relays: [], |
|||
timeouts: {}, |
|||
intervals: {}, |
|||
relaysCount: {}, |
|||
// activeSection: this.routeSection || this.store.layout.getActiveItem('relays')?.slug, |
|||
// activeSubsection: this.routeSubsection || this.store.layout.getActiveItem(`relays/${this.activeSection}`)?.slug, |
|||
} |
|||
}, |
|||
|
|||
updated(){}, |
|||
|
|||
beforeMount(){ |
|||
this.routeSection = this.parseHash.section || false |
|||
this.routeSubsection = this.parseHash.subsection || false |
|||
}, |
|||
|
|||
async mounted() { |
|||
this.navSubsection.forEach( item => this.relaysCount[item.slug] = 0 ) //move this |
|||
// this.relaysMountNav() |
|||
}, |
|||
|
|||
computed: { |
|||
activeSection: function(){ return this.store.layout.getActiveItem('relays')?.slug }, |
|||
activeSubsection: function(){ return this.store.layout.getActiveItem(`relays/${this.activeSection}`)?.slug }, |
|||
navSubsection: function() { return this.store.layout.getNavGroup(`relays/${this.activeSection}`) || [] }, |
|||
getRelaysCount: function() { |
|||
return (subsection) => { |
|||
if(subsection === 'all') |
|||
return this.store.relays.getAll.length |
|||
if(subsection === 'favorite') |
|||
return this.store.relays.getFavorites.length |
|||
return this.store.relays.getAll.filter( (relay) => this.results?.[relay]?.aggregate == subsection).length |
|||
|
|||
} |
|||
}, |
|||
parseHash |
|||
}, |
|||
|
|||
methods: Object.assign(RelaysLib, localMethods), |
|||
|
|||
}) |
|||
</script> |
@ -1,256 +0,0 @@ |
|||
<template> |
|||
|
|||
<td class="status-indicator" :key="generateKey(relay, 'aggregate')"> |
|||
<span :class="result?.aggregate" class="aggregate indicator"> |
|||
<span></span> |
|||
<span></span> |
|||
</span> |
|||
</td> |
|||
|
|||
<td class="relay left-align relay-url"> |
|||
<router-link :to="`/relay/${relayClean(relay)}`" active-class="active">{{ relay }}</router-link> |
|||
</td> |
|||
|
|||
<td class="verified text-center"> |
|||
<span v-if="result?.identities"> |
|||
<span v-tooltip:top.tooltip="identityList"> <span class="verified-shape-wrapper" v-if="Object.entries(result?.identities).length"><span class="shape verified"></span></span></span> |
|||
</span> |
|||
</td> |
|||
|
|||
<td class="location text-center">{{ getFlag(relay) }}</td> |
|||
|
|||
<td class="latency text-center"> |
|||
<span>{{ result?.latency?.final }}<span v-if="result?.check?.latency">ms</span></span> |
|||
</td> |
|||
|
|||
<td class="connect text-center" :key="generateKey(relay, 'check.connect')"> |
|||
<span :class="getIndicatorClass(relay, 'connect')">Connect</span> |
|||
</td> |
|||
|
|||
<td class="read text-center" :key="generateKey(relay, 'check.read')"> |
|||
<span :class="getIndicatorClass(relay, 'read')">Read</span> |
|||
</td> |
|||
|
|||
<td class="write text-center" :key="generateKey(relay, 'check.write')"> |
|||
<span :class="getIndicatorClass(relay, 'write')">write</span> |
|||
</td> |
|||
|
|||
<td class="fav text-center" :key="generateKey(relay, 'check.write')"> |
|||
<a |
|||
class=" hover:opacity-100 cursor-pointer" |
|||
:class="store.relays.isFavorite(relay) ? 'opacity-100' : 'opacity-10'" |
|||
@click="store.relays.toggleFavorite(relay)"> |
|||
❤️ |
|||
</a> |
|||
</td> |
|||
|
|||
<!-- <td class="info"> |
|||
<ul v-if="result?.observations && result?.observations.length"> |
|||
<li class="observation" v-for="(alert) in result?.observations" :key="generateKey(relay, alert.description)"> |
|||
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'notice'">✉️</span> |
|||
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'caution'">⚠️</span> |
|||
</li> |
|||
</ul> |
|||
</td>--> |
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent, toRefs } from 'vue' |
|||
// import { InspectorResult } from 'nostr-relay-inspector' |
|||
import { countryCodeEmoji } from 'country-code-emoji'; |
|||
import emoji from 'node-emoji'; |
|||
import { setupStore } from '@/store' |
|||
import crypto from 'crypto' |
|||
|
|||
export default defineComponent({ |
|||
name: 'RelaySingleComponent', |
|||
components: { |
|||
}, |
|||
props: { |
|||
relay:{ |
|||
type: String, |
|||
default(){ |
|||
return "" |
|||
} |
|||
}, |
|||
selectedRelays:{ |
|||
type: Array, |
|||
default(){ |
|||
return [] |
|||
} |
|||
}, |
|||
resultProp:{ |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
geo: {}, |
|||
showModal: false |
|||
} |
|||
}, |
|||
mounted(){ |
|||
this.geo = this.store.relays.getGeo(this.relay) |
|||
|
|||
}, |
|||
setup(props){ |
|||
const {resultProp: result} = toRefs(props) |
|||
return { |
|||
store : setupStore(), |
|||
result: result |
|||
} |
|||
}, |
|||
computed: { |
|||
relayGeo(){ |
|||
return (relay) => this.store.relays.getGeo(relay) |
|||
}, |
|||
getIndicatorClass(){ |
|||
return (url, key) => { |
|||
let cl = this.result?.check?.[key] === true |
|||
? 'success' |
|||
: this.result?.check?.[key] === false |
|||
? 'failure' |
|||
: 'pending' |
|||
return `indicator ${cl}` |
|||
} |
|||
}, |
|||
generateKey(){ |
|||
return (url, key) => crypto.createHash('md5').update(`${url}_${key}`).digest('hex') |
|||
}, |
|||
getFlag () { |
|||
return (relay) => this.relayGeo(relay)?.countryCode ? countryCodeEmoji(this.relayGeo(relay)?.countryCode) : emoji.get('shrug'); |
|||
}, |
|||
identityList () { |
|||
let string = '', |
|||
extraString = '', |
|||
users = Object.entries(this.result?.identities), |
|||
count = 0 |
|||
|
|||
if(this.result?.identities) { |
|||
if(this.result?.identities.serverAdmin) { |
|||
string = `Relay has registered an administrator pubkey: ${this.result?.identities.serverAdmin}. ` |
|||
extraString = "Additionally, " |
|||
} |
|||
|
|||
const total = users.filter(([key]) => key!='serverAdmin').length, |
|||
isOne = total==1 |
|||
|
|||
if(total) { |
|||
string = `${string}${extraString}Relay domain contains NIP-05 verification data for:` |
|||
users.forEach( ([key]) => { |
|||
if(key == "serverAdmin") return |
|||
count++ |
|||
string = `${string} ${(count==total && !isOne) ? 'and' : ''} @${key}${(count!=total && !isOne) ? ', ' : ''}` |
|||
}) |
|||
} |
|||
} |
|||
return string |
|||
}, |
|||
relayClean() { |
|||
return (relay) => relay.replace('wss://', '') |
|||
}, |
|||
}, |
|||
methods: { |
|||
// setCheck (bool) { |
|||
// return bool ? '✅ ' : '' |
|||
// }, |
|||
// setCross (bool) { |
|||
// return !bool ? '❌' : '' |
|||
// }, |
|||
// setCaution (bool) { |
|||
// return !bool ? '⚠️' : '' |
|||
// }, |
|||
// nipSignature(key){ |
|||
// return key.toString().length == 1 ? `0${key}` : key |
|||
// }, |
|||
// nipFormatted(key){ |
|||
// return `NIP-${this.nipSignature(key)}` |
|||
// }, |
|||
// nipLink(key){ |
|||
// return `https://github.com/nostr-protocol/nips/blob/master/${this.nipSignature(key)}.md` |
|||
// }, |
|||
async copy(text) { |
|||
try { |
|||
await navigator.clipboard.writeText(text); |
|||
} catch(err) { |
|||
console.error(err) |
|||
} |
|||
}, |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
ul { |
|||
margin:0; |
|||
padding:0; |
|||
} |
|||
|
|||
li { |
|||
margin:0; |
|||
padding:0; |
|||
list-style:none; |
|||
} |
|||
|
|||
td.nip-11, |
|||
td.verified span { |
|||
cursor: pointer |
|||
} |
|||
|
|||
::v-deep(.modal-container) { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
::v-deep(.modal-content) { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: Column; |
|||
max-height: 90%; |
|||
margin: 0 1rem; |
|||
padding: 1rem; |
|||
border: 1px solid #e2e8f0; |
|||
border-radius: 0.25rem; |
|||
background: #fff; |
|||
} |
|||
.modal__title { |
|||
margin: 0 2rem 0 0; |
|||
font-size: 1.5rem; |
|||
font-weight: 700; |
|||
} |
|||
.modal__content { |
|||
flex-grow: 1; |
|||
overflow-y: auto; |
|||
} |
|||
.modal__action { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-shrink: 0; |
|||
padding: 1rem 0 0; |
|||
} |
|||
.modal__close { |
|||
position: absolute; |
|||
top: 0.5rem; |
|||
right: 0.5rem; |
|||
} |
|||
|
|||
|
|||
.dark-mode div ::v-deep(.modal-content) { |
|||
border-color: #2d3748; |
|||
background-color: #1a202c; |
|||
} |
|||
|
|||
.restricted.aggregate.indicator { |
|||
position:relative; |
|||
} |
|||
|
|||
td { |
|||
padding-top:2px; |
|||
padding-bottom:2px; |
|||
} |
|||
|
|||
</style> |
@ -0,0 +1,104 @@ |
|||
<template> |
|||
<Disclosure as="nav" class="bg-white mb-5" v-slot="{ open }"> |
|||
<div class="mx-auto max-w-7xl px-0"> |
|||
<div class="flex h-12 justify-between"> |
|||
<div class="flex px-2 lg:px-0"> |
|||
<div class="hidden lg:flex lg:space-x-2"> |
|||
<a v-for="item in store.layout.getNavGroup(this.navSlug)" |
|||
:key="`subnav-${item.slug}`" |
|||
:href="item.href" |
|||
@click="setActiveContent(item.slug)" |
|||
:class="[isActive(item) ? 'bg-slate-500 text-white' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900', 'group flex items-center px-3 py-2 text-sm font-medium']" |
|||
class="inline-flex items-center pt-1.5 text-sm font-medium"> |
|||
{{ item.name }} |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end"> |
|||
<RelaysSearchFilter /> |
|||
</div> |
|||
<div class="flex items-center lg:hidden"> |
|||
<!-- Mobile menu button --> |
|||
<DisclosureButton class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"> |
|||
<span class="sr-only">Open main menu</span> |
|||
<Bars3Icon v-if="!open" class="block h-6 w-6" aria-hidden="true" /> |
|||
<XMarkIcon v-else class="block h-6 w-6" aria-hidden="true" /> |
|||
</DisclosureButton> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<DisclosurePanel class="lg:hidden"> |
|||
<div class="space-y-1 pt-2 pb-3"> |
|||
<!-- Current: "bg-indigo-50 border-indigo-500 text-indigo-700", Default: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800" --> |
|||
<DisclosureButton |
|||
v-for="item in store.layout.getNavGroup(this.navSlug)" |
|||
:key="`subnav-${item.slug}`" |
|||
@click="setActiveContent(item.slug)" |
|||
:class="[isActive(item) ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900', 'group flex items-center px-3 py-2 text-sm font-medium']" |
|||
as="a" |
|||
:href="item.href" |
|||
class="block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700"> |
|||
{{ item.name }} |
|||
</DisclosureButton> |
|||
</div> |
|||
</DisclosurePanel> |
|||
</Disclosure> |
|||
</template> |
|||
|
|||
<script> |
|||
//vue |
|||
import { defineComponent } from 'vue' |
|||
//pinia |
|||
import { setupStore } from '@/store' |
|||
//components |
|||
import RelaysSearchFilter from '@/components/relays/blocks/RelaysSearchFilter.vue' |
|||
//nav conf |
|||
import { items } from './config/relays.find.yaml' |
|||
//shared methods |
|||
import RelaysLib from '@/shared/relays-lib.js' |
|||
//hash router |
|||
import { setupNavData, mountNav, setActiveContent, loadNavContent, routeValid, parseHash, contentIsActive } from '@/shared/hash-router.js' |
|||
//tailwind |
|||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' |
|||
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/outline' |
|||
|
|||
export default defineComponent({ |
|||
title: "nostr.watch registry & network status", |
|||
name: 'RelaysFindNav', |
|||
components: { |
|||
RelaysSearchFilter, |
|||
Disclosure, DisclosureButton, DisclosurePanel, |
|||
Bars3Icon, XMarkIcon, |
|||
// PreferencesComponent, |
|||
// AuthComponent |
|||
}, |
|||
props: {}, |
|||
data(){ |
|||
return setupNavData('relays/find') |
|||
}, |
|||
setup(){ |
|||
return { |
|||
store : setupStore() |
|||
} |
|||
}, |
|||
updated(){ |
|||
|
|||
}, |
|||
created(){ |
|||
this.mountNav('subsection', items) |
|||
}, |
|||
mounted(){ |
|||
|
|||
}, |
|||
methods: Object.assign(RelaysLib, { mountNav, setActiveContent, loadNavContent}), |
|||
|
|||
computed: { |
|||
isActive(){ |
|||
return (item) => item.slug==this.navActiveContent |
|||
}, |
|||
contentIsActive, |
|||
routeValid, |
|||
parseHash |
|||
}, |
|||
}); |
|||
</script> |
@ -1,24 +1,24 @@ |
|||
items: |
|||
- name: All |
|||
slug: all |
|||
href: '#/relays/find/all' |
|||
href: '#all' |
|||
description: All relays ordered by latency |
|||
- name: Public |
|||
slug: public |
|||
href: '#/relays/find/public' |
|||
href: '#public' |
|||
description: All public relays ordered by latency. Public relays are relays where anyone can read or write. |
|||
- name: Restricted |
|||
slug: restricted |
|||
href: '#/relays/find/restricted' |
|||
href: '#restricted' |
|||
description: Restricted relays ordered by latency. Restricted relays are relays where there are read and/or write restrictions |
|||
- name: Offline |
|||
slug: offline |
|||
href: '#/relays/find/offline' |
|||
href: '#offline' |
|||
group: relays |
|||
description: Relays where a connection cannot currently be established |
|||
- name: Favorites |
|||
slug: favorite |
|||
href: '#/relays/find/favorite' |
|||
href: '#favorite' |
|||
condition: auth |
|||
description: Relays you have favorited |
|||
# - name: Invalid |
@ -0,0 +1,135 @@ |
|||
<template> |
|||
<MapSummary |
|||
:resultsProp="results" |
|||
:activePageItemProp="activeSubsection" /> |
|||
|
|||
<div id="wrapper" class="mx-auto max-w-7xl"> |
|||
<div class="pt-5 px-1 sm:px-6 lg:px-8"> |
|||
<div class="sm:flex sm:items-center"> |
|||
<div class="sm:flex-auto text-left"> |
|||
<h1 class="text-4xl capitalize font-semibold text-gray-900"> |
|||
<span class="inline-flex rounded bg-green-800 text-sm px-2 py-1 text-white relative -top-2"> |
|||
{{ getRelaysCount(activeSubsection) }} |
|||
</span> |
|||
{{ activeSubsection }} Relays |
|||
</h1> |
|||
<p class="mt-2 text-xl text-gray-700"> |
|||
<!-- {{ store.layout.getActiveItem('relays/find') }} --> |
|||
{{ store.layout.getActiveItem('relays/find')?.description }} |
|||
</p> |
|||
</div> |
|||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> |
|||
<button |
|||
@click="$router.push('/relays/add')" |
|||
type="button" |
|||
class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-m font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"> |
|||
Add Relay |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="mt-8 flex flex-col"> |
|||
<RelaysFindNav /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
v-for="subsection in navSubsection" |
|||
:key="subsection.slug" > |
|||
<div v-if="subsection.slug == activeSubsection"> |
|||
<RelaysResultTable |
|||
:resultsProp="results" |
|||
:subsectionProp="subsection.slug" |
|||
v-bind:relaysCountProp="relaysCount" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
//vue |
|||
import { defineComponent, toRefs } from 'vue' |
|||
import {useRoute} from 'vue-router' |
|||
//pinia |
|||
import { setupStore } from '@/store' |
|||
//shared methods |
|||
import RelaysLib from '@/shared/relays-lib.js' |
|||
import { parseHash } from '@/shared/hash-router.js' |
|||
//components |
|||
import RelaysFindNav from '@/components/relays/nav/RelaysFindNav.vue' |
|||
import RelaysResultTable from '@/components/relays/blocks/RelaysResultTable.vue' |
|||
import MapSummary from '@/components/relays/blocks/MapSummary.vue' |
|||
|
|||
const localMethods = {} |
|||
|
|||
export default defineComponent({ |
|||
name: 'HomePage', |
|||
|
|||
components: { |
|||
RelaysResultTable, |
|||
RelaysFindNav, |
|||
MapSummary |
|||
}, |
|||
|
|||
setup(props){ |
|||
const {resultsProp: results} = toRefs(props) |
|||
return { |
|||
store : setupStore(), |
|||
results: results |
|||
} |
|||
}, |
|||
|
|||
props: { |
|||
resultsProp: { |
|||
type: Object, |
|||
default: () => {} |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
relays: [], |
|||
timeouts: {}, |
|||
intervals: {}, |
|||
relaysCount: {}, |
|||
// activeSection: this.routeSection || this.store.layout.getActiveItem('relays')?.slug, |
|||
// activeSubsection: this.routeSubsection || this.store.layout.getActiveItem(`relays/${this.activeSection}`)?.slug, |
|||
} |
|||
}, |
|||
|
|||
updated(){}, |
|||
|
|||
beforeMount(){ |
|||
this.routeSection = this.parseHash.section || false |
|||
this.routeSubsection = this.parseHash.subsection || false |
|||
|
|||
if(this.path !== 'relays/find') |
|||
this.$router.push('/relays/find') |
|||
}, |
|||
|
|||
async mounted() { |
|||
this.navSubsection.forEach( item => this.relaysCount[item.slug] = 0 ) //move this |
|||
// this.relaysMountNav() |
|||
}, |
|||
|
|||
computed: { |
|||
path: function() { return useRoute().path }, |
|||
activeSection: function(){ return this.store.layout.getActiveItem('relays')?.slug }, |
|||
activeSubsection: function(){ return this.store.layout.getActiveItem(`relays/${this.activeSection}`)?.slug }, |
|||
navSubsection: function() { return this.store.layout.getNavGroup(`relays/${this.activeSection}`) || [] }, |
|||
getRelaysCount: function() { |
|||
return (subsection) => { |
|||
if(subsection === 'all') |
|||
return this.store.relays.getAll.length |
|||
if(subsection === 'favorite') |
|||
return this.store.relays.getFavorites.length |
|||
return this.store.relays.getAll.filter( (relay) => this.results?.[relay]?.aggregate == subsection).length |
|||
|
|||
} |
|||
}, |
|||
parseHash |
|||
}, |
|||
|
|||
methods: Object.assign(RelaysLib, localMethods), |
|||
|
|||
}) |
|||
</script> |
@ -0,0 +1,188 @@ |
|||
<template> |
|||
<div :class="mapToggleClass()" class="relative min-h-screen"> |
|||
<l-map |
|||
ref="map" |
|||
v-model:zoom="zoom" |
|||
:center="[34.41322, -1.219482]" |
|||
:minZoom="minZoom" |
|||
:maxZoom="maxZoom" |
|||
> |
|||
<!-- :maxBounds="[34.41322, -1.219482]" |
|||
:maxBoundsViscosity="1.0" --> |
|||
|
|||
<l-tile-layer |
|||
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" |
|||
layer-type="base" |
|||
name="OpenStreetMap" |
|||
attribution='<a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' |
|||
/> |
|||
|
|||
|
|||
<!-- <l-marker v-for="([relay, result]) in Object.entries(geo)" :lat-lng="getLatLng(result)" :key="relay"> |
|||
<l-popup> |
|||
{{ relay }} |
|||
</l-popup> |
|||
</l-marker> --> |
|||
|
|||
<l-marker |
|||
v-for="relay in getRelaysWithGeo" |
|||
:lat-lng="getLatLng(relay)" |
|||
:key="relay" |
|||
:radius="2" |
|||
:weight="4" |
|||
:color="getCircleColor(relay)" |
|||
:fillOpacity="1" > |
|||
<l-popup> |
|||
{{ relay }} |
|||
</l-popup> |
|||
</l-marker> |
|||
</l-map> |
|||
</div> |
|||
|
|||
|
|||
|
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
import { defineComponent, toRefs } from 'vue' |
|||
import "leaflet/dist/leaflet.css" |
|||
import { LMap, LTileLayer, LMarker, LPopup } from "@vue-leaflet/vue-leaflet" |
|||
import { setupStore } from '@/store' |
|||
import RelaysLib from '@/shared/relays-lib.js' |
|||
|
|||
export default defineComponent({ |
|||
name: "MapInteractive", |
|||
components: { |
|||
LMap, |
|||
LTileLayer, |
|||
LMarker, |
|||
LPopup, |
|||
}, |
|||
data() { |
|||
return { |
|||
minZoom: 4, |
|||
maxZoom: 7, |
|||
center: [70.41322, -1.219482], |
|||
expanded: false, |
|||
relays: [], |
|||
}; |
|||
}, |
|||
setup(props){ |
|||
const {activePageItemProp: activePageItem} = toRefs(props) |
|||
const {resultsProp: results} = toRefs(props) |
|||
return { |
|||
store : setupStore(), |
|||
results: results, |
|||
activePageItem: activePageItem |
|||
} |
|||
}, |
|||
mounted() { |
|||
console.log('results', this.results) |
|||
this.geo = this.store.relays.geo |
|||
}, |
|||
updated(){}, |
|||
unmounted(){ |
|||
console.log('unmounted', '$refs', this.$refs) |
|||
delete this.$refs.map |
|||
}, |
|||
props: { |
|||
resultsProp: { |
|||
type: Object, |
|||
default(){ |
|||
return {} |
|||
} |
|||
}, |
|||
activePageItemProp: { |
|||
type: String, |
|||
default(){ |
|||
return "" |
|||
} |
|||
}, |
|||
}, |
|||
computed: { |
|||
getCircleClass(relay){ |
|||
console.log('the relay', relay) |
|||
return (relay) => { |
|||
return { |
|||
visible: this.isRelayInActiveSubsection(relay), |
|||
hidden: !this.isRelayInActiveSubsection(relay), |
|||
[relay]: true |
|||
} |
|||
} |
|||
}, |
|||
getRelaysWithGeo(){ |
|||
return this.store.relays.getAll.filter( relay => this.geo?.[relay] instanceof Object ) |
|||
}, |
|||
}, |
|||
methods: Object.assign(RelaysLib, { |
|||
mapHeight(){ |
|||
return this.expanded ? "500px" : "250px" |
|||
}, |
|||
getLatLng(relay){ |
|||
// console.log('geo', relay, [this.geo.lat, this.geo.lon]) |
|||
return [this.geo[relay].lat, this.geo[relay].lon] |
|||
}, |
|||
getCircleColor(relay){ |
|||
if(!this.isRelayInActiveSubsection(relay)) |
|||
return 'transparent' |
|||
|
|||
if(this.results[relay]?.aggregate == 'public') |
|||
return '#00AA00' |
|||
|
|||
if(this.results[relay]?.aggregate == 'restricted') |
|||
return '#FFA500' |
|||
|
|||
if(this.results[relay]?.aggregate == 'offline') |
|||
return '#FF0000' |
|||
|
|||
}, |
|||
|
|||
isRelayInActiveSubsection(relay){ |
|||
// console.log(this.store.relays.getRelays(this.activePageItem).length, this.activePageItem, relay, this.store.relays.getRelays(this.activePageItem).includes(relay)) |
|||
return this.store.relays.getRelays(this.activePageItem, this.results).includes(relay) |
|||
}, |
|||
toggleMap(){ |
|||
this.expanded = !this.expanded |
|||
setTimeout(() => { this.resetMapSize() }, 1) |
|||
}, |
|||
mapToggleClass(){ |
|||
return { |
|||
expanded: this.expanded |
|||
} |
|||
}, |
|||
resetMapSize(){ |
|||
if (this.$refs.map.ready) |
|||
this.$refs.map.leafletObject.invalidateSize() |
|||
} |
|||
}), |
|||
|
|||
}); |
|||
|
|||
|
|||
</script> |
|||
|
|||
<style> |
|||
/* :root { |
|||
--map-tiles-filter: brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7); |
|||
} |
|||
|
|||
@media (prefers-color-scheme: dark) { |
|||
.leaflet-tile { |
|||
filter:var(--map-tiles-filter, none); |
|||
} |
|||
} */ |
|||
</style> |
|||
|
|||
<style scoped> |
|||
.leaflet-container { |
|||
position:absolute; |
|||
z-index:900; |
|||
margin:0; |
|||
padding:0; |
|||
top:0; |
|||
bottom:0; |
|||
width:100%; |
|||
height:100% !important; |
|||
} |
|||
</style> |
Loading…
Reference in new issue