Merge pull request #22 from lnbits/allow_multiple_images
Multiple fixes
This commit is contained in:
commit
efa5741445
6 changed files with 103 additions and 70 deletions
|
|
@ -5,13 +5,12 @@ async function chatDialog(path) {
|
||||||
name: 'chat-dialog',
|
name: 'chat-dialog',
|
||||||
template,
|
template,
|
||||||
|
|
||||||
props: ['account', 'merchant', 'relays'],
|
props: ['account', 'merchant', 'relays', 'pool'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
isChat: true,
|
isChat: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
pool: null,
|
|
||||||
nostrMessages: [],
|
nostrMessages: [],
|
||||||
newMessage: '',
|
newMessage: '',
|
||||||
ordersTable: {
|
ordersTable: {
|
||||||
|
|
@ -82,11 +81,10 @@ async function chatDialog(path) {
|
||||||
},
|
},
|
||||||
async closeDialog() {
|
async closeDialog() {
|
||||||
this.dialog = false
|
this.dialog = false
|
||||||
await this.pool.close(Array.from(this.relays))
|
await this.sub.unsub()
|
||||||
},
|
},
|
||||||
async startPool() {
|
async startPool() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.pool = new NostrTools.SimplePool()
|
|
||||||
let messagesMap = new Map()
|
let messagesMap = new Map()
|
||||||
let sub = this.pool.sub(Array.from(this.relays), [
|
let sub = this.pool.sub(Array.from(this.relays), [
|
||||||
{
|
{
|
||||||
|
|
@ -98,6 +96,7 @@ async function chatDialog(path) {
|
||||||
'#p': [this.account.pubkey]
|
'#p': [this.account.pubkey]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
sub.on('eose', () => {
|
sub.on('eose', () => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.nostrMessages = Array.from(messagesMap.values())
|
this.nostrMessages = Array.from(messagesMap.values())
|
||||||
|
|
@ -133,6 +132,7 @@ async function chatDialog(path) {
|
||||||
console.error('Unable to decrypt message!')
|
console.error('Unable to decrypt message!')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.sub = sub
|
||||||
},
|
},
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
if (this.newMessage && this.newMessage.length < 1) return
|
if (this.newMessage && this.newMessage.length < 1) return
|
||||||
|
|
@ -146,6 +146,10 @@ async function chatDialog(path) {
|
||||||
}
|
}
|
||||||
event.id = NostrTools.getEventHash(event)
|
event.id = NostrTools.getEventHash(event)
|
||||||
event.sig = this.signEvent(event)
|
event.sig = this.signEvent(event)
|
||||||
|
// This doesn't work yet
|
||||||
|
// this.pool.publish(Array.from(this.relays), event)
|
||||||
|
// this.newMessage = ''
|
||||||
|
// We need this hack
|
||||||
for (const url of Array.from(this.relays)) {
|
for (const url of Array.from(this.relays)) {
|
||||||
try {
|
try {
|
||||||
let relay = NostrTools.relayInit(url)
|
let relay = NostrTools.relayInit(url)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
:account="account ? account : dropIn"
|
:account="account ? account : dropIn"
|
||||||
:merchant="stall.pubkey"
|
:merchant="stall.pubkey"
|
||||||
:relays="relays"
|
:relays="relays"
|
||||||
|
:pool="pool"
|
||||||
/>
|
/>
|
||||||
<shopping-cart
|
<shopping-cart
|
||||||
:cart="cart"
|
:cart="cart"
|
||||||
|
|
@ -235,12 +236,7 @@
|
||||||
@click="copyText(qrCodeDialog.data.payment_request)"
|
@click="copyText(qrCodeDialog.data.payment_request)"
|
||||||
>Copy invoice</q-btn
|
>Copy invoice</q-btn
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn @click="closeQrCodeDialog" flat color="grey" class="q-ml-auto"
|
||||||
@click="closeQrCodeDialog"
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Close</q-btn
|
>Close</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ async function customerStall(path) {
|
||||||
'products',
|
'products',
|
||||||
'product-detail',
|
'product-detail',
|
||||||
'change-page',
|
'change-page',
|
||||||
'relays'
|
'relays',
|
||||||
|
'pool'
|
||||||
],
|
],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
|
@ -38,6 +39,7 @@ async function customerStall(path) {
|
||||||
data: {
|
data: {
|
||||||
payment_request: null
|
payment_request: null
|
||||||
},
|
},
|
||||||
|
dismissMsg: null,
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
downloadOrderDialog: {
|
downloadOrderDialog: {
|
||||||
|
|
@ -85,10 +87,17 @@ async function customerStall(path) {
|
||||||
return LNbits.utils.formatCurrency(amount, unit)
|
return LNbits.utils.formatCurrency(amount, unit)
|
||||||
},
|
},
|
||||||
addToCart(item) {
|
addToCart(item) {
|
||||||
console.log('add to cart', item)
|
|
||||||
let prod = this.cart.products
|
let prod = this.cart.products
|
||||||
if (prod.has(item.id)) {
|
if (prod.has(item.id)) {
|
||||||
let qty = prod.get(item.id).quantity
|
let qty = prod.get(item.id).quantity
|
||||||
|
if (qty == item.quantity) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: `${item.name} only has ${item.quantity} units!`,
|
||||||
|
icon: 'production_quantity_limits'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
prod.set(item.id, {
|
prod.set(item.id, {
|
||||||
...prod.get(item.id),
|
...prod.get(item.id),
|
||||||
quantity: qty + 1
|
quantity: qty + 1
|
||||||
|
|
@ -114,7 +123,6 @@ async function customerStall(path) {
|
||||||
this.updateCart(+item.price, true)
|
this.updateCart(+item.price, true)
|
||||||
},
|
},
|
||||||
updateCart(price, del = false) {
|
updateCart(price, del = false) {
|
||||||
console.log(this.cart, this.cartMenu)
|
|
||||||
if (del) {
|
if (del) {
|
||||||
this.cart.total -= price
|
this.cart.total -= price
|
||||||
this.cart.size--
|
this.cart.size--
|
||||||
|
|
@ -125,7 +133,10 @@ async function customerStall(path) {
|
||||||
this.cartMenu = Array.from(this.cart.products, item => {
|
this.cartMenu = Array.from(this.cart.products, item => {
|
||||||
return {id: item[0], ...item[1]}
|
return {id: item[0], ...item[1]}
|
||||||
})
|
})
|
||||||
console.log(this.cart, this.cartMenu)
|
this.$q.localStorage.set(`diagonAlley.carts.${this.stall.id}`, {
|
||||||
|
...this.cart,
|
||||||
|
products: Object.fromEntries(this.cart.products)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
resetCart() {
|
resetCart() {
|
||||||
this.cart = {
|
this.cart = {
|
||||||
|
|
@ -133,6 +144,7 @@ async function customerStall(path) {
|
||||||
size: 0,
|
size: 0,
|
||||||
products: new Map()
|
products: new Map()
|
||||||
}
|
}
|
||||||
|
this.$q.localStorage.remove(`diagonAlley.carts.${this.stall.id}`)
|
||||||
},
|
},
|
||||||
async downloadOrder() {
|
async downloadOrder() {
|
||||||
let created_at = Math.floor(Date.now() / 1000)
|
let created_at = Math.floor(Date.now() / 1000)
|
||||||
|
|
@ -181,6 +193,7 @@ async function customerStall(path) {
|
||||||
this.checkoutDialog.show = true
|
this.checkoutDialog.show = true
|
||||||
},
|
},
|
||||||
resetCheckout() {
|
resetCheckout() {
|
||||||
|
this.loading = false
|
||||||
this.checkoutDialog = {
|
this.checkoutDialog = {
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -188,8 +201,22 @@ async function customerStall(path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openQrCodeDialog() {
|
||||||
|
this.qrCodeDialog = {
|
||||||
|
data: {
|
||||||
|
payment_request: null
|
||||||
|
},
|
||||||
|
dismissMsg: this.$q.notify({
|
||||||
|
message: 'Waiting for invoice from merchant...'
|
||||||
|
}),
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
closeQrCodeDialog() {
|
closeQrCodeDialog() {
|
||||||
this.qrCodeDialog.show = false
|
this.qrCodeDialog.show = false
|
||||||
|
setTimeout(() => {
|
||||||
|
this.qrCodeDialog.dismissMsg()
|
||||||
|
}, 1000)
|
||||||
},
|
},
|
||||||
async placeOrder() {
|
async placeOrder() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
@ -247,9 +274,6 @@ async function customerStall(path) {
|
||||||
await this.sendOrder(event)
|
await this.sendOrder(event)
|
||||||
},
|
},
|
||||||
async sendOrder(order) {
|
async sendOrder(order) {
|
||||||
this.$q.notify({
|
|
||||||
message: 'Waiting for invoice from merchant...'
|
|
||||||
})
|
|
||||||
for (const url of Array.from(this.relays)) {
|
for (const url of Array.from(this.relays)) {
|
||||||
try {
|
try {
|
||||||
let relay = NostrTools.relayInit(url)
|
let relay = NostrTools.relayInit(url)
|
||||||
|
|
@ -278,7 +302,7 @@ async function customerStall(path) {
|
||||||
}
|
}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.resetCart()
|
this.resetCart()
|
||||||
this.qrCodeDialog.show = true
|
this.openQrCodeDialog()
|
||||||
this.listenMessages()
|
this.listenMessages()
|
||||||
},
|
},
|
||||||
async listenMessages() {
|
async listenMessages() {
|
||||||
|
|
@ -319,7 +343,7 @@ async function customerStall(path) {
|
||||||
|
|
||||||
this.messageFilter(plaintext, cb => Promise.resolve(pool.close))
|
this.messageFilter(plaintext, cb => Promise.resolve(pool.close))
|
||||||
} catch {
|
} catch {
|
||||||
console.error('Unable to decrypt message!')
|
console.debug('Unable to decrypt message! Probably not for us!')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -331,9 +355,8 @@ async function customerStall(path) {
|
||||||
let json = JSON.parse(text)
|
let json = JSON.parse(text)
|
||||||
if (json.id != this.activeOrder) return
|
if (json.id != this.activeOrder) return
|
||||||
if (json.payment_options) {
|
if (json.payment_options) {
|
||||||
let payment_request = json.payment_options.find(
|
let payment_request = json.payment_options.find(o => o.type == 'ln')
|
||||||
o => o.type == 'ln'
|
.link
|
||||||
).link
|
|
||||||
if (!payment_request) return
|
if (!payment_request) return
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.qrCodeDialog.data.payment_request = payment_request
|
this.qrCodeDialog.data.payment_request = payment_request
|
||||||
|
|
@ -359,6 +382,18 @@ async function customerStall(path) {
|
||||||
this.customerPubkey = this.account?.pubkey
|
this.customerPubkey = this.account?.pubkey
|
||||||
this.customerPrivkey = this.account?.privkey
|
this.customerPrivkey = this.account?.privkey
|
||||||
this.customerUseExtension = this.account?.useExtension
|
this.customerUseExtension = this.account?.useExtension
|
||||||
|
let storedCart = this.$q.localStorage.getItem(
|
||||||
|
`diagonAlley.carts.${this.stall.id}`
|
||||||
|
)
|
||||||
|
if (storedCart) {
|
||||||
|
this.cart.total = storedCart.total
|
||||||
|
this.cart.size = storedCart.size
|
||||||
|
this.cart.products = new Map(Object.entries(storedCart.products))
|
||||||
|
|
||||||
|
this.cartMenu = Array.from(this.cart.products, item => {
|
||||||
|
return {id: item[0], ...item[1]}
|
||||||
|
})
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.nostr) {
|
if (window.nostr) {
|
||||||
this.hasNip07 = true
|
this.hasNip07 = true
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@
|
||||||
View details
|
View details
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="!isStall"
|
||||||
flat
|
flat
|
||||||
class="text-weight-bold text-capitalize q-ml-auto"
|
class="text-weight-bold text-capitalize q-ml-auto"
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,16 @@ const market = async () => {
|
||||||
]
|
]
|
||||||
const eventToObj = event => {
|
const eventToObj = event => {
|
||||||
event.content = JSON.parse(event.content) || null
|
event.content = JSON.parse(event.content) || null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...event,
|
...event,
|
||||||
...Object.values(event.tags).reduce((acc, tag) => {
|
...Object.values(event.tags).reduce((acc, tag) => {
|
||||||
let [key, value] = tag
|
let [key, value] = tag
|
||||||
|
if (key == 't') {
|
||||||
return {...acc, [key]: [...(acc[key] || []), value]}
|
return {...acc, [key]: [...(acc[key] || []), value]}
|
||||||
|
} else {
|
||||||
|
return {...acc, [key]: value}
|
||||||
|
}
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +60,8 @@ const market = async () => {
|
||||||
inputRelay: null,
|
inputRelay: null,
|
||||||
activePage: 'market',
|
activePage: 'market',
|
||||||
activeStall: null,
|
activeStall: null,
|
||||||
activeProduct: null
|
activeProduct: null,
|
||||||
|
pool: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -209,13 +215,18 @@ const market = async () => {
|
||||||
this.products.forEach(p => products.set(p.id, p))
|
this.products.forEach(p => products.set(p.id, p))
|
||||||
|
|
||||||
events.map(eventToObj).map(e => {
|
events.map(eventToObj).map(e => {
|
||||||
if (e.kind == 30018) {
|
if (e.kind == 0) {
|
||||||
|
this.profiles.set(e.pubkey, e.content)
|
||||||
|
if (e.pubkey == this.account?.pubkey) {
|
||||||
|
this.accountMetadata = this.profiles.get(this.account.pubkey)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if (e.kind == 30018) {
|
||||||
//it's a product `d` is the prod. id
|
//it's a product `d` is the prod. id
|
||||||
products.set(e.d, {...e.content, id: e.d[0], categories: e.t})
|
products.set(e.d, {...e.content, id: e.d, categories: e.t})
|
||||||
} else if (e.kind == 30017) {
|
} else if (e.kind == 30017) {
|
||||||
// it's a stall `d` is the stall id
|
// it's a stall `d` is the stall id
|
||||||
stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey})
|
stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -226,7 +237,7 @@ const market = async () => {
|
||||||
let stall = this.stalls.find(s => s.id == obj.stall_id)
|
let stall = this.stalls.find(s => s.id == obj.stall_id)
|
||||||
if (!stall) return
|
if (!stall) return
|
||||||
obj.stallName = stall.name
|
obj.stallName = stall.name
|
||||||
obj.images = [obj.image]
|
obj.images = obj.images || [obj.image]
|
||||||
if (obj.currency != 'sat') {
|
if (obj.currency != 'sat') {
|
||||||
obj.formatedPrice = this.getAmountFormated(
|
obj.formatedPrice = this.getAmountFormated(
|
||||||
obj.price,
|
obj.price,
|
||||||
|
|
@ -241,10 +252,9 @@ const market = async () => {
|
||||||
this.$q.loading.show()
|
this.$q.loading.show()
|
||||||
const pool = new NostrTools.SimplePool()
|
const pool = new NostrTools.SimplePool()
|
||||||
let relays = Array.from(this.relays)
|
let relays = Array.from(this.relays)
|
||||||
let products = new Map()
|
|
||||||
let stalls = new Map()
|
|
||||||
// Get metadata and market data from the pubkeys
|
// Get metadata and market data from the pubkeys
|
||||||
let sub = await pool
|
await pool
|
||||||
.list(relays, [
|
.list(relays, [
|
||||||
{
|
{
|
||||||
kinds: [0, 30017, 30018], // for production kind is 30017
|
kinds: [0, 30017, 30018], // for production kind is 30017
|
||||||
|
|
@ -252,46 +262,31 @@ const market = async () => {
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
.then(async events => {
|
.then(async events => {
|
||||||
this.events = events || []
|
if (!events || events.length == 0) return
|
||||||
this.events.map(eventToObj).map(e => {
|
await this.updateData(events)
|
||||||
if (e.kind == 0) {
|
|
||||||
this.profiles.set(e.pubkey, e.content)
|
|
||||||
if (e.pubkey == this.account?.pubkey) {
|
|
||||||
this.accountMetadata = this.profiles.get(this.account.pubkey)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else if (e.kind == 30018) {
|
|
||||||
//it's a product `d` is the prod. id
|
|
||||||
products.set(e.d, {...e.content, id: e.d[0], categories: e.t})
|
|
||||||
} else if (e.kind == 30017) {
|
|
||||||
// it's a stall `d` is the stall id
|
|
||||||
stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
await Promise.resolve(sub)
|
|
||||||
this.stalls = await Array.from(stalls.values())
|
|
||||||
|
|
||||||
this.products = Array.from(products.values())
|
|
||||||
.map(obj => {
|
|
||||||
let stall = this.stalls.find(s => s.id == obj.stall_id)
|
|
||||||
if (!stall) return
|
|
||||||
obj.stallName = stall.name
|
|
||||||
obj.images = [obj.image]
|
|
||||||
if (obj.currency != 'sat') {
|
|
||||||
obj.formatedPrice = this.getAmountFormated(
|
|
||||||
obj.price,
|
|
||||||
obj.currency
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
.filter(f => f)
|
|
||||||
this.$q.loading.hide()
|
this.$q.loading.hide()
|
||||||
pool.close(relays)
|
this.pool = pool
|
||||||
|
this.poolSubscribe()
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
async poolSubscribe() {
|
||||||
|
this.poolSub = this.pool.sub(Array.from(this.relays), [
|
||||||
|
{
|
||||||
|
kinds: [0, 30017, 30018],
|
||||||
|
authors: Array.from(this.pubkeys),
|
||||||
|
since: Date.now() / 1000
|
||||||
|
}
|
||||||
|
])
|
||||||
|
this.poolSub.on(
|
||||||
|
'event',
|
||||||
|
event => {
|
||||||
|
this.updateData([event])
|
||||||
|
},
|
||||||
|
{id: 'masterSub'} //pass ID to cancel previous sub
|
||||||
|
)
|
||||||
|
},
|
||||||
navigateTo(page, opts = {stall: null, product: null, pubkey: null}) {
|
navigateTo(page, opts = {stall: null, product: null, pubkey: null}) {
|
||||||
let {stall, product, pubkey} = opts
|
let {stall, product, pubkey} = opts
|
||||||
let url = new URL(window.location)
|
let url = new URL(window.location)
|
||||||
|
|
@ -356,7 +351,7 @@ const market = async () => {
|
||||||
`diagonAlley.merchants`,
|
`diagonAlley.merchants`,
|
||||||
Array.from(this.pubkeys)
|
Array.from(this.pubkeys)
|
||||||
)
|
)
|
||||||
await this.initNostr()
|
this.initNostr()
|
||||||
},
|
},
|
||||||
removePubkey(pubkey) {
|
removePubkey(pubkey) {
|
||||||
// Needs a hack for Vue reactivity
|
// Needs a hack for Vue reactivity
|
||||||
|
|
@ -368,7 +363,7 @@ const market = async () => {
|
||||||
`diagonAlley.merchants`,
|
`diagonAlley.merchants`,
|
||||||
Array.from(this.pubkeys)
|
Array.from(this.pubkeys)
|
||||||
)
|
)
|
||||||
Promise.resolve(this.initNostr())
|
this.initNostr()
|
||||||
},
|
},
|
||||||
async addRelay() {
|
async addRelay() {
|
||||||
let relay = String(this.inputRelay).trim()
|
let relay = String(this.inputRelay).trim()
|
||||||
|
|
@ -379,7 +374,7 @@ const market = async () => {
|
||||||
this.relays.add(relay)
|
this.relays.add(relay)
|
||||||
this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays))
|
this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays))
|
||||||
this.inputRelay = null
|
this.inputRelay = null
|
||||||
await this.initNostr()
|
this.initNostr()
|
||||||
},
|
},
|
||||||
removeRelay(relay) {
|
removeRelay(relay) {
|
||||||
// Needs a hack for Vue reactivity
|
// Needs a hack for Vue reactivity
|
||||||
|
|
@ -387,6 +382,7 @@ const market = async () => {
|
||||||
relays.delete(relay)
|
relays.delete(relay)
|
||||||
this.relays = new Set(Array.from(relays))
|
this.relays = new Set(Array.from(relays))
|
||||||
this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays))
|
this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays))
|
||||||
|
this.initNostr()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@
|
||||||
:product-detail="activeProduct"
|
:product-detail="activeProduct"
|
||||||
:relays="relays"
|
:relays="relays"
|
||||||
:account="account"
|
:account="account"
|
||||||
|
:pool="pool"
|
||||||
@change-page="navigateTo"
|
@change-page="navigateTo"
|
||||||
></customer-stall>
|
></customer-stall>
|
||||||
<customer-market
|
<customer-market
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue