Browse Source

vue router

develop
dskvr 2 years ago
parent
commit
5cea60dcfd
  1. 3
      src/components/layout/HeaderComponent.vue
  2. 0
      src/components/layout/NavComponent.vue
  3. 104
      src/components/relays/FindRelaysSubnav.vue
  4. 23
      src/components/relays/LikeComponent.vue
  5. 189
      src/components/relays/MapInteractive.vue
  6. 34
      src/components/relays/RelayControl.vue
  7. 124
      src/components/relays/RelaysFind.vue
  8. 256
      src/components/relays/SingleClearnet.vue
  9. 0
      src/components/relays/TaskQueue.vue
  10. 0
      src/components/relays/blocks/AddRelay.vue
  11. 0
      src/components/relays/blocks/MapSingle.vue
  12. 0
      src/components/relays/blocks/MapSummary.vue
  13. 57
      src/components/relays/blocks/RefreshComponent.vue
  14. 4
      src/components/relays/blocks/RelaysResultTable.vue
  15. 2
      src/components/relays/blocks/RelaysSearchFilter.vue
  16. 104
      src/components/relays/nav/RelaysFindNav.vue
  17. 34
      src/components/relays/nav/RelaysNav.vue
  18. 10
      src/components/relays/nav/config/relays.find.yaml
  19. 0
      src/components/relays/nav/config/relays.yaml
  20. 135
      src/components/relays/pages/RelaysFind.vue
  21. 42
      src/components/relays/pages/RelaysHome.vue
  22. 188
      src/components/relays/pages/RelaysMap.vue
  23. 10
      src/components/relays/pages/RelaysSingle.vue
  24. 13
      src/components/relays/pages/RelaysStatistics.vue
  25. 0
      src/components/relays/redirects/RedirectComponent.vue
  26. 0
      src/components/user/AuthComponent.vue
  27. 0
      src/components/user/PreferencesComponent.vue
  28. 6
      src/main.js
  29. 33
      src/router/index.js
  30. 5
      src/shared/hash-router.js
  31. 42
      src/store/tasks.js
  32. 606
      src/styles/main.scss

3
src/components/layout/HeaderComponent.vue

@ -117,8 +117,7 @@ import { defineComponent } from 'vue'
import { setupStore } from '@/store'
import UserLib from '@/shared/user-lib.js'
// import PreferencesComponent from '@/components/PreferencesComponent.vue'
import AuthComponent from '@/components/AuthComponent.vue'
import AuthComponent from '@/components/user/AuthComponent.vue'
import {version} from '../../../package.json'

0
src/components/NavComponent.vue → src/components/layout/NavComponent.vue

104
src/components/relays/FindRelaysSubnav.vue

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

23
src/components/relays/LikeComponent.vue

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

189
src/components/relays/MapInteractive.vue

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

34
src/components/relays/RelayControl.vue

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

124
src/components/relays/RelaysFind.vue

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

256
src/components/relays/SingleClearnet.vue

@ -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
src/components/relays/TaskQueue.vue

0
src/components/relays/AddRelay.vue → src/components/relays/blocks/AddRelay.vue

0
src/components/relays/MapSingle.vue → src/components/relays/blocks/MapSingle.vue

0
src/components/relays/MapSummary.vue → src/components/relays/blocks/MapSummary.vue

57
src/components/relays/RefreshComponent.vue → src/components/relays/blocks/RefreshComponent.vue

