Browse Source

fully ported to nostr-js

update/libraries
dskvr 2 years ago
parent
commit
8d3ce398e1
  1. 9
      codes.yaml
  2. 2
      package.json
  3. 3
      relays.yaml
  4. 560
      src/components/BaseRelays.vue
  5. 8
      src/main.js

9
codes.yaml

@ -1,7 +1,7 @@
messages:
11cb5ca38038c3eb41bd014814f6e2e18da18ff1:
text: "we don't accept any events"
code: "READ_ONLY"
code: "CONNECT_ONLY"
1003d4ec1466033d0dcc4a1babc6c5f409784593:
text: "NIP-05 verification needed to publish events"
@ -24,18 +24,21 @@ messages:
text: 'failed to save event from 5a462fa6044b4b8da318528a6987a45e3adf832bd1c64bd6910eacfecdf07541'
code: 'BLOCKS_WRITE_STATUS_CHECK'
codes:
CONNECT_ONLY:
type: "very_restricted"
description: "This relay can be connected to, but has zero read/write access"
READ_ONLY:
type: "write_restricted"
description: "This relay only access read queries"
WRITE_ONLY:
type: "write_restricted"
type: "read_restricted"
description: "This relay only accepts the publishing of events"
NIP_05_REQUIRED:
type: "write_restricted"
description: "This relay only accepts the publishing of events from NIP-05 verified public keys"
WHITELIST_REQUIRED:
type: "write_restricted"
description: "This relay onoly accepts the publishing of events from whitelisted public keys"
description: "This relay only accepts the publishing of events from whitelisted public keys"
BLOCKS_WRITE_STATUS_CHECK:
type: "maybe_public"
description: "This relay blocks the events that enable us to test writing to the relay, so there's some uncertainty"

2
package.json

@ -25,6 +25,8 @@
"nostr-tools": "0.24.1",
"onion-regex": "2.0.8",
"requests": "0.3.0",
"sass": "1.56.1",
"sass-loader": "13.2.0",
"socks-proxy-agent": "7.0.0",
"stream-browserify": "3.0.0",
"vue": "^3.2.13",

3
relays.yaml

@ -6,11 +6,11 @@ relays:
- 'wss://nostr-relay.wlvs.space'
- 'wss://nostr.onsats.org'
- 'wss://nostr-relay.untethr.me'
- 'wss://nostr.unknown.place'
- 'wss://nostr.semisol.dev'
- 'wss://nostr-pub.semisol.dev'
- 'wss://nostr-verified.wellorder.net'
- 'wss://nostr.drss.io'
- 'wss://nostr.unknown.place'
- 'wss://relay.damus.io'
- 'wss://nostr.openchain.fr'
- 'wss://nostr.delo.software'
@ -24,4 +24,5 @@ relays:
- 'wss://nostr-pub.wellorder.net'
- 'wss://relayer.fiatjaf.com'
- 'wss://nostr.rocks'
- 'wss://none.sandwich.farm'
- 'wss://nostr.sandwich.farm'

560
src/components/BaseRelays.vue

