351 lines
11 KiB
HTML
351 lines
11 KiB
HTML
{% extends "public.html" %} {% block page %}
|
|
<q-layout view="hHh Lpr lff">
|
|
<q-drawer v-model="drawer" side="left">
|
|
<q-toolbar class="bg-primary text-white shadow-2">
|
|
<q-toolbar-title>Settings</q-toolbar-title>
|
|
<q-btn flat round dense icon="close" @click="drawer = !drawer"></q-btn>
|
|
</q-toolbar>
|
|
<div class="q-pa-md">
|
|
<q-list padding>
|
|
<q-expansion-item
|
|
expand-separator
|
|
icon="perm_identity"
|
|
label="Merchants"
|
|
caption="Add/Remove pubkeys"
|
|
>
|
|
<q-card>
|
|
<q-card-section>
|
|
<q-input
|
|
filled
|
|
v-model="inputPubkey"
|
|
@keydown.enter="addPubkey"
|
|
type="text"
|
|
label="Pubkey/Npub"
|
|
hint="Add merchants"
|
|
>
|
|
<q-btn @click="addPubkey" dense flat icon="add"></q-btn>
|
|
</q-input>
|
|
<q-list class="q-mt-md">
|
|
<q-item v-for="pub in Array.from(pubkeys)" :key="pub">
|
|
{%raw%}
|
|
<q-item-section avatar>
|
|
<q-avatar>
|
|
<img
|
|
v-if="profiles.get(pub) && profiles.get(pub).picture"
|
|
:src="profiles.get(pub).picture"
|
|
/>
|
|
<img
|
|
v-else
|
|
src="/nostrmarket/static/images/blank-avatar.webp"
|
|
/>
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label
|
|
v-if="profiles.get(pub) && profiles.get(pub).name"
|
|
>{{ profiles.get(pub).name }}</q-item-label
|
|
>
|
|
<q-item-label v-else
|
|
>{{ `${pub.slice(0, 5)}...${pub.slice(-5)}`
|
|
}}</q-item-label
|
|
>
|
|
<q-tooltip>{{ pub }}</q-tooltip>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-btn
|
|
class="gt-xs"
|
|
size="12px"
|
|
flat
|
|
dense
|
|
round
|
|
icon="delete"
|
|
@click="removePubkey(pub)"
|
|
/>
|
|
</q-item-section>
|
|
{%endraw%}
|
|
</q-item>
|
|
</q-list>
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-expansion-item>
|
|
<q-expansion-item
|
|
expand-separator
|
|
icon="perm_identity"
|
|
label="Relays"
|
|
caption="Add/Remove relays"
|
|
>
|
|
<q-card>
|
|
<q-card-section>
|
|
<q-input
|
|
filled
|
|
v-model="inputRelay"
|
|
@keydown.enter="addRelay"
|
|
type="text"
|
|
label="Relay URL"
|
|
hint="Add relays"
|
|
>
|
|
<q-btn @click="addRelay" dense flat icon="add"></q-btn>
|
|
</q-input>
|
|
<q-list dense class="q-mt-md">
|
|
<q-item v-for="url in Array.from(relays)" :key="url">
|
|
{%raw%}
|
|
<q-item-section>
|
|
<q-item-label>{{ url }}</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-btn
|
|
class="gt-xs"
|
|
size="12px"
|
|
flat
|
|
dense
|
|
round
|
|
icon="delete"
|
|
@click="removeRelay(url)"
|
|
/>
|
|
</q-item-section>
|
|
{%endraw%}
|
|
</q-item>
|
|
</q-list>
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-expansion-item>
|
|
</q-list>
|
|
</div>
|
|
</q-drawer>
|
|
<q-page-container>
|
|
<div class="row q-mb-md">
|
|
<div class="col-12 q-gutter-y-md">
|
|
<q-toolbar>
|
|
<q-btn flat round dense icon="menu" @click="drawer = !drawer"></q-btn>
|
|
{%raw%}
|
|
<q-toolbar-title style="text-transform: capitalize">
|
|
{{ activePage }}
|
|
</q-toolbar-title>
|
|
{%endraw%}
|
|
<q-input
|
|
class="float-left q-ml-md"
|
|
standout
|
|
square
|
|
dense
|
|
outlined
|
|
clearable
|
|
v-model.trim="searchText"
|
|
label="Search for products"
|
|
>
|
|
<template v-slot:append>
|
|
<q-icon v-if="!searchText" name="search" />
|
|
</template>
|
|
</q-input>
|
|
</q-toolbar>
|
|
</div>
|
|
</div>
|
|
<customer-market
|
|
v-if="activePage == 'market'"
|
|
:products="filterProducts"
|
|
:exchange-rates="exchangeRates"
|
|
></customer-market>
|
|
</q-page-container>
|
|
</q-layout>
|
|
{% endblock %} {% block scripts %}
|
|
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
|
|
|
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
|
<script src="{{ url_for('nostrmarket_static', path='components/customer-market/customer-market.js') }}"></script>
|
|
<script>
|
|
const nostr = window.NostrTools
|
|
const defaultRelays = [
|
|
'wss://relay.damus.io',
|
|
'wss://relay.snort.social',
|
|
'wss://nos.lol',
|
|
'wss://nostr.wine',
|
|
'wss://relay.nostr.bg',
|
|
'wss://nostr-pub.wellorder.net',
|
|
'wss://nostr-pub.semisol.dev',
|
|
'wss://eden.nostr.land',
|
|
'wss://nostr.mom',
|
|
'wss://nostr.fmt.wiz.biz',
|
|
'wss://nostr.zebedee.cloud',
|
|
'wss://nostr.rocks'
|
|
]
|
|
const eventToObj = event => {
|
|
event.content = JSON.parse(event.content)
|
|
return {
|
|
...event,
|
|
...Object.fromEntries(event.tags)
|
|
}
|
|
}
|
|
|
|
Vue.component(VueQrcode.name, VueQrcode)
|
|
|
|
Promise.all([
|
|
customerMarket('static/components/customer-market/customer-market.html')
|
|
])
|
|
|
|
new Vue({
|
|
el: '#vue',
|
|
mixins: [windowMixin],
|
|
data: function () {
|
|
return {
|
|
drawer: false,
|
|
pubkeys: new Set(),
|
|
relays: new Set(),
|
|
events: [],
|
|
stalls: new Map(),
|
|
products: [],
|
|
profiles: new Map(),
|
|
searchText: null,
|
|
exchangeRates: null,
|
|
inputPubkey: null,
|
|
inputRelay: null,
|
|
activePage: 'market'
|
|
}
|
|
},
|
|
computed: {
|
|
filterProducts() {
|
|
if (!this.searchText || this.searchText.length < 2) return this.products
|
|
return this.products.filter(p => {
|
|
return (
|
|
p.product.includes(this.searchText) ||
|
|
p.description.includes(this.searchText) ||
|
|
p.categories.includes(this.searchText)
|
|
)
|
|
})
|
|
}
|
|
},
|
|
async created() {
|
|
// Hardcode pubkeys for testing
|
|
this.pubkeys.add(
|
|
'855ea22a88d7df7ccd8497777db81f115575d5362f51df3af02ead383f5eaba2'
|
|
)
|
|
this.pubkeys.add(
|
|
'8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd'
|
|
)
|
|
// What component to render on start
|
|
let stall_id = JSON.parse('{{ stall_id | tojson }}')
|
|
let product_id = JSON.parse('{{ product_id | tojson }}')
|
|
if (stall_id) {
|
|
if (product_id) {
|
|
this.activePage = 'product'
|
|
} else {
|
|
this.activePage = 'stall'
|
|
}
|
|
}
|
|
this.$q.loading.show()
|
|
this.relays = new Set(defaultRelays)
|
|
await this.initNostr()
|
|
this.$q.loading.hide()
|
|
},
|
|
methods: {
|
|
async initNostr() {
|
|
const pool = new nostr.SimplePool()
|
|
let relays = Array.from(this.relays)
|
|
let products = new Map()
|
|
let stalls = new Map()
|
|
// Get metadata and market data from the pubkeys
|
|
let sub = await pool
|
|
.list(relays, [
|
|
{
|
|
kinds: [0, 30005],
|
|
authors: Array.from(this.pubkeys)
|
|
}
|
|
])
|
|
.then(events => {
|
|
this.events = events || []
|
|
this.events.map(eventToObj).map(e => {
|
|
if (e.kind == 0) {
|
|
this.profiles.set(e.pubkey, e.content)
|
|
return
|
|
} else if (e.content.stall) {
|
|
//it's a product `d` is the prod. id
|
|
products.set(e.d, {...e.content, id: e.d})
|
|
} else {
|
|
// it's a stall `d` is the stall id
|
|
stalls.set(e.d, {...e.content, id: e.d})
|
|
return
|
|
}
|
|
})
|
|
})
|
|
await Promise.resolve(sub)
|
|
this.stalls = await Array.from(stalls.values())
|
|
|
|
this.products = Array.from(products.values()).map(obj => {
|
|
obj.currency = 'EUR' // placeholder for testing/dev
|
|
let stall = this.stalls.find(s => s.id == obj.stall)
|
|
obj.stallName = stall.name
|
|
if (obj.currency != 'sat') {
|
|
obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency)
|
|
obj.priceInSats = this.getValueInSats(obj.price, obj.currency)
|
|
}
|
|
return obj
|
|
})
|
|
|
|
pool.close(relays)
|
|
},
|
|
async getRates() {
|
|
let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat')
|
|
if (noFiat) return
|
|
try {
|
|
let rates = await axios.get('https://api.opennode.co/v1/rates')
|
|
this.exchangeRates = rates.data.data
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
getValueInSats(amount, unit = 'USD') {
|
|
if (!this.exchangeRates) return 0
|
|
return Math.ceil(
|
|
(amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8
|
|
)
|
|
},
|
|
getAmountFormated(amount, unit = 'USD') {
|
|
return LNbits.utils.formatCurrency(amount, unit)
|
|
},
|
|
async addPubkey() {
|
|
let pubkey = String(this.inputPubkey).trim()
|
|
let regExp = /^#([0-9a-f]{3}){1,2}$/i
|
|
if (pubkey.startsWith('n')) {
|
|
try {
|
|
let {type, data} = nostr.nip19.decode(pubkey)
|
|
if (type === 'npub') pubkey = data
|
|
else if (type === 'nprofile') {
|
|
pubkey = data.pubkey
|
|
givenRelays = data.relays
|
|
}
|
|
this.pubkeys.add(pubkey)
|
|
this.inputPubkey = null
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
} else if (regExp.test(pubkey)) {
|
|
pubkey = pubkey
|
|
}
|
|
this.pubkeys.add(pubkey)
|
|
await this.initNostr()
|
|
},
|
|
removePubkey(pubkey) {
|
|
// Needs a hack for Vue reactivity
|
|
let pubkeys = this.pubkeys
|
|
pubkeys.delete(pubkey)
|
|
this.profiles.delete(pubkey)
|
|
this.pubkeys = new Set(Array.from(pubkeys))
|
|
},
|
|
async addRelay() {
|
|
let relay = String(this.inputRelay).trim()
|
|
if (!relay.startsWith('ws')) {
|
|
console.debug('invalid url')
|
|
return
|
|
}
|
|
this.relays.add(relay)
|
|
this.inputRelay = null
|
|
await this.initNostr()
|
|
},
|
|
removeRelay(relay) {
|
|
// Needs a hack for Vue reactivity
|
|
let relays = this.relays
|
|
relays.delete(relay)
|
|
this.relays = new Set(Array.from(relays))
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
{% endblock %}
|