@ -6,7 +6,7 @@
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ this.store.tasks.getProcessedRelays.length }}/{{ this.relays.length }} Relays Checked
{{ this.store.tasks.getProcessed('relays').length }}/{{ this.relays.length }} Relays Checked
</span>
</span>
<span class="text-white text-sm mr-2 mt-1.5" v-if="!store.tasks.isProcessing">-</span>
@ -37,6 +37,13 @@ import { Inspector } from 'nostr-relay-inspector'
const localMethods = {
addToQueue: function(id, fn){
this.store.tasks.addJob({
id: id,
handler: fn.bind(this)
})
},
setRefreshInterval: function(){
clearInterval(this.interval)
this.interval = setInterval(() => {
@ -59,14 +66,25 @@ const localMethods = {
else
this.windowActive = true
},
// handleRelaysFind(){
// this.addToQueue('relays/find', () => this.invalidate())
// },
// handleRelaysSingle(relayURL){
// this.addToQueue('relays/single', () => this.invalidate(false, relayUrl))
// },
invalidate: async function(force, single){
if( (!this.isExpired && !force) )
return
this.store.tasks.startProcessing()
this.store.tasks.startProcessing('relays')
const relays = this.relays.filter( relay => !this.store.tasks.isRelayProcessed(relay) )
// if(this.store.tasks.isActive)
// this.store.tasks.setRate('relays/find', 0)
// else
// this.store.tasks.setRate('relays/find', 2000)
if(single) {
await this.check(single)
}
@ -74,27 +92,13 @@ const localMethods = {
// const processed = new Set()
for(let index = 0; index < relays.length; index++) {
const relay = relays[index]
await this.delay(this.averageLatency).then( () => {
this.check(relay)
.then((result) => {
if(this.store.tasks.isRelayProcessed(result.uri))
return
this.store.tasks.addProcessedRelay(result.uri)
this.results[result.uri] = result
this.setCache(result)
// //console.log('processing status', processed, '/', this.relays.length)
console.log('complete?', result.uri, this.store.tasks.getProcessedRelays.length, this.relays.length)
if(this.store.tasks.getProcessedRelays.length >= this.relays.length)
this.completeAll()
})
.catch( err => console.error(err) )
}).catch(err => console.error(err))
await this.check(relay)
}
}
},
completeAll: function(){
this.store.tasks.finishProcessing()
this.store.tasks.finishProcessing('relays')
this.store.relays.updateNow()
//console.log('all are complete?', this.store.tasks.isProcessing)
// const aggregates = new Object()
@ -109,6 +113,8 @@ const localMethods = {
check: async function(relay){
await this.delay(this.averageLatency)
return new Promise( (resolve, reject) => {
const opts = {
checkLatency: true,
@ -132,6 +138,7 @@ const localMethods = {
instance.relay.close()
instance.result.log = instance.log
resolve(instance.result)
// console.log('complete?', instance.result.uri, this.store.tasks.getProcessed('relays', relays.length), this.relays.length)
})
.on('close', (relay) => {
console.log(`${relay.url} has closed`)
@ -141,6 +148,20 @@ const localMethods = {
})
.run()
})
.then((result) => {
if(this.store.tasks.isRelayProcessed(result.uri))
return
this.store.tasks.addProcessed('relays', result.uri)
this.results[result.uri] = result
this.setCache(result)
if(this.store.tasks.getProcessed('relays').length >= this.relays.length)
this.completeAll()
})
.catch( err => console.error(err) )
},
getAverageLatency: function(){
const latencies = new Array()

4
src/components/relays/ListClearnet.vue → src/components/relays/blocks/RelaysResultTable.vue

@ -41,8 +41,8 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<tr v-for="(relay, index) in subsectionRelays" :key="relay" :class="getResultClass(relay, index)">
<td class="status-indicator" :key="generateKey(relay, 'aggregate')">
<tr v-for="(relay, index) in subsectionRelays" :key="generateKey(relay, 'aggregate')" :class="getResultClass(relay, index)">
<td class="status-indicator">
<span :class="results[relay]?.aggregate" class="aggregate indicator">
<span></span>
<span></span>

2
src/components/relays/ToolFilter.vue → src/components/relays/blocks/RelaysSearchFilter.vue

@ -35,7 +35,7 @@ const localMethods = {
}
export default defineComponent({
name: 'ToolFilter',
name: 'RelaysSearchFilter',
components: {
MagnifyingGlassIcon
},

104
src/components/relays/nav/RelaysFindNav.vue

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

34
src/components/relays/SubnavComponent.vue → src/components/relays/nav/RelaysNav.vue

@ -4,39 +4,44 @@
<div class="flex lg:w-32 lg:px-8 lg:ml-8 sm:px-0 sm:w-0 md:w-0"></div>
<div class="lg:flex lg:px-0">
<div class="lg:ml-6 lg:flex lg:space-x-8">
<a v-for="item in store.layout.getNavGroup(this.navSlug)"
<router-link to="/relays/find" :class="isActive ? 'bg-color-white-100 text-white' : inactiveClass" class="inline-flex items-center mx-1 text-sm font-medium text-white">Relays</router-link>
<router-link to="/relays/map" class="inline-flex items-center mx-1 text-sm font-medium text-white">Map</router-link>
<router-link to="/relays/statistics" class="inline-flex items-center mx-1 text-sm font-medium text-white">Go to Foo</router-link>
<!-- <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-gray-100 text-gray-900' : ' hover:bg-gray-50 hover:text-gray-900', 'group flex items-center px-3 text-sm font-medium']"
class="inline-flex items-center mx-1 text-sm font-medium text-white">
{{ item.name }}
</a>
</a> -->
</div>
</div>
<div class="width-max lg:flex lg:ml-auto">
<RefreshComponent
v-bind:resultsProp="results" />
v-bind:resultsProp="results"
v-if="path == '/relays/find'"/>
</div>
</div>
</div>
{{ isActive }}
</template>
<script>
import { defineComponent, toRefs } from 'vue'
import { items } from './config/subnav.yaml'
import {useRoute} from 'vue-router'
// import { items } from './config/nav.yaml'
import { setupStore } from '@/store'
import RelaysLib from '@/shared/relays-lib.js'
import { setupNavData, mountNav, setActiveContent, loadNavContent, routeValid, parseHash, contentIsActive } from '@/shared/hash-router.js'
import RefreshComponent from '@/components/relays/RefreshComponent.vue'
import RefreshComponent from '@/components/relays/blocks/RefreshComponent.vue'
export default defineComponent({
title: "nostr.watch registry & network status",
name: 'NavComponent',
components: {
// PreferencesComponent,
// AuthComponent
RefreshComponent,
},
props: {
@ -48,7 +53,7 @@ export default defineComponent({
},
},
data(){
return setupNavData('relays')
// return setupNavData('relays')
},
setup(props){
const {resultsProp: results} = toRefs(props)
@ -62,17 +67,20 @@ export default defineComponent({
},
beforeMount(){
this.mountNav('section', items)
console.log('rightt now', this.store.layout)
// this.mountNav('section', this.store.layout.getNavGroup('relays'))
},
mounted(){
},
methods: Object.assign(RelaysLib, { mountNav, setActiveContent, loadNavContent, routeValid, parseHash, contentIsActive }),
methods: Object.assign(RelaysLib, setupNavData, mountNav, setActiveContent, loadNavContent, routeValid, parseHash, contentIsActive),
computed: {
isActive(){
return (item) => item.slug==this.navActiveContent
}
path: function() { return useRoute().path },
// isActive(){
// return (item) => item.slug==this.navActiveContent
// }
},
});
</script>

10
src/components/relays/config/find-pagenav.yaml → src/components/relays/nav/config/relays.find.yaml

@ -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
src/components/relays/config/subnav.yaml → src/components/relays/nav/config/relays.yaml

135
src/components/relays/pages/RelaysFind.vue

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

42
src/components/relays/pages/RelaysHome.vue

@ -1,38 +1,30 @@
<template>
<SubnavComponent
<RelaysNav
v-bind:resultsProp="results" />
<MapSummary
<RelaysFind
:resultsProp="results"
:activePageItemProp="activeSubsection"
v-if="activeSection == 'find'" />
v-if="this.path == '/relays/find' || this.path == '/relays' || this.path == '/'" />
<MapInteractive
:resultsProp="results"
v-if="activeSection == 'map'" />
<div id="wrapper" class="mx-auto max-w-7xl">
<RelaysFind
:resultsProp="results"
v-if="activeSection == 'find'" />
v-if="this.path == '/relays/map'" />
<RelayStatistics
:resultsProp="results"
v-if="activeSection == 'stats'" />
v-if="this.path == '/relays/statistics'" />
<div id="footer">
<span class="credit">
<!-- <a href="http://sandwich.farm">Another 🥪 by sandwich.farm</a>, -->
built with <a href="https://github.com/jb55/nostr-js">nostr-js</a> and <a href="https://github.com/dskvr/nostr-relay-inspector">nostr-relay-inspector</a>, inspired by <a href="https://github.com/fiatjaf/nostr-relay-registry">nostr-relay-registry</a></span>
</div>
</div>
</template>
<script>
//vue
import { defineComponent } from 'vue'
import {useRoute} from 'vue-router'
import { useHead } from '@vueuse/head'
import { setupStore } from '@/store'
//shared methods
@ -40,12 +32,11 @@ import RelaysLib from '@/shared/relays-lib.js'
import { parseHash } from '@/shared/hash-router.js'
//components
import MapSummary from '@/components/relays/MapSummary.vue'
import SubnavComponent from '@/components/relays/SubnavComponent.vue'
import RelaysNav from '@/components/relays/nav/RelaysNav.vue'
import RelaysFind from '@/components/relays/RelaysFind.vue'
import RelayStatistics from '@/components/relays/RelayStatistics.vue'
import MapInteractive from '@/components/relays/MapInteractive.vue'
import RelaysFind from '@/components/relays/pages/RelaysFind.vue'
import RelayStatistics from '@/components/relays/pages/RelaysStatistics.vue'
import MapInteractive from '@/components/relays/pages/RelaysMap.vue'
//data
import { relays } from '../../../../relays.yaml'
import { geo } from '../../../../cache/geo.yaml'
@ -69,8 +60,7 @@ export default defineComponent({
name: 'HomePage',
components: {
SubnavComponent,
MapSummary,
RelaysNav,
RelaysFind,
RelayStatistics,
MapInteractive
@ -97,13 +87,10 @@ export default defineComponent({
timeouts: {},
intervals: {},
relaysCount: {},
// activeSection: this.routeSection || this.store.layout.getActiveItem('relays')?.slug,
// activeSubsection: this.routeSubsection || this.store.layout.getActiveItem(`relays/${this.activeSection}`)?.slug,
}
},
beforeMount(){
this.routeSection = this.parseHash.section || false
this.routeSubsection = this.parseHash.subsection || false
this.relaysLoadData()
},
@ -123,8 +110,9 @@ export default defineComponent({
computed: {
activeSection: function(){ return this.store.layout.getActiveItem('relays')?.slug },
activeSubsection: function(){ return this.store.layout.getActiveItem(`relays/${this.activeSection}`)?.slug },
// activeSection: function(){ return this.store.layout.getActiveItem('relays')?.slug },
path: function() { return useRoute().path },
activeSubsection: function(){ return this.navSubsection?.slug },
navSubsection: function() { return this.store.layout.getNavGroup(`relays/${this.activeSection}`) || [] },
parseHash
},

188
src/components/relays/pages/RelaysMap.vue

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

10
src/components/relays/pages/RelaysSingle.vue

@ -1,7 +1,6 @@
<template>
<SubnavComponent
v-bind:resultsProp="results" />
<RelaysNav/>
<MapSingle
:geo="geo"
@ -144,8 +143,8 @@
import { defineComponent} from 'vue'
import SubnavComponent from '@/components/relays/SubnavComponent.vue'
import MapSingle from '@/components/relays/MapSingle.vue'
import RelaysNav from '@/components/relays/nav/RelaysNav.vue'
import MapSingle from '@/components/relays/blocks/MapSingle.vue'
import SafeMail from "@2alheure/vue-safe-mail";
@ -181,8 +180,7 @@ export default defineComponent({
components: {
MapSingle,
SafeMail,
SubnavComponent
// RefreshComponent,
RelaysNav // RefreshComponent,
},
data() {

13
src/components/relays/RelayStatistics.vue → src/components/relays/pages/RelaysStatistics.vue

@ -27,7 +27,7 @@
<pre>
{{ this.store.relays.getAll.filter( (relay) => this.results?.[relay]?.aggregate == 'offline').length }}
</pre>
<!--
<pre>
{{ store.stats.get('history') }}
</pre>
@ -46,7 +46,7 @@
<pre>
{{ }}
</pre>
</pre> -->
<!-- <pre>
{{ this.collateSupportedNips }}
@ -103,7 +103,11 @@ export default defineComponent({
this.store.stats.set('nips', this.collateSupportedNips)
this.store.stats.set('continents', this.collateContinents)
this.store.stats.set('countries', this.collateCountries)
this.store.stats.setHistory(await this.historicalData())
this.remoteTask = await this.historicalData()
this.store.stats.setHistory(this.remoteTask)
},
unmounted(){
delete this.remoteTask
},
data: function(){
return {
@ -112,7 +116,8 @@ export default defineComponent({
bySupportedNips: null,
byCountry: null,
byContinent: null,
history: null
history: null,
remoteTask: null
}
},
props: {

0
src/components/RedirectComponent.vue → src/components/relays/redirects/RedirectComponent.vue

0
src/components/AuthComponent.vue → src/components/user/AuthComponent.vue

0
src/components/PreferencesComponent.vue → src/components/user/PreferencesComponent.vue

6
src/main.js

@ -8,7 +8,7 @@ import directives from "./directives/"
import { plugin as storePlugin } from './store'
import { RelayPool } from 'nostr'
import VueInputAutowidth from 'vue-input-autowidth'
import { relays } from '../relays.yaml'
// import { relays } from '../relays.yaml'
const app = createApp(App)
.use(router)
@ -19,8 +19,8 @@ const app = createApp(App)
directives(app);
app.config.globalProperties.$pool = new RelayPool(relays, {reconnect: false})
// app.config.globalProperties.$pool = new RelayPool(['wss://relay.nostr.ch'])
// app.config.globalProperties.$pool = new RelayPool(relays, {reconnect: false})
app.config.globalProperties.$pool = new RelayPool(['wss://relay.nostr.ch'])
await router.isReady()

33
src/router/index.js

@ -1,9 +1,13 @@
// /router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import RelaysHome from '@/components/relays/pages/RelaysHome.vue'
// import ByStatus from '../pages/ByStatus.vue'
import RelaysFind from '@/components/relays/pages/RelaysFind.vue'
import RelaysSingle from '@/components/relays/pages/RelaysSingle.vue'
import RedirectComponent from '@/components/RedirectComponent.vue'
import RelaysMap from '@/components/relays/pages/RelaysMap.vue'
import RelaysStatistics from '@/components/relays/pages/RelaysStatistics.vue'
import RedirectComponent from '@/components/relays/redirects/RedirectComponent.vue'
const routes = [
{
@ -13,13 +17,36 @@ const routes = [
},
component: RedirectComponent
},
{
path: '/relays',
component: RelaysHome,
children: [
{
path: 'map',
component: RelaysMap,
},
{
path: 'find',
component: RelaysFind,
},
{
path: 'find/(*.)',
component: RelaysFind,
},
{
path: 'statistics',
component: RelaysStatistics,
},
]
},
{
path: '/relay/:relayUrl(.*)',
component: RelaysSingle
},
{
path: '/',
component: RelaysHome
component: RelaysFind
},

5
src/shared/hash-router.js

@ -63,10 +63,7 @@ const contentIsActive = function(){
const parseHash = function(){
const hashParams = this.$route.hash.replace('#', '').replace(/^\/+/g, '').split('/')
const result = {}
result.page = hashParams[0] || null
result.section = hashParams[1] || null
result.subsection = hashParams[2] || null
result.relay = hashParams[1] && hashParams[1].includes('.') ? hashParams[1] : null
result.section = hashParams[0] || null
return result
}

42
src/store/tasks.js

@ -8,24 +8,32 @@ export const useTaskStore = defineStore('tasks', {
active: new Object(),
//legacy
processing: false,
processedRelays: new Array(),
currentTask: null
processing: new Object(),
processed: new Object(),
currentTask: new Object(),
}),
getters: {
//legacy
getProcessedRelays: (state) => Array.from(state.processedRelays),
getProcessed: (state) => (key) => {
if( !(state.processed[key] instanceof Array) )
state.processed[key] = new Array()
return state.processed[key]
},
isProcessing: (state) => state.processing,
isRelayProcessed: (state) => (relay) => state.processedRelays.includes(relay),
isRelayProcessed: (state) => (relay) => state.getProcessed('relay').includes(relay),
//queue/lists
getPending: (state) => state.pending,
getActive: (state) => state.active,
getActiveSlug: (state) => state.active.id,
getCompleted: (state) => state.completed,
//queue/states
isActive: (state) => Object.keys( state.active ).length > 0,
isIdle: (state) => Object.keys( state.active ).length == 0,
arePending: (state) => state.pending.length > 0,
//
// getRate: (state) => (key) => state.rate[key],
},
actions: {
//queue
@ -45,7 +53,6 @@ export const useTaskStore = defineStore('tasks', {
this.active = {}
}
},
clearJobs(type){
this[type] = new Array()
},
@ -57,18 +64,21 @@ export const useTaskStore = defineStore('tasks', {
this.pending.splice( index, 1 )
},
//legacy
addProcessedRelay(relay){
if(!this.processedRelays.includes(relay))
this.processedRelays.push(relay)
startProcessing(key) {
this.processing[key] = true
this.currentTask[key] = key
},
finishProcessing() {
this.processing = false
this.processedRelays = new Array()
this.currentTask = null
finishProcessing(key) {
this.processed[key] = new Array()
this.processing[key] = false
this.currentTask[key] = null
},
startProcessing() {
this.processing = true
this.currentTask = "relays/check" //need to figure this out.
addProcessed(key, relay){
if( !(this.processed[key] instanceof Array) )
this.processed[key] = new Array()
if(!this.processed[key].includes(relay))
this.processed[key].push(relay)
},
},
})

606
src/styles/main.scss

@ -4,329 +4,329 @@
@tailwind utilities;
// td ul { padding:0; margin:0; list-style: none; }
// td ul li { list-style: none; }
td ul { padding:0; margin:0; list-style: none; }
td ul li { list-style: none; }
// .q-tabs {
// border-bottom: 1px solid var(--q-accent)
// }
// table {
// width:100%;
// }
// .left-align {
// text-align:left;
// }
// tr.relay td {
// font-style: italic;
// opacity: 0.5;
// }
// tr.relay.loaded td {
// font-style: normal;
// opacity: 1;
// }
// // .indicator {
// // display:block;
// // margin: 0 auto;
// // height: 14px;
// // width: 14px;
// // border-radius: 7px;
// // border-width:0px;
// // }
// .badge {
// height:auto;
// width: auto;
// display:inline-block;
// padding: 2px 5px;
// font-size: 15px;
// position: relative;
// top: -3px;
// min-width: 15px;
// margin-right:5px;
// }
// .public .badge,
// .offline .badge {
// color: white;
// }
.q-tabs {
border-bottom: 1px solid var(--q-accent)
}
// .public.online .badge {
// background:green
// }
// .restricted.online .badge {
// background:orange
// }
// .offline .badge {
// background:red
// }
// tr.online .indicator.failure {
// background-color:orange !important;
// }
// .aggregate.indicator {
// background-color: transparent;
// border-radius: 0px;
// border-style: solid;
// }
// .indicator.pending {
// background-color: #c0c0c0;
// border-color: rgba(55,55,55,0.5);
// }
// .indicator.success {
// background-color: green;
// border-color: rgba(0,255,0,0.5);
// }
table {
width:100%;
}
// .indicator.failure {
// background-color: red;
// border-color: rgba(255,0,0,0.5);
// }
.left-align {
text-align:left;
}
// .indicator.caution {
// background-color: orange;
// border-color: rgba(255, 191, 0,0.5);
// }
tr.relay td {
font-style: italic;
opacity: 0.5;
}
// .indicator.public {
// background-color: green;
// border-color: rgba(0,255,0,0.5);
// }
tr.relay.loaded td {
font-style: normal;
opacity: 1;
}
// .indicator {
// display:block;
// margin: 0 auto;
// height: 14px;
// width: 14px;
// border-radius: 7px;
// border-width:0px;
// }
.badge {
height:auto;
width: auto;
display:inline-block;
padding: 2px 5px;
font-size: 15px;
position: relative;
top: -3px;
min-width: 15px;
margin-right:5px;
}
.public .badge,
.offline .badge {
color: white;
}
.public.online .badge {
background:green
}
.restricted.online .badge {
background:orange
}
.offline .badge {
background:red
}
tr.online .indicator.failure {
background-color:orange !important;
}
.aggregate.indicator {
background-color: transparent;
border-radius: 0px;
border-style: solid;
}
.indicator.pending {
background-color: #c0c0c0;
border-color: rgba(55,55,55,0.5);
}
.indicator.success {
background-color: green;
border-color: rgba(0,255,0,0.5);
}
.indicator.failure {
background-color: red;
border-color: rgba(255,0,0,0.5);
}
.indicator.caution {
background-color: orange;
border-color: rgba(255, 191, 0,0.5);
}
.indicator.public {
background-color: green;
border-color: rgba(0,255,0,0.5);
}
.indicator.restricted {
position:relative;
border-color: transparent;
background-color: transparent
}
.indicator.restricted span:first-child {
position:absolute;
width: 0;
height: 0;
border-top: 14px solid green;
border-right: 14px solid transparent;
}
.indicator.restricted span:last-child {
position:absolute;
width: 0;
height: 0;
border-bottom: 14px solid orange;
border-left: 14px solid transparent;
}
//
// .indicator.restricted {
// position:relative;
// border-color: transparent;
// background-color: transparent
// }
//
// .indicator.restricted span:first-child {
// position:absolute;
// width: 0;
// height: 0;
// border-top: 14px solid green;
// border-right: 14px solid transparent;
// border-bottom: 14px solid orange;
// border-left: 14px solid transparent;
// }
//
// .indicator.restricted span:last-child {
// position:absolute;
// width: 0;
// height: 0;
// border-bottom: 14px solid orange;
// border-left: 14px solid transparent;
// }
// //
// // .indicator.restricted {
// // position:relative;
// // border-color: transparent;
// // background-color: transparent
// // }
// //
// // .indicator.restricted span:first-child {
// // position:absolute;
// // width: 0;
// // height: 0;
// // border-bottom: 14px solid orange;
// // border-left: 14px solid transparent;
// // }
// //
// // .indicator.restricted span:last-child {
// // position:absolute;
// // width: 0;
// // height: 0;
// // border-top: 14px solid green;
// // border-right: 14px solid transparent;
// // }
// .indicator.offline {
// background-color: red;
// border-color: rgba(255,0,0,0.5);
// }
// tr.online .relay-url {
// cursor: pointer;
// }
// .verified-shape-wrapper {
// display:inline-block;
// width: 16px;
// height: 16px;
// }
// .shape.verified {
// background: #777;
// width: 16px;
// height: 16px;
// position: relative;
// top: 5px;
// left:-5px;
// text-align: center;
// }
// th .shape.verified:before,
// th .shape.verified:after {
// background:#c0c0c0 !important;
// }
// .shape.verified:before,
// .shape.verified:after {
// content: "";
// position: absolute;
// top: 0;
// left: 0;
// height: 13px;
// width: 13px;
// background: #777;
// }
// .shape.verified:before {
// transform: rotate(30deg);
// }
// .shape.verified:after {
// transform: rotate(75deg);
// }
// .credit {
// display:inline-block;
// color:#333;
// text-transform: lowercase;
// font-size:12px;
// margin-top:25px;
// }
// .credit a {
// text-decoration:none;
// }
// // #wrapper {
// // max-width:1400px;
// // margin:0 auto;
// // }
// div.block {
// display:block;
// margin:40px 0;
// }
// h1 {
// position:relative;
// display:inline-block;
// margin: 0 auto
// }
// h1 sup {
// color: #c0c0c0;
// font-size:15px;
// position:absolute;
// right: -45px;
// top:15px;
// }
// .title-card {
// text-align:center;
// }
// .title-card h1 {
// font-size:4.5em;
// text-align:center;
// }
// .row {
// -webkit-box-shadow: 0px 1px 32px 4px rgba(0,0,0,0.16);
// -moz-box-shadow: 0px 1px 32px 4px rgba(0,0,0,0.16);
// box-shadow: 0px 1px 32px 4px rgba(0,0,0,0.16);
// }
// .title-info-card {
// border-radius: 20px;
// text-align:center;
// }
// .title-info-card span {
// display:inline-block;
// margin-top:0.80em;
// font-size: 4em;
// letter-spacing: -0.1em;
// text-align:right;
// }
// .processing-card {
// margin: 0.8em 0;
// display:block;
// margin:1.5em 0;
// font-size: 4em;
// letter-spacing: -0.1em;
// text-align:center;
// }
// .loading {
// animation-duration: 1.8s;
// animation-fill-mode: forwards;
// animation-iteration-count: infinite;
// animation-name: placeHolderShimmer;
// animation-timing-function: linear;
// background: #f6f7f8;
// background: linear-gradient(to right, #fafafa 8%, #f4f4f4 38%, #fafafa 54%);
// background-size: 1000px 640px;
// position: relative
// }
// .loaded .loading {
// animation: none;
// bakground:none;
// display:none;
// }
// @keyframes placeHolderShimmer{
// 0%{
// background-position: -468px 0
// }
// 100%{
// background-position: 468px 0
// }
// }
// li.observation {
// display: inline;
// cursor: pointer;
// }
// tr.offline .verified, tr.offline .verified *,
// tr.offline .latency,
// tr.offline .nip,
// tr.offline .location {
// visibility: hidden;
// }
// @media only screen and (min-device-width: 640px)
// and (max-device-width: 480px)
// and (orientation: portrait) {
// table .latency,
// table .verified,
// table .nip {
// display: none;
// }
// }
// @media only screen and (min-device-width: 640px)
// and (max-device-width: 480px)
// and (orientation: landscape) {
// table .nip {
// display: none;
// border-top: 14px solid green;
// border-right: 14px solid transparent;
// }
// }
.indicator.offline {
background-color: red;
border-color: rgba(255,0,0,0.5);
}
tr.online .relay-url {
cursor: pointer;
}
.verified-shape-wrapper {
display:inline-block;
width: 16px;
height: 16px;
}
.shape.verified {
background: #777;
width: 16px;
height: 16px;
position: relative;
top: 5px;
left:-5px;
text-align: center;
}
th .shape.verified:before,
th .shape.verified:after {
background:#c0c0c0 !important;
}
.shape.verified:before,
.shape.verified:after {
content: "";
position: absolute;
top: 0;
left: 0;
height: 13px;
width: 13px;
background: #777;
}
.shape.verified:before {
transform: rotate(30deg);
}
.shape.verified:after {
transform: rotate(75deg);
}
.credit {
display:inline-block;
color:#333;
text-transform: lowercase;
font-size:12px;
margin-top:25px;
}
.credit a {
text-decoration:none;
}
// #wrapper {
// max-width:1400px;
// margin:0 auto;
// }
div.block {
display:block;
margin:40px 0;
}
h1 {
position:relative;
display:inline-block;
margin: 0 auto
}
h1 sup {
color: #c0c0c0;
font-size:15px;
position:absolute;
right: -45px;
top:15px;
}
.title-card {
text-align:center;
}
.title-card h1 {
font-size:4.5em;
text-align:center;
}
.row {
-webkit-box-shadow: 0px 1px 32px 4px rgba(0,0,0,0.16);
-moz-box-shadow: 0px 1px 32px 4px rgba(0,0,0,0.16);
box-shadow: 0px 1px 32px 4px rgba(0,0,0,0.16);
}
.title-info-card {
border-radius: 20px;
text-align:center;
}
.title-info-card span {
display:inline-block;
margin-top:0.80em;
font-size: 4em;
letter-spacing: -0.1em;
text-align:right;
}
.processing-card {
margin: 0.8em 0;
display:block;
margin:1.5em 0;
font-size: 4em;
letter-spacing: -0.1em;
text-align:center;
}
.loading {
animation-duration: 1.8s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: placeHolderShimmer;
animation-timing-function: linear;
background: #f6f7f8;
background: linear-gradient(to right, #fafafa 8%, #f4f4f4 38%, #fafafa 54%);
background-size: 1000px 640px;
position: relative
}
.loaded .loading {
animation: none;
bakground:none;
display:none;
}
@keyframes placeHolderShimmer{
0%{
background-position: -468px 0
}
100%{
background-position: 468px 0
}
}
li.observation {
display: inline;
cursor: pointer;
}
tr.offline .verified, tr.offline .verified *,
tr.offline .latency,
tr.offline .nip,
tr.offline .location {
visibility: hidden;
}
@media only screen and (min-device-width: 640px)
and (max-device-width: 480px)
and (orientation: portrait) {
table .latency,
table .verified,
table .nip {
display: none;
}
}
@media only screen and (min-device-width: 640px)
and (max-device-width: 480px)
and (orientation: landscape) {
table .nip {
display: none;
}
}

Loading…
Cancel
Save