@ -1,7 +1,7 @@
<template>
<div>
<div class="text-h5 text-bold q-py-md q-px-sm full-width flex row justify-start">
<h1>Nostr Relay Registry</h1>
<h1>Nostr Relay Status <sup>alpha</sup></h1>
<span>Next ping in {{ nextPing }} seconds</span> |
<span v-if="relays.filter((url) => status[url] && !status[url].complete).length > 0">Processing {{relays.filter((url) => status[url].complete).length}}/{{relays.length}}</span>
</div>
@ -10,39 +10,40 @@
<column :xs="12" :md="12" :lg="6">
<div>
<h2><span class="indicator badge readwrite">{{ query('public').length }}</span>Public</h2>
<table class="online" v-if="query('public').length > 0">
<table class="online public" v-if="query('public').length > 0">
<tr>
<th></th>
<th></th>
<th>ℹ️</th>
<th>🔌</th>
<th>👁🗨</th>
<th></th>
<th><span class="verified-shape-wrapper"><span class="shape verified"></span></span></th>
<th>🌎</th>
<!-- <td>wl</td>
<td>nip-05><td> -->
<th></th>
<th>ℹ️</th>
<th>NIP-15</th>
<th>NIP-20</th>
<th>FILTER: LIMIT</th>
</tr>
<tr v-for="relay in query('public')" :key="{relay}" :class="getLoadingClass(relay)">
<td :key="generateKey(relay, 'aggregate')"><span :class="getAggregateStatusClass(relay)"></span></td>
<td class="left-align relay-url" @click="copy(relay)">{{ relay }}</td>
<td>
<ul v-if="Object.keys(status[relay].notes).length">
<li v-tooltip:left.tooltip="key" v-for="(message, key) in status[relay].notes" :key="generateKey(relay, key)"></li>
</ul>
</td>
<td :key="generateKey(relay, 'didConnect')"><span :class="getStatusClass(relay, 'didConnect')"></span></td>
<td :key="generateKey(relay, 'didRead')"><span :class="getStatusClass(relay, 'didRead')"></span></td>
<td :key="generateKey(relay, 'didWrite')"><span :class="getStatusClass(relay, 'didWrite')"></span></td>
<td></td>
<td>{{status[relay].flag}}</td>
<td><span v-if="status[relay].didConnect">{{ status[relay].latency }}<span v-if="status[relay].latency">ms</span></span></td>
<td>
<Popper v-if="Object.keys(status[relay].messages).length">
{{ status[relay].type }}
<button @mouseover="showPopper">log</button>
<template #content>
<ul>
<li v-for="(message, key) in status[relay].messages" :key="generateKey(relay, key)">{{key}}</li>
</ul>
</template>
</Popper>
</td>
<td>{{ setCheck(status[relay].didNip15) }}</td>
<td>{{ setCheck(status[relay].didNip20) }}</td>
<td>{{ setCaution(status[relay].didSubscribeFilterLimit) }}</td>
</tr>
</table>
</div>
@ -71,14 +72,9 @@
<td>{{status[relay].flag}}</td>
<td><span v-if="status[relay].didConnect">{{ status[relay].latency }}<span v-if="status[relay].latency">ms</span></span></td>
<td>
<Popper v-if="Object.keys(status[relay].messages).length">
<button @mouseover="showPopper">log</button>
<template #content>
<ul>
<li v-for="(message, key) in status[relay].messages" :key="generateKey(relay, key)">{{key}}</li>
</ul>
</template>
</Popper>
<ul v-if="Object.keys(status[relay].notes).length">
<li v-tooltip:left.tooltip="key" v-for="(message, key) in status[relay].notes" :key="generateKey(relay, key)"></li>
</ul>
</td>
</tr>
</table>
@ -91,7 +87,7 @@
<th>🔌</th>
<th>👁🗨</th>
<th></th>
<th>msg</th>
<th>ℹ️</th>
</tr>
<tr v-for="relay in query('offline')" :key="{relay}" :class="getLoadingClass(relay)">
<td :key="generateKey(relay, 'aggregate')"><span :class="getAggregateStatusClass(relay)"></span></td>
@ -100,14 +96,9 @@
<td :key="generateKey(relay, 'didRead')"><span :class="getStatusClass(relay, 'didRead')"></span></td>
<td :key="generateKey(relay, 'didWrite')"><span :class="getStatusClass(relay, 'didWrite')"></span></td>
<td>
<Popper v-if="Object.keys(status[relay].messages).length">
<button>log</button>
<template #content>
<ul>
<li v-for="(message, key) in status[relay].messages" :key="generateKey(relay, key)">{{key}}</li>
</ul>
</template>
</Popper>
<ul v-if="Object.keys(status[relay].notes).length">
<li v-tooltip:left.tooltip="key" v-for="(message, key) in status[relay].notes" :key="generateKey(relay, key)"></li>
</ul>
</td>
</tr>
</table>
@ -116,7 +107,7 @@
</row>
<!-- <h2>Processing</h2>
<table v-if="relays.filter((url) => !status[url].complete).length > 0">
<table v-if="relays.filter((url) => status[url] && !status[url].complete).length > 0">
<tr>
<th></th>
</tr>
@ -125,12 +116,20 @@
</tr>
</table>
<a href="./relays/">JSON API</a> -->
<a class="credit" href="http://sandwich.farm">Another 🥪 by sandwich.farm</a>
</div>
</template>
<script>
/* eslint-disable */
import { defineComponent} from 'vue'
import TooltipComponent from './TooltipComponent.vue'
// import nip04 from 'nostr-tools/nip04'
// import nip05 from '../../node_modules/nostr-tools/nip05'
// import nip06 from 'nostr-tools/nip06'
// import { relayConnect } from 'nostr-tools/relay'
import { RelayPool, Relay } from 'nostr'
@ -152,7 +151,7 @@ export default defineComponent({
components: {
Row,
Column,
Popper
TooltipComponent
},
data() {
@ -164,11 +163,156 @@ export default defineComponent({
connections: {},
latency: {},
pool: null,
timeouts: {},
nips: {},
}
},
mounted() {
this.relays.forEach(async url => {
this.status[url] = {}
await this.testRelay(url)
})
let latencyIntVal
let counterIntVal
// eslint-disable-next-line
let latencyTimeout = setTimeout(() => {
this.testRelayLatency()
// eslint-disable-next-line
latencyIntVal = setInterval(() => { this.testRelayLatency() }, refreshMillis)
// eslint-disable-next-line
latencyIntVal = setInterval(() => { this.nextPing = Math.round((this.lastPing + refreshMillis - Date.now())/1000)}, 1000)
}, 10000)
},
methods: {
hardFail(url) {
if(!this.status[url]) this.status[url] = {}
this.status[url].didConnect = false
this.status[url].didRead = false
this.status[url].didWrite = false
this.tryComplete(url)
if(this.connections[url].close) this.connections[url].close()
},
async testRelay (url) {
this.lastPing = Date.now()
this.latency[url] = {}
this.timeouts[url] = {}
this.status[url].notes = {}
this.status[url].state = 'pending'
this.nips[url] = new Array(99).fill(null);
this.status[url].readEventCount = 0
this.status[url].writeEventCount = 0
this.status[url].latencyEventCount = 0
this.status[url].didNip15 = false
this.status[url].didNip20 = false
this.timeouts[url].didConnect = setTimeout(() => {
console.log(url, "TIMEOUT")
if(Object.keys(this.status[url].notes).length == 0) this.status[url].notes['Reason: Timeout'] = {}
this.hardFail(url)
}, 20000)
let relay = Relay(url, {reconnect: false})
relay.on('open', e => {
console.log(url, "OPEN")
clearTimeout(this.timeouts[url].didConnect)
this.status[url].didConnect = true
this.testRead(url, "testRead")
this.testWrite(url, "testWrite")
this.tryComplete(url)
console.log(url, "did connect", this.status[url].didConnect)
})
relay.on('eose', e => {
//console.log('EOSE', e)
// relay.close()
this.tryComplete(url)
this.status[url].didNip15 = true
})
relay.on('error', (e) => {
//console.log('ERROR', e)
clearTimeout(this.timeouts[url].didConnect)
this.status[url].notes['Reason: Error'] = {}
this.hardFail(url)
})
relay.on('ok', () => {
this.status[url].didNip20 = true
})
relay.on('close', (e) => {
console.log('close', e)
// console.dir(arguments)
})
relay.on('other', (e) => {
// console.log('OTHER!!!!', e)
})
relay.on('event', (sub_id, ev) => {
// console.log('event', sub_id, ev)
if(sub_id == this.getID(url, "testRead")) {
// console.log("SUCCESS:", "READ")
this.status[url].readEventCount++
this.status[url].didRead = true
clearTimeout(this.timeouts[url].testRead)
this.connections[url].unsubscribe(sub_id)
// this.tryComplete(url)
this.tryComplete(url)
}
if(sub_id == this.getID(url, "testWrite")) {
if(this.status[url].writeEventCount < 1) {
// console.log("SUCCESS:", "WRITE")
this.status[url].didWrite = true
console.log(ev)
// this.tryComplete(url)
//console.log(url, this.timeouts[url].testWrite)
clearTimeout(this.timeouts[url].testWrite)
this.tryComplete(url)
}
this.status[url].writeEventCount++
}
if(sub_id == this.getID(url, "testLatency")) {
if(this.status[url].latencyEventCount < 1) {
console.log(url, "SUCCESS:", "test latency")
clearTimeout(this.timeouts[url].testLatency)
console.log(this.latency[url].start, this.latency[url].final)
this.latency[url].final = Date.now() - this.latency[url].start
this.setLatency(url)
}
this.status[url].latencyEventCount++
// this.tryComplete(url)
//console.log(url, this.timeouts[url].testRead)
}
// relay.unsubscribe(sub_id)
})
relay.on('message', (message) => {
// console.log('message', message)
// console.dir(arguments)
})
relay.on('notice', (message) => {
const hash = this.sha1(message)
let message_obj = RELAY_MESSAGES[hash]
let code_obj = RELAY_CODES[message_obj.code]
message_obj.type = code_obj.type
message_obj.hash = hash
this.status[url].notes[code_obj.description] = message_obj
// this.adjustStatus(url, hash)
})
this.connections[url] = relay
await this.getIP(url)
await this.setGeo(url)
this.setFlag(url)
// this.setNip05(url)
},
// query (group, filterType) {
query (group) {
let unordered,
@ -214,10 +358,10 @@ export default defineComponent({
getStatusClass (url, key) {
let status = this.status?.[url]?.[key] === true
? 'green'
? 'success'
: this.status?.[url]?.[key] === false
? 'red'
: 'silver'
? 'failure'
: 'pending'
return `indicator ${status}`
},
@ -225,6 +369,14 @@ export default defineComponent({
return this.status?.[url]?.complete ? "relay loaded" : "relay"
},
// setNip05(url){
// const data = nip05(url.replace('wss://', ''))
// if(data.length) {
// this.nips[url][5] = data
// this.status[url].nip05 = true
// }
// },
setAggregateStatus (url) {
let aggregateTally = 0
aggregateTally += this.status?.[url]?.didConnect ? 1 : 0
@ -249,123 +401,43 @@ export default defineComponent({
}
},
setComplete (url) {
tryComplete (url) {
let connect = typeof this.status?.[url]?.didConnect !== 'undefined',
read = typeof this.status?.[url]?.didRead !== 'undefined',
write = typeof this.status?.[url]?.didWrite !== 'undefined'
// console.log(connect, read, write)
this.setAggregateStatus(url)
console.log(url, 'trying complete', connect, read, write)
if(connect && read && write) {
console.log(url, 'did complete')
this.setAggregateStatus(url)
this.adjustStatus(url)
this.status[url].complete = true
this.connections[url].close()
this.status[url].testing = false
if(this.status[url].readEventCount > 1) {
this.status[url].didSubscribeFilterLimit = false
} else {
this.status[url].didSubscribeFilterLimit = true
}
}
},
generateKey (url, key) {
return `${url}_${key}`
},
testConnect (url) {
// console.log(url, "CONNECT", "TEST")
// const self = this
// this.connections[url] = Relay(url)
// console.log(this.connections[url])
// this.connections[url]
// .on('open', (e) => {
// console.log('open', e)
// self.testWrite(url)
// self.testRead(url)
// })
// .on('error', (e) => {
// console.log('error', e)
// })
//
// .on('message', (message) => {
// console.log('message', message)
//
// // console.log(url, "CONNECT", "SUCCESS")
// // const hash = this.sha1(message)
// // let message_obj = RELAY_MESSAGES[hash]
// // let code_obj = RELAY_CODES[message_obj.code]
// //
// // message_obj.type = code_obj.type
// // this.status[url].messages[message] = message_obj
// // this.adjustStatus(url, hash)
// })
// .on('event', (relay, sub_id, ev) => {
// console.log('event', relay, sub_id, ev)
// })
// .on('notice', (message) => {
// console.log('notice', message)
// })
// .on('close', (e) => {
// console.log('close', e)
// })
// // () => {},
// (message) => {
//
//
// console.log(hash)
// console.dir(message_obj)
// console.dir(code_obj)
//
//
// // console.log("RECIEVED MESSAGE!")
// // console.dir(this.status[url].messages)
// },
// () => {
// console.log(url, "CONNECT", "FAILURE")
// this.status[url].didConnect = false
// this.status[url].didRead = false
// this.status[url].didWrite = false
// this.setComplete(url)
// }
// )
// this.status[url].didConnect = true
async testRead (url, id, benchmark) {
const subid = this.getID(url, id)
if(benchmark) this.latency[url].start = Date.now()
if(benchmark) console.log(url, subid, this.latency[url].start)
this.connections[url].subscribe(subid, {limit: 1, kinds:[1]})
this.timeouts[url][id] = setTimeout(() => {
if(!benchmark) this.status[url].didRead = false
this.tryComplete(url)
}, 3000)
},
async testRead (url) {
// console.log(url, "READ", "TEST")
this.connections[url].subscribe(this.getID(url, "read"), {limit: 1, kinds:[1]})
// console.dir(this.connections[url])
// // console.log(this.connections[url]['get status']())
// console.log(url, "READ", "TEST")
// let start
// start = Date.now();
//
//
// let {unsub} = await this.connections[url].sub(
// {
// cb: () => {
// console.log(url, "READ", "SUCCESS")
// this.status[url].didRead = true
// this.setComplete(url)
// this.latency[url].read = Date.now() - start;
// unsub()
// clearTimeout(willUnsub)
// },
// filter: {
// ids: [
// '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0'
// ]
// }
// },
// 'nostr-registry'
// )
// let willUnsub = setTimeout(() => {
// unsub()
// console.log(url, "READ", "FAILURE")
// if(!this.status[url].maybe_public) this.status[url].didRead = false
// this.setComplete(url)
// }, 10000)
},
async testWrite (url) {
async testWrite (url, id, benchmark) {
// console.log(url, "WRITE", "TEST")
const message = {
id: '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0',
pubkey:
@ -377,97 +449,39 @@ export default defineComponent({
sig: '08e6303565e9282f32bed41eee4136f45418f366c0ec489ef4f90d13de1b3b9fb45e14c74f926441f8155236fb2f6fef5b48a5c52b19298a0585a2c06afe39ed'
}
this.connections[url].send(["EVENT", message])
this.connections[url].subscribe(this.getID(url, "write"), {limit: 1, kinds:[1], ids:['41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0']})
this.connections[url].subscribe(this.getID(url, id), {limit: 1, kinds:[1], ids:['41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0']})
this.timeouts[url][id] = setTimeout(() => {
console.log(url, "did write", id, false)
if(!benchmark) this.status[url].didWrite = false
this.tryComplete(url)
}, 10000)
},
getID(url, keyword) {
return `${keyword}_${url}`
},
async testRelay (url) {
this.lastPing = Date.now()
this.latency[url] = {}
this.status[url].messages = {}
let relay
relay = Relay(url)
relay.on('open', e => {
// console.log(url, "OPEN")
this.status[url].didConnect = true
this.testRead(url)
this.testWrite(url)
this.setComplete(url)
})
relay.on('eose', e => {
// relay.close()
})
relay.on('ok', () => {
// console.log('ok')
// console.dir(arguments)
})
relay.on('error', (e, b) => {
// console.log(b, e)
this.status[url].didConnect = false
})
relay.on('close', (e) => {
// console.log('close', e)
// console.dir(arguments)
})
relay.on('other', (e) => {
// console.log('OTHER!!!!', e)
})
relay.on('event', (sub_id, ev) => {
// console.log('event', sub_id, ev)
if(sub_id == this.getID(url, "read")) {
// console.log("SUCCESS:", "READ")
this.status[url].didRead = true
this.setComplete(url)
}
if(sub_id == this.getID(url, "write")) {
// console.log("SUCCESS:", "WRITE")
this.status[url].didWrite = true
this.setComplete(url)
}
// relay.unsubscribe(sub_id)
})
relay.on('message', (message) => {
// console.log('message', message)
// console.dir(arguments)
})
relay.on('notice', (message) => {
console.log('notice', message)
// console.dir(arguments)
})
this.connections[url] = relay
this.setLatency(url)
// await this.getIP(url)
// await this.setGeo(url)
this.setFlag(url)
},
isOnion(url){
return onionRegex().test(url)
},
setLatency(url) {
this.status[url].latency = this.latency[url].read
// console.log(this.status[url].didConnect === true, this.status[url]. latency,this.latency[url].final)
if (this.status[url].didConnect === true) this.status[url].latency = this.latency[url].final
// console.log(this.status[url].didConnect === true, this.status[url]. latency,this.latency[url].final)
console.log("latency",this.latency[url])
},
testRelayLatency(){
// console.log('testing latency')
console.log('testing latency')
this.relays.forEach(url => {
// this.testWrite(url, true)
this.testRead(url, true)
this.setLatency(url)
console.log(url, 'did read', this.status[url].didRead)
if(this.status[url].didRead) {
console.log(url, 'testing read')
this.testRead(url, "testLatency", true)
}
this.lastPing = Date.now()
})
this.lastPing = Date.now()
},
async getIP(url){
@ -491,49 +505,42 @@ export default defineComponent({
this.status[url].flag = this.status[url].geo?.countryCode ? countryCodeEmoji(this.status[url].geo.countryCode) : emoji.get('shrug');
},
adjustStatus (url, hash) {
let code = RELAY_MESSAGES[hash].code,
type = RELAY_CODES[code].type
this.status[url][type] = code
if (type == "maybe_public") {
this.status[url].didWrite = true
this.status[url].didRead = true
}
if (type == "write_restricted") {
this.status[url].didWrite = false
}
setCheck (bool) {
return bool ? '✅ ' : ''
},
setCross (bool) {
return !bool ? '❌' : ''
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
// console.log(message, ':', hash)
return hash
setCaution (bool) {
return !bool ? '⚠️' : ''
},
},
mounted() {
this.relays.forEach(async url => {
this.status[url] = {} //statusInterface
if (this.isOnion(url)) {
url = `${url}.to` //add proxy
}
await this.testRelay(url)
})
adjustStatus (url) {
Object.entries(this.status[url].notes).forEach( ([key, value]) => {
if(!value.hasOwnProperty("hash")) return
let code = RELAY_MESSAGES[value.hash].code,
type = RELAY_CODES[code].type
// // eslint-disable-next-line
// let latencyTimeout = setTimeout(() => { this.testRelayLatency() }, 10000)
//
// // eslint-disable-next-line
// let latencyIntVal = setInterval(() => { this.testRelayLatency() }, refreshMillis)
// // eslint-disable-next-line
// let counterIntVal = setInterval(() => {
// this.nextPing = Math.round((this.lastPing + refreshMillis - Date.now())/1000)
// }, 1000)
this.status[url][type] = code
if (type == "maybe_public") {
this.status[url].didWrite = false
this.status[url].aggregate = 'public'
}
if (type == "write_restricted") {
this.status[url].didWrite = false
}
})
},
sha1 (message) {
const hash = crypto.createHash('sha1').update(JSON.stringify(message)).digest('hex')
// console.log(message, ':', hash)
return hash
},
},
})
@ -589,7 +596,8 @@ tr.relay.loaded td {
}
.badge.write-only,
.badge.read-only {
.badge.read-only,
table.online.public .indicator.failure {
background-color:orange !important;
}
@ -599,22 +607,22 @@ tr.relay.loaded td {
border-style: solid;
}
.indicator.silver {
.indicator.pernding {
background-color: #c0c0c0;
border-color: rgba(55,55,55,0.5);
}
.indicator.green {
.indicator.success {
background-color: green;
border-color: rgba(0,255,0,0.5);
}
.indicator.red {
.indicator.failure {
background-color: red;
border-color: rgba(255,0,0,0.5);
}
.indicator.orange {
.indicator.caution {
background-color: orange;
border-color: rgba(255, 191, 0,0.5);
}
@ -678,4 +686,50 @@ table.online .relay-url {
cursor: pointer;
}
.verified-shape-wrapper {
display:inline-block;
width: 16px;
height: 16px;
}
.shape.verified {
background: blue;
width: 16px;
height: 16px;
position: relative;
top: 5px;
left:-5px;
text-align: center;
}
.shape.verified:before,
.shape.verified:after {
content: "";
position: absolute;
top: 0;
left: 0;
height: 13px;
width: 13px;
background: blue;
}
.shape.verified:before {
transform: rotate(30deg);
}
.shape.verified:after {
transform: rotate(75deg);
}
sup {
color: #c0c0c0;
font-size:15px;
}
.credit {
display:inline-block;
text-decoration:none;
color:#333;
text-transform: uppercase;
font-size:12px;
margin-top:25px;
}
</style>

8
src/main.js

@ -1,4 +1,10 @@
import { createApp } from 'vue'
import App from './App.vue'
import "./styles/main.scss"
import directives from "./directives/"
createApp(App).mount('#app')
const app = createApp(App)
directives(app);
app.mount('#app')

Loading…
Cancel
Save