nostrmarket/templates/nostrmarket/market.html
2023-03-09 10:01:28 +00:00

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 %}