Extract market place (#78)
* chore: remove marketplace components * feat: add static marketplace * feat: add entry point for static marketplace * doc: add comment * chore: include nostr-bundle.js
This commit is contained in:
parent
8ebe2fe458
commit
a3299b63c4
60 changed files with 18008 additions and 3197 deletions
|
|
@ -1,95 +0,0 @@
|
|||
<div class="q-mr-md">
|
||||
<q-btn dense round flat icon="chat" @click="startDialog" />
|
||||
<q-dialog
|
||||
v-model="dialog"
|
||||
maximized
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
>
|
||||
<q-card>
|
||||
<q-bar>
|
||||
<q-btn dense flat icon="chat" label="Chat" @click="isChat = true" />
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
icon="receipt_long"
|
||||
label="Orders"
|
||||
@click="isChat = false"
|
||||
/>
|
||||
|
||||
<q-space></q-space>
|
||||
|
||||
<q-btn dense flat icon="close" @click="closeDialog">
|
||||
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div v-if="isChat">
|
||||
<q-card-section
|
||||
class="q-ml-auto"
|
||||
style="
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
height: calc(100vh - 120px);
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
"
|
||||
>
|
||||
<q-chat-message
|
||||
:key="index"
|
||||
v-for="(message, index) in sortedMessages"
|
||||
:name="message.sender"
|
||||
:text="[message.msg]"
|
||||
:sent="message.sender == 'Me'"
|
||||
:bg-color="message.json ? 'purple-2' : message.sender == 'Me' ? 'white' : 'light-green-2'"
|
||||
:stamp="message.timestamp"
|
||||
size="6"
|
||||
><template v-slot:avatar v-if="message.json">
|
||||
<q-icon
|
||||
color="secondary"
|
||||
name="smart_toy"
|
||||
class="q-message-avatar q-message-avatar--received"
|
||||
/> </template
|
||||
></q-chat-message>
|
||||
</q-card-section>
|
||||
<q-card-actions>
|
||||
<q-form @submit="sendMessage" class="full-width chat-input">
|
||||
<q-input
|
||||
ref="newMessage"
|
||||
v-model="newMessage"
|
||||
placeholder="Message"
|
||||
class="full-width"
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
<template>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
type="submit"
|
||||
icon="send"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</q-card-actions>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
title="Orders"
|
||||
:data="ordersList"
|
||||
:columns="ordersTable.columns"
|
||||
:pagination.sync="ordersTable.pagination"
|
||||
row-key="id"
|
||||
/>
|
||||
</q-card-section>
|
||||
</div>
|
||||
<q-inner-loading :showing="loading">
|
||||
<q-spinner-comment size="50px" color="primary" />
|
||||
</q-inner-loading>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
async function chatDialog(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('chat-dialog', {
|
||||
name: 'chat-dialog',
|
||||
template,
|
||||
|
||||
props: ['account', 'merchant', 'relays', 'pool'],
|
||||
data: function () {
|
||||
return {
|
||||
dialog: false,
|
||||
isChat: true,
|
||||
loading: false,
|
||||
messagesMap: new Map(),
|
||||
nostrMessages: [],
|
||||
newMessage: '',
|
||||
ordersTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
align: 'left',
|
||||
label: 'ID',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
align: 'left',
|
||||
label: 'Created/Updated',
|
||||
field: 'created_at',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'paid',
|
||||
align: 'left',
|
||||
label: 'Paid',
|
||||
field: 'paid',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'shipped',
|
||||
align: 'left',
|
||||
label: 'Shipped',
|
||||
field: 'shipped',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'invoice',
|
||||
align: 'left',
|
||||
label: 'Invoice',
|
||||
field: row =>
|
||||
row.payment_options &&
|
||||
row.payment_options.find(p => p.type == 'ln')?.link
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sortedMessages() {
|
||||
return this.nostrMessages.sort((a, b) => b.created_at - a.created_at)
|
||||
},
|
||||
ordersList() {
|
||||
let orders = this.nostrMessages
|
||||
.filter(o => o.json)
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
.reduce((acc, cur) => {
|
||||
const obj = cur.json
|
||||
const key = obj.id
|
||||
const curGroup = acc[key] ?? {created_at: cur.timestamp}
|
||||
return {...acc, [key]: {...curGroup, ...obj}}
|
||||
}, {})
|
||||
|
||||
return Object.values(orders)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async startDialog() {
|
||||
this.dialog = true
|
||||
await this.startPool()
|
||||
},
|
||||
async closeDialog() {
|
||||
this.dialog = false
|
||||
await this.sub.unsub()
|
||||
},
|
||||
async startPool() {
|
||||
this.loading = true
|
||||
let relays = Array.from(this.relays)
|
||||
let filters = [
|
||||
{
|
||||
kinds: [4],
|
||||
authors: [this.account.pubkey]
|
||||
},
|
||||
{
|
||||
kinds: [4],
|
||||
'#p': [this.account.pubkey]
|
||||
}
|
||||
]
|
||||
let events = await this.pool.list(relays, filters)
|
||||
|
||||
for (const event of events) {
|
||||
await this.processMessage(event)
|
||||
}
|
||||
|
||||
this.nostrMessages = Array.from(this.messagesMap.values())
|
||||
this.loading = false
|
||||
|
||||
let sub = this.pool.sub(
|
||||
relays,
|
||||
filters.map(f => ({...f, since: Date.now() / 1000}))
|
||||
)
|
||||
sub.on('event', async event => {
|
||||
await this.processMessage(event)
|
||||
this.nostrMessages = Array.from(this.messagesMap.values())
|
||||
})
|
||||
this.sub = sub
|
||||
},
|
||||
async processMessage(event) {
|
||||
let mine = event.pubkey == this.account.pubkey
|
||||
let sender = mine ? this.merchant : event.pubkey
|
||||
|
||||
try {
|
||||
let plaintext
|
||||
if (this.account.privkey) {
|
||||
plaintext = await NostrTools.nip04.decrypt(
|
||||
this.account.privkey,
|
||||
sender,
|
||||
event.content
|
||||
)
|
||||
} else if (this.account.useExtension && this.hasNip07) {
|
||||
plaintext = await window.nostr.nip04.decrypt(sender, event.content)
|
||||
}
|
||||
if (plaintext) {
|
||||
let {text, json} = this.filterJsonMsg(plaintext)
|
||||
this.messagesMap.set(event.id, {
|
||||
created_at: event.created_at,
|
||||
msg: text,
|
||||
timestamp: timeFromNow(event.created_at * 1000),
|
||||
sender: `${mine ? 'Me' : 'Merchant'}`,
|
||||
json
|
||||
})
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
console.debug('Unable to decrypt message! Not for us...')
|
||||
return null
|
||||
}
|
||||
},
|
||||
filterJsonMsg(text) {
|
||||
let json = null
|
||||
|
||||
if (!isJson(text)) return {text, json}
|
||||
|
||||
json = JSON.parse(text)
|
||||
if (json.message) {
|
||||
text = json.message
|
||||
} else if (json.items) {
|
||||
text = `Order placed!<br />OrderID: ${json.id}`
|
||||
} else if (json.payment_options) {
|
||||
text = `Invoice for order: ${json.id} <br /><a href="lightning:${
|
||||
json.payment_options.find(o => o.type == 'ln')?.link
|
||||
}" target="_blank" class="text-secondary">LN ⚡︎</a>`
|
||||
}
|
||||
|
||||
return {text, json}
|
||||
},
|
||||
async sendMessage() {
|
||||
if (this.newMessage && this.newMessage.length < 1) return
|
||||
let event = {
|
||||
...(await NostrTools.getBlankEvent()),
|
||||
kind: 4,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', this.merchant]],
|
||||
pubkey: this.account.pubkey,
|
||||
content: await this.encryptMsg()
|
||||
}
|
||||
event.id = NostrTools.getEventHash(event)
|
||||
event.sig = this.signEvent(event)
|
||||
|
||||
let pub = this.pool.publish(Array.from(this.relays), event)
|
||||
pub.on('ok', () => console.debug(`Event was sent`))
|
||||
pub.on('failed', error => console.error(error))
|
||||
this.newMessage = ''
|
||||
},
|
||||
async encryptMsg() {
|
||||
try {
|
||||
let cypher
|
||||
if (this.account.privkey) {
|
||||
cypher = await NostrTools.nip04.encrypt(
|
||||
this.account.privkey,
|
||||
this.merchant,
|
||||
this.newMessage
|
||||
)
|
||||
} else if (this.account.useExtension && this.hasNip07) {
|
||||
cypher = await window.nostr.nip04.encrypt(
|
||||
this.merchant,
|
||||
this.newMessage
|
||||
)
|
||||
}
|
||||
return cypher
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
async signEvent(event) {
|
||||
if (this.account.privkey) {
|
||||
event.sig = await NostrTools.signEvent(event, this.account.privkey)
|
||||
} else if (this.account.useExtension && this.hasNip07) {
|
||||
event = await window.nostr.signEvent(event)
|
||||
}
|
||||
return event
|
||||
}
|
||||
},
|
||||
created() {
|
||||
setTimeout(() => {
|
||||
if (window.nostr) {
|
||||
this.hasNip07 = true
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<div>
|
||||
|
||||
|
||||
<q-infinite-scroll v-if="showProducts" @load="onLoad" :offset="250">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-3" v-for="(item, idx) in partialProducts" :key="idx">
|
||||
<product-card :product="item" @change-page="changePageM" @add-to-cart="addToCart"></product-card>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="lastProductIndex < filteredProducts.length" v-slot:loading>
|
||||
<div class="row justify-center q-my-md">
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
</q-infinite-scroll>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
async function customerMarket(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('customer-market', {
|
||||
name: 'customer-market',
|
||||
template,
|
||||
|
||||
props: ['filtered-products', 'search-text', 'filter-categories'],
|
||||
data: function () {
|
||||
return {
|
||||
search: null,
|
||||
partialProducts: [],
|
||||
productsPerPage: 12,
|
||||
startIndex: 0,
|
||||
lastProductIndex: 0,
|
||||
showProducts: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchText: function () {
|
||||
this.refreshProducts()
|
||||
},
|
||||
filteredProducts: function () {
|
||||
this.refreshProducts()
|
||||
},
|
||||
filterCategories: function () {
|
||||
this.refreshProducts()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshProducts: function () {
|
||||
this.showProducts = false
|
||||
this.partialProducts = []
|
||||
|
||||
this.startIndex = 0
|
||||
this.lastProductIndex = Math.min(this.filteredProducts.length, this.productsPerPage)
|
||||
this.partialProducts.push(...this.filteredProducts.slice(0, this.lastProductIndex))
|
||||
|
||||
setTimeout(() => { this.showProducts = true }, 0)
|
||||
},
|
||||
|
||||
addToCart(item) {
|
||||
this.$emit('add-to-cart', item)
|
||||
},
|
||||
changePageM(page, opts) {
|
||||
this.$emit('change-page', page, opts)
|
||||
},
|
||||
|
||||
onLoad(_, done) {
|
||||
setTimeout(() => {
|
||||
if (this.startIndex >= this.filteredProducts.length) {
|
||||
done()
|
||||
return
|
||||
}
|
||||
this.startIndex = this.lastProductIndex
|
||||
this.lastProductIndex = Math.min(this.filteredProducts.length, this.lastProductIndex + this.productsPerPage)
|
||||
this.partialProducts.push(...this.filteredProducts.slice(this.startIndex, this.lastProductIndex))
|
||||
done()
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.lastProductIndex = Math.min(this.filteredProducts.length, 24)
|
||||
this.partialProducts.push(...this.filteredProducts.slice(0, this.lastProductIndex))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
<div>
|
||||
<q-card v-if="!merchantOrders?.length" bordered class="q-mb-md">
|
||||
<q-card-section>
|
||||
<strong>No orders!</strong>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div v-for="merchant in merchantOrders">
|
||||
<q-card bordered class="q-mb-md">
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img v-if="merchant.profile?.picture" :src="merchant.profile?.picture">
|
||||
<img v-else src="/nostrmarket/static/images/blank-avatar.webp">
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>
|
||||
<span v-text="merchant.profile?.name"></span>
|
||||
</strong>
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
<span v-text="merchant.pubkey"></span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
|
||||
|
||||
<q-card-section class="col-12">
|
||||
<q-list>
|
||||
<div v-for="order in merchant.orders" :key="order.id" class="q-mb-md">
|
||||
<q-expansion-item dense expand-separator>
|
||||
|
||||
<template v-slot:header>
|
||||
<q-item-section class="q-mt-sm">
|
||||
<q-item-label><strong> <span v-text="order.stallName"></span> </strong>
|
||||
<q-badge @click="showInvoice(order)" v-if="order.invoice?.human_readable_part?.amount"
|
||||
color="orange" class="q-ml-lg">
|
||||
<span v-text="formatCurrency(order.invoice?.human_readable_part?.amount / 1000, 'sat')"></span>
|
||||
</q-badge></q-item-label>
|
||||
|
||||
<q-item-label>
|
||||
<div class="text-caption text-grey ellipsis-2-lines">
|
||||
<p v-if="order.createdAt"><span v-text="fromNow(order.createdAt)"></span></p>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label>
|
||||
|
||||
<q-badge :color="order.paid ? 'green' : 'gray'"><span
|
||||
v-text="order.paid ? 'Paid' : 'Not Paid'"></span></q-badge>
|
||||
<q-badge :color="order.shipped ? 'green' : 'gray'"><span
|
||||
v-text="order.shipped ? 'Shipped' : 'Not Shipped'"></span></q-badge>
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
<div class="text-caption text-grey ellipsis-2-lines">
|
||||
<p>
|
||||
<span v-text="order.items?.length"></span>
|
||||
<span v-text="order.items?.length === 1 ? 'product': 'products'"></span>
|
||||
</p>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</template>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section class="col-12">
|
||||
<q-item-section>
|
||||
<q-item-label> <strong>Order ID: </strong> <span v-text="order.id"></span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section horizontal>
|
||||
|
||||
<q-card-section class="col-7">
|
||||
<q-item-section class="q-mt-sm">
|
||||
<q-item-label> <strong>Products</strong></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item v-for="product in order.products" :key="product.id">
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img v-if="product.images && product.images[0] || product.image"
|
||||
:src="product.images[0] || product.image" />
|
||||
<img v-else src="/nostrmarket/static/images/placeholder.png" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-sm">
|
||||
<q-item-label></q-item-label>
|
||||
<q-item-label>
|
||||
<strong>{{ product.orderedQuantity}} x {{ product.name}} </strong></q-item-label>
|
||||
<q-item-label>
|
||||
<div class="text-caption text-grey ellipsis-2-lines">
|
||||
<p>{{ product.description }}</p>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
</q-card-section>
|
||||
<q-separator vertical />
|
||||
<q-card-section>
|
||||
<q-item-section class="q-mt-md q-ml-sm">
|
||||
<q-item-label> <strong>Shipping Zone: </strong>
|
||||
<span v-text="order.shippingZone?.name || ''"></span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section v-if="order.message" class="q-mt-md q-ml-sm">
|
||||
<q-item-label> <strong>Message: </strong>
|
||||
<span v-text="order.message"></span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-md q-ml-sm">
|
||||
<q-item-label> <strong>Invoice: </strong>
|
||||
<q-badge @click="showInvoice(order)" v-if="order.invoice?.human_readable_part?.amount"
|
||||
color="orange" class="cursor-pointer">
|
||||
<span v-text="formatCurrency(order.invoice?.human_readable_part?.amount / 1000, 'sat')"></span>
|
||||
</q-badge>
|
||||
<!-- <q-badge v-if="!order.paid">Request Invoice Again</q-badge> -->
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator class="q-mb-xl"></q-separator>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
</div>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
|
||||
|
||||
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
async function customerOrders(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('customer-orders', {
|
||||
name: 'orders',
|
||||
template,
|
||||
|
||||
props: ['orders', 'products', 'stalls', 'merchants'],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
merchantOrders: function () {
|
||||
return Object.keys(this.orders)
|
||||
.map(pubkey => ({
|
||||
pubkey,
|
||||
profile: this.merchantProfile(pubkey),
|
||||
orders: this.orders[pubkey].map(this.enrichOrder)
|
||||
}
|
||||
))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
enrichOrder: function (order) {
|
||||
const stall = this.stallForOrder(order)
|
||||
return {
|
||||
...order,
|
||||
stallName: stall?.name || 'Stall',
|
||||
shippingZone: stall?.shipping?.find(s => s.id === order.shipping_id) || { id: order.shipping_id, name: order.shipping_id },
|
||||
invoice: this.invoiceForOrder(order),
|
||||
products: this.getProductsForOrder(order)
|
||||
}
|
||||
},
|
||||
stallForOrder: function (order) {
|
||||
try {
|
||||
const productId = order.items && order.items[0]?.product_id
|
||||
if (!productId) return
|
||||
const product = this.products.find(p => p.id === productId)
|
||||
if (!product) return
|
||||
const stall = this.stalls.find(s => s.id === product.stall_id)
|
||||
if (!stall) return
|
||||
return stall
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
},
|
||||
invoiceForOrder: function (order) {
|
||||
try {
|
||||
const lnPaymentOption = order?.payment_options?.find(p => p.type === 'ln')
|
||||
if (!lnPaymentOption?.link) return
|
||||
return decode(lnPaymentOption.link)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
|
||||
merchantProfile: function (pubkey) {
|
||||
const merchant = this.merchants.find(m => m.publicKey === pubkey)
|
||||
return merchant?.profile
|
||||
},
|
||||
|
||||
getProductsForOrder: function (order) {
|
||||
if (!order?.items?.length) return []
|
||||
|
||||
return order.items.map(i => {
|
||||
const product = this.products.find(p => p.id === i.product_id) || { id: i.product_id, name: i.product_id }
|
||||
return {
|
||||
...product,
|
||||
orderedQuantity: i.quantity
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
showInvoice: function (order) {
|
||||
if (order.paid) return
|
||||
const invoice = order?.payment_options?.find(p => p.type === 'ln').link
|
||||
if (!invoice) return
|
||||
this.$emit('show-invoice', invoice)
|
||||
},
|
||||
|
||||
formatCurrency: function (value, unit) {
|
||||
return formatCurrency(value, unit)
|
||||
},
|
||||
|
||||
fromNow: function (date) {
|
||||
if (!date) return ''
|
||||
return moment(date * 1000).fromNow()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<div v-if="showStalls" class="row q-col-gutter-md">
|
||||
<div v-for="stall of stalls" :key="stall.id" class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
|
||||
<q-card class="card--product">
|
||||
<q-card-section class="q-pb-xs q-pt-md">
|
||||
<div class="q-pa-md q-gutter-sm" style="height: 80px">
|
||||
<q-avatar v-for="(image, i) of stall.images" :key="i" size="40px" class="overlapping"
|
||||
:style="`left: ${i * 25}px; border: 2px solid white; position: absolute`">
|
||||
<img :src="image">
|
||||
</q-avatar>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pb-xs q-pt-md">
|
||||
<div class="row no-wrap items-center">
|
||||
<div class="col text-subtitle2 ellipsis-2-lines">{{ stall.name }}</div>
|
||||
</div>
|
||||
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-pl-sm">
|
||||
<div>
|
||||
<span class="text-caption text-green-8 text-weight-bolder q-mt-md"><span v-text="stall.productCount"></span>
|
||||
products</span>
|
||||
<span v-text="stall.currency" class="float-right"></span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pl-sm">
|
||||
<div v-if="stall.categories" class="text-subtitle1">
|
||||
<q-virtual-scroll :items="stall.categories || []" virtual-scroll-horizontal>
|
||||
<template v-slot="{ item, index }">
|
||||
<q-chip :key="index" dense><span v-text="item"></span></q-chip>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
</div>
|
||||
<div v-else class="text-subtitle1">
|
||||
 
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey ellipsis-2-lines" style="min-height: 40px">
|
||||
<p>{{ stall.description || '' }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
</q-card-section>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-card-actions>
|
||||
<div class="q-ml-auto">
|
||||
<q-btn flat class="text-weight-bold text-capitalize q-ml-auto float-left" dense color="primary"
|
||||
@click="$emit('change-page', 'stall', {stall: stall.id})">
|
||||
Visit Stall
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
async function customerStallList(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('customer-stall-list', {
|
||||
name: 'customer-stall-list',
|
||||
template,
|
||||
|
||||
props: ['stalls'],
|
||||
data: function () {
|
||||
return {
|
||||
showStalls: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
stalls() {
|
||||
this.showProducts = false
|
||||
setTimeout(() => { this.showProducts = true }, 0)
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
|
||||
},
|
||||
created() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<div>
|
||||
<div id="product-focus-area"></div>
|
||||
<div v-if="productDetail && product" class="row">
|
||||
<div class="col-12 auto-width">
|
||||
<product-detail :product="product" @add-to-cart="addToCart"></product-detail>
|
||||
</div>
|
||||
|
||||
<div class="col-12 q-my-lg">
|
||||
<q-separator></q-separator>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-3" v-for="(item, idx) in products" :key="idx">
|
||||
<product-card :product="item" @change-page="changePageS" @add-to-cart="addToCart" :is-stall="true"></product-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
async function customerStall(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('customer-stall', {
|
||||
name: 'customer-stall',
|
||||
template,
|
||||
|
||||
props: [
|
||||
'stall',
|
||||
'products',
|
||||
'product-detail',
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
product() {
|
||||
if (this.productDetail) {
|
||||
return this.products.find(p => p.id == this.productDetail)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changePageS(page, opts) {
|
||||
if (page === 'stall' && opts?.product) {
|
||||
document.getElementById('product-focus-area')?.scrollIntoView()
|
||||
}
|
||||
this.$emit('change-page', page, opts)
|
||||
},
|
||||
addToCart(item) {
|
||||
this.$emit('add-to-cart', item)
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="q-pt-md">
|
||||
<div class="q-gutter-y-md">
|
||||
<q-tabs v-model="tab" active-color="primary" align="justify">
|
||||
<q-tab name="merchants" label="Merchants" @update="val => tab = val.name"></q-tab>
|
||||
<q-tab name="relays" label="Relays" @update="val => tab = val.name"></q-tab>
|
||||
<q-tab name="marketplace" label="Look And Feel" @update="val => tab = val.name"></q-tab>
|
||||
|
||||
</q-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-y-md">
|
||||
|
||||
<q-tab-panels v-model="tab">
|
||||
<q-tab-panel name="merchants">
|
||||
<q-list v-if="!readNotes.merchants" class="q-mb-lg" bordered>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<q-icon color="primary" name="info" size="xl" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-sm q-ml-lg">
|
||||
<q-item-label><strong>Note</strong></q-item-label>
|
||||
<q-item-label>
|
||||
<div class="text-caption">
|
||||
<ul>
|
||||
<li><span class="text-subtitle1">
|
||||
Here all the mercants of the marketplace are listed.
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-subtitle1">
|
||||
You can easily add a new merchant by
|
||||
entering its public key in the input below.
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-subtitle1">
|
||||
When a merchant is added all its products and stalls will be available in the Market page.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn @click="markNoteAsRead('merchants')" size="lg" outline color="primary" label="Got it!"
|
||||
icon="check_small" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div>
|
||||
<q-input outlined v-model="merchantPubkey" @keydown.enter="addMerchant" type="text" label="Pubkey/Npub"
|
||||
hint="Add merchants">
|
||||
<q-btn @click="addMerchant" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<q-list class="q-mt-md">
|
||||
<q-item v-for="{publicKey, profile} in merchants" :key="publicKey">
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img v-if="profile?.picture" :src="profile.picture" />
|
||||
<img v-else src="/nostrmarket/static/images/blank-avatar.webp" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-sm">
|
||||
<q-item-label><strong>{{ profile?.name}}</strong></q-item-label>
|
||||
<q-item-label>
|
||||
<div class="text-caption text-grey ellipsis-2-lines">
|
||||
<p>{{ publicKey }}</p>
|
||||
</div>
|
||||
</q-item-label>
|
||||
<q-tooltip>{{ publicKey }}</q-tooltip>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn size="12px" flat dense round icon="delete" @click="removeMerchant(publicKey)" />
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="relays">
|
||||
<div>
|
||||
<div>
|
||||
<q-input outlined v-model="relayUrl" @keydown.enter="addRelay" type="text" label="wss://"
|
||||
hint="Add realays">
|
||||
<q-btn @click="addRelay" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<q-list class="q-mt-md">
|
||||
<q-item v-for="relay in relays" :key="relay">
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<q-icon name="router"></q-icon>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-sm">
|
||||
<q-item-label><strong>{{ relay}}</strong></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn size="12px" flat dense round icon="delete" @click="removeRelay(relay)" />
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="marketplace">
|
||||
<q-list v-if="!readNotes.marketUi" class="q-mb-lg" bordered>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<q-icon color="primary" name="info" size="xl" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-sm q-ml-lg">
|
||||
<q-item-label><strong>Note</strong></q-item-label>
|
||||
<q-item-label>
|
||||
<div class="text-caption">
|
||||
<ul>
|
||||
<li><span class="text-subtitle1">
|
||||
Here one can customize the look and feel of the Market.
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-subtitle1">
|
||||
When the Market Profile is shared (via <code>naddr</code> ) these customisations will be
|
||||
available to the
|
||||
customers.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn @click="markNoteAsRead('marketUi')" size="lg" outline color="primary" label="Got it!"
|
||||
icon="check_small" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div class="q-mb-md"> <strong>Information</strong></div>
|
||||
<q-input @change="updateUiConfig" outlined v-model="configData.name" type="text" label="Market Name"
|
||||
hint="Short name for the market" class="q-mb-md">
|
||||
</q-input>
|
||||
<q-input @change="updateUiConfig" outlined v-model="configData.about" type="textarea" rows="3"
|
||||
label="Marketplace Description"
|
||||
hint="It will be displayed on top of the banner image. Can be a longer text." class="q-mb-lg"></q-input>
|
||||
|
||||
<div class="q-mb-md q-mt-lg">
|
||||
<strong>UI Configurations</strong>
|
||||
</div>
|
||||
|
||||
<q-input @change="updateUiConfig" outlined v-model="configData.ui.picture" type="text" label="Logo"
|
||||
hint="It will be displayed next to the search input. Can be png, jpg, ico, gif, svg." class="q-mb-md">
|
||||
</q-input>
|
||||
<q-input @change="updateUiConfig" outlined v-model="configData.ui.banner" type="text" label="Banner"
|
||||
hint="It represents the visual identity of the market. Can be png, jpg, ico, gif, svg." class="q-mb-md">
|
||||
</q-input>
|
||||
<q-select @input="updateUiConfig" filled v-model="configData.ui.theme"
|
||||
hint="The colors of the market will vary based on the theme. It applies to all components (buttons, labels, inputs, etc)"
|
||||
:options="themeOptions" label="Marketplace Theme"></q-select>
|
||||
|
||||
<q-checkbox @input="updateUiConfig" v-model="configData.ui.darkMode" label="Dark Mode"
|
||||
size="sm"></q-checkbox>
|
||||
|
||||
</q-tab-panel>
|
||||
|
||||
</q-tab-panels>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section>
|
||||
<div class="float-right">
|
||||
<q-btn @click="clearAllData" flat label="Clear All Data" icon="delete" class="q-ml-lg" color="negative"></q-btn>
|
||||
<q-btn @click="publishNaddr" flat label="Share Market Profile" icon="share" class="q-ml-lg"
|
||||
color="primary"></q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section></q-card-section>
|
||||
</q-card>
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
async function marketConfig(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('market-config', {
|
||||
name: 'market-config',
|
||||
props: ['merchants', 'relays', 'config-ui', 'read-notes'],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
tab: 'merchants',
|
||||
merchantPubkey: null,
|
||||
relayUrl: null,
|
||||
configData: {
|
||||
identifier: null,
|
||||
name: null,
|
||||
about: null,
|
||||
ui: {
|
||||
picture: null,
|
||||
banner: null,
|
||||
theme: null,
|
||||
darkMode: false
|
||||
}
|
||||
},
|
||||
themeOptions: [
|
||||
'classic',
|
||||
'bitcoin',
|
||||
'flamingo',
|
||||
'cyber',
|
||||
'freedom',
|
||||
'mint',
|
||||
'autumn',
|
||||
'monochrome',
|
||||
'salvador'
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addMerchant: async function () {
|
||||
if (!isValidKey(this.merchantPubkey, 'npub')) {
|
||||
this.$q.notify({
|
||||
message: 'Invalid Public Key!',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
const publicKey = isValidKeyHex(this.merchantPubkey) ? this.merchantPubkey : NostrTools.nip19.decode(this.merchantPubkey).data
|
||||
this.$emit('add-merchant', publicKey)
|
||||
this.merchantPubkey = null
|
||||
},
|
||||
removeMerchant: async function (publicKey) {
|
||||
this.$emit('remove-merchant', publicKey)
|
||||
},
|
||||
addRelay: async function () {
|
||||
const relayUrl = (this.relayUrl || '').trim()
|
||||
if (!relayUrl.startsWith('wss://') && !relayUrl.startsWith('ws://')) {
|
||||
this.relayUrl = null
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: `Invalid relay URL.`,
|
||||
caption: "Should start with 'wss://'' or 'ws://'"
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
new URL(relayUrl);
|
||||
this.$emit('add-relay', relayUrl)
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: `Invalid relay URL.`,
|
||||
caption: `Error: ${error}`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.relayUrl = null
|
||||
},
|
||||
removeRelay: async function (relay) {
|
||||
this.$emit('remove-relay', relay)
|
||||
},
|
||||
updateUiConfig: function () {
|
||||
const { name, about, ui } = this.configData
|
||||
console.log('### this.info', { name, about, ui })
|
||||
this.$emit('ui-config-update', { name, about, ui })
|
||||
},
|
||||
publishNaddr() {
|
||||
this.$emit('publish-naddr')
|
||||
},
|
||||
clearAllData() {
|
||||
this.$emit('clear-all-data')
|
||||
},
|
||||
markNoteAsRead(noteId) {
|
||||
this.$emit('note-read', noteId)
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.configUi) {
|
||||
this.configData = {
|
||||
...this.configData,
|
||||
...this.configUi,
|
||||
ui: {
|
||||
...this.configData.ui, ...(this.configUi.ui || {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<q-card class="card--product">
|
||||
<q-img
|
||||
:src="(product.images && product.images.length > 0 && product.images[0]) ? product.images[0] : '/nostrmarket/static/images/placeholder.png'"
|
||||
alt="Product Image" loading="lazy" spinner-color="white" fit="contain" height="300px"></q-img>
|
||||
|
||||
<q-card-section class="q-pb-xs q-pt-md">
|
||||
<q-btn round :disabled="product.quantity < 1" color="primary" rounded icon="shopping_cart" size="lg" style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
" @click="$emit('add-to-cart', product)"><q-tooltip> Add to cart </q-tooltip></q-btn>
|
||||
<div class="row no-wrap items-center">
|
||||
<div class="col text-subtitle2 ellipsis-2-lines">{{ product.name }}</div>
|
||||
</div>
|
||||
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-py-sm">
|
||||
<div>
|
||||
<span v-if="product.currency == 'sat'">
|
||||
<span class="text-h6">{{ product.price }} sats</span><q-tooltip> BTC {{ (product.price /
|
||||
1e8).toFixed(8) }}</q-tooltip>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="text-h6">{{ product.formatedPrice }}</span>
|
||||
</span>
|
||||
<span class="q-ml-md text-caption text-green-8 text-weight-bolder q-mt-md">{{ product.quantity }} left</span>
|
||||
</div>
|
||||
<div v-if="product.categories" class="text-subtitle1">
|
||||
<q-virtual-scroll :items="product.categories || []" virtual-scroll-horizontal>
|
||||
<template v-slot="{ item, index }">
|
||||
<q-chip :key="index" dense><span v-text="item"></span></q-chip>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
</div>
|
||||
<div v-else class="text-subtitle1">
|
||||
 
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey ellipsis-2-lines" style="min-height: 40px">
|
||||
<p v-if="product.description">{{ product.description }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-actions>
|
||||
<div class="text-caption text-weight-bolder">{{ product.stallName }}</div>
|
||||
</q-card-actions>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-card-actions>
|
||||
<div class="q-ml-auto">
|
||||
<q-btn v-if="!isStall" flat class="text-weight-bold text-capitalize q-ml-auto float-left" dense color="primary"
|
||||
@click="$emit('change-page', 'stall', {stall: product.stall_id})">
|
||||
Visit Stall
|
||||
</q-btn>
|
||||
<q-btn flat class="text-weight-bold text-capitalize q-ml-auto" dense color="primary"
|
||||
@click="$emit('change-page', 'stall', {stall: product.stall_id, product: product.id})">
|
||||
View details
|
||||
</q-btn>
|
||||
|
||||
</div>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
async function productCard(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('product-card', {
|
||||
name: 'product-card',
|
||||
template,
|
||||
|
||||
props: ['product', 'change-page', 'add-to-cart', 'is-stall'],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
methods: {},
|
||||
created() {}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-5 col-md-5 col-sm-12 col-xs-12 q-mt-sm">
|
||||
<div class="q-pr-md" v-if="product.images && product.images[0]">
|
||||
<q-carousel
|
||||
swipeable
|
||||
animated
|
||||
v-model="slide"
|
||||
thumbnails
|
||||
infinite
|
||||
arrows
|
||||
transition-prev="slide-right"
|
||||
transition-next="slide-left"
|
||||
navigation-icon="radio_button_unchecked"
|
||||
control-type="regular"
|
||||
control-color="secondary"
|
||||
control-text-color="white"
|
||||
>
|
||||
<q-carousel-slide
|
||||
v-for="(img, i) in product.images"
|
||||
:name="i + 1"
|
||||
:key="i"
|
||||
:img-src="img"
|
||||
></q-carousel-slide>
|
||||
</q-carousel>
|
||||
|
||||
</div>
|
||||
<div v-else class="q-pr-md">
|
||||
<q-img src="/nostrmarket/static/images/placeholder.png" :ratio="16/9"></q-img>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 col-xs-12 q-mt-sm">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-12"
|
||||
:class="$q.platform.is.desktop ? '' : 'q-px-md'"
|
||||
>
|
||||
<div class="text-subtitle1 q-mt-sm q-pt-xs">{{ product.name }}</div>
|
||||
<div v-if="product.categories" class="text-subtitle1">
|
||||
<q-chip v-for="(cat, i) in product.categories" :key="i" dense
|
||||
>{{cat}}</q-chip
|
||||
>
|
||||
</div>
|
||||
<div class="q-mt-sm text-weight-bold">{{ product.description }}</div>
|
||||
<div>
|
||||
<span v-if="product.currency == 'sat'">
|
||||
<span class="text-h6">{{ product.price }} sats</span
|
||||
><span class="q-ml-sm text-grey-6"
|
||||
>BTC {{ (product.price / 1e8).toFixed(8) }}</span
|
||||
>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="text-h6">{{ product.formatedPrice }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="q-ml-md text-caption text-green-8 text-weight-bolder q-mt-md"
|
||||
>{{ product.quantity > 0 ? `In stock. ${product.quantity} left.` : 'Out of stock.' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="q-mt-md">
|
||||
<q-btn
|
||||
class="q-mt-md"
|
||||
color="primary"
|
||||
rounded
|
||||
icon="shopping_cart"
|
||||
label="Add to cart"
|
||||
@click="$emit('add-to-cart', product)"
|
||||
/>
|
||||
<!-- <q-btn
|
||||
class="q-mt-md q-ml-md"
|
||||
color="primary"
|
||||
icon="share"
|
||||
label="Share"
|
||||
/> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- RATING TO BE DONE -->
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
async function productDetail(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('product-detail', {
|
||||
name: 'product-detail',
|
||||
template,
|
||||
|
||||
props: ['product', 'add-to-cart'],
|
||||
data: function () {
|
||||
return {
|
||||
slide: 1
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
created() {}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
<div>
|
||||
<q-card v-if="cart && stall" bordered class="q-mb-md">
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img v-if="cart.merchant?.profile?.picture" :src="cart.merchant?.profile?.picture">
|
||||
<img v-else src="/nostrmarket/static/images/blank-avatar.webp">
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>
|
||||
<span v-text="cart.products[0]?.stallName"></span>
|
||||
</strong>
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
By <span v-text="cart.merchant?.profile?.name || cart.merchant?.publicKey"></span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
<q-card-section v-if="orderConfirmed">
|
||||
<div class="row q-mt-md q-ml-md q-pr-md">
|
||||
<div class="col-xs-12 col-sm-12 col-md-2 q-mt-md">
|
||||
<strong>Message:</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-10">
|
||||
<q-input v-model.trim="contactData.message" outlined type="textarea" rows="3"
|
||||
label="Message (optional)" hint="Message merchant about additional order needs"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md q-ml-md q-pr-md">
|
||||
<div class="col-xs-12 col-sm-12 col-md-2 q-mt-md">
|
||||
<strong>Address:</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-10">
|
||||
<q-input v-model.trim="contactData.address" outlined type="textarea" rows="3"
|
||||
label="Address (optional)" hint="Must provide for physical shipping">
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md q-ml-md q-pr-md">
|
||||
<div class="col-xs-12 col-sm-12 col-md-2 q-mt-md">
|
||||
<strong>Email:</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-10">
|
||||
<q-input v-model.trim="contactData.email" type="email" outlined label="Email (optional)"
|
||||
hint="Merchant may not use email"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md q-ml-md q-pr-md">
|
||||
<div class="col-xs-12 col-sm-12 col-md-2 q-mt-md">
|
||||
<strong>Npub:</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-10">
|
||||
<q-input v-model.trim="contactData.npub" outlined label="Alternative Npub (optional)"
|
||||
hint="Use a different Npub to communicate with the merchant"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else horizontal>
|
||||
<q-card-section class="col-7">
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
<strong>Subtotal:</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
<strong>{{formatCurrency(cartTotal, stall.currency)}}</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
<strong>Shipping:</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
<strong v-if="shippingZone">{{formatCurrency(shippingZone.cost, stall.currency)}}</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
<q-btn-dropdown unelevated color="secondary" rounded :label="shippingZoneLabel">
|
||||
<q-item v-for="zone of stall.shipping" @click="selectShippingZone(zone)" :key="zone.id"
|
||||
clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label><span v-text="zone.name"></span></q-item-label>
|
||||
<q-item-label caption><span
|
||||
v-text="zone.countries?.join(', ')"></span></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<q-separator class="q-mt-sm" />
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
<strong>Total:</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
<strong>{{formatCurrency(cartTotalWithShipping, stall.currency)}}</strong>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</q-card-section>
|
||||
|
||||
<q-separator vertical />
|
||||
|
||||
<q-card-section>
|
||||
<strong>Payment Method</strong>
|
||||
<q-option-group v-model="paymentMethod" :options="paymentOptions" color="green" disable />
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-card-actions align="right">
|
||||
<div v-if="orderConfirmed">
|
||||
<q-btn @click="orderConfirmed = false" flat color="grey">
|
||||
Back
|
||||
</q-btn>
|
||||
<q-btn @click="placeOrder()" flat color="primary">
|
||||
Place Order
|
||||
</q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-btn @click="goToShoppingCart" flat color="grey">
|
||||
Back
|
||||
</q-btn>
|
||||
<q-btn @click="confirmOrder" flat color="primary">
|
||||
Confirm
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
|
||||
</div>
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
async function shoppingCartCheckout(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('shopping-cart-checkout', {
|
||||
name: 'shopping-cart-checkout',
|
||||
template,
|
||||
|
||||
props: ['cart', 'stall', 'customer-pubkey'],
|
||||
data: function () {
|
||||
return {
|
||||
orderConfirmed: false,
|
||||
paymentMethod: 'ln',
|
||||
shippingZone: null,
|
||||
contactData: {
|
||||
email: null,
|
||||
npub: null,
|
||||
address: null,
|
||||
message: null
|
||||
},
|
||||
paymentOptions: [
|
||||
{
|
||||
label: 'Lightning Network',
|
||||
value: 'ln'
|
||||
},
|
||||
{
|
||||
label: 'BTC Onchain',
|
||||
value: 'btc'
|
||||
},
|
||||
{
|
||||
label: 'Cashu',
|
||||
value: 'cashu'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cartTotal() {
|
||||
if (!this.cart.products?.length) return 0
|
||||
return this.cart.products.reduce((t, p) => p.price + t, 0)
|
||||
},
|
||||
cartTotalWithShipping() {
|
||||
if (!this.shippingZone) return this.cartTotal
|
||||
return this.cartTotal + this.shippingZone.cost
|
||||
},
|
||||
shippingZoneLabel() {
|
||||
if (!this.shippingZone) {
|
||||
return 'Shipping Zone'
|
||||
}
|
||||
const zoneName = this.shippingZone.name.substring(0, 10)
|
||||
if (this.shippingZone?.name.length < 10) {
|
||||
return zoneName
|
||||
}
|
||||
return zoneName + '...'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatCurrency: function (value, unit) {
|
||||
return formatCurrency(value, unit)
|
||||
},
|
||||
selectShippingZone: function (zone) {
|
||||
this.shippingZone = zone
|
||||
},
|
||||
|
||||
confirmOrder: function () {
|
||||
if (!this.shippingZone) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Please select a shipping zone!',
|
||||
})
|
||||
return
|
||||
}
|
||||
this.orderConfirmed = true
|
||||
},
|
||||
async placeOrder() {
|
||||
if (!this.shippingZone) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Please select a shipping zone!',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!this.customerPubkey) {
|
||||
this.$emit('login-required')
|
||||
return
|
||||
}
|
||||
const order = {
|
||||
address: this.contactData.address,
|
||||
message: this.contactData.message,
|
||||
contact: {
|
||||
nostr: this.contactData.npub,
|
||||
email: this.contactData.email
|
||||
},
|
||||
items: Array.from(this.cart.products, p => {
|
||||
return { product_id: p.id, quantity: p.orderedQuantity }
|
||||
}),
|
||||
shipping_id: this.shippingZone.id,
|
||||
type: 0
|
||||
}
|
||||
const created_at = Math.floor(Date.now() / 1000)
|
||||
order.id = await hash(
|
||||
[this.customerPubkey, created_at, JSON.stringify(order)].join(':')
|
||||
)
|
||||
|
||||
const event = {
|
||||
...(await NostrTools.getBlankEvent()),
|
||||
kind: 4,
|
||||
created_at,
|
||||
tags: [['p', this.stall.pubkey]],
|
||||
pubkey: this.customerPubkey
|
||||
}
|
||||
|
||||
this.$emit('place-order', { event, order, cartId: this.cart.id })
|
||||
|
||||
},
|
||||
goToShoppingCart: function () {
|
||||
this.$emit('change-page', 'shopping-cart-list')
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.stall.shipping?.length === 1) {
|
||||
this.shippingZone = this.stall.shipping[0]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
<div>
|
||||
<q-card v-if="!carts?.length" bordered class="q-mb-md">
|
||||
<q-card-section>
|
||||
<strong>No products in cart!</strong>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div v-for="cart in carts">
|
||||
|
||||
<q-card bordered class="q-mb-md">
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img v-if="cart.merchant?.profile?.picture" :src="cart.merchant?.profile?.picture">
|
||||
<img v-else src="/nostrmarket/static/images/blank-avatar.webp">
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>
|
||||
<span v-text="cart.products[0]?.stallName"></span>
|
||||
</strong>
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
By <span v-text="cart.merchant?.profile?.name || cart.merchant?.publicKey"></span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div>
|
||||
<q-btn @click="removeCart(cart.id)" flat color="pink">
|
||||
Clear Cart
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section horizontal>
|
||||
|
||||
<q-card-section class="col-12">
|
||||
<q-list class="q-mt-md">
|
||||
<q-item v-for="product in cart.products" :key="product.id">
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img v-if="product.images[0] || product.image" :src="product.images[0] || product.image" />
|
||||
<img v-else src="/nostrmarket/static/images/placeholder.png" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-sm">
|
||||
<q-item-label>{{ product.name}}</q-item-label>
|
||||
<q-item-label>
|
||||
<div class="text-caption text-grey ellipsis-2-lines">
|
||||
<p>{{ product.description }}</p>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-mt-sm">
|
||||
<q-item-label><strong>{{ formatCurrency(product.price, product.currency)}}</strong></q-item-label>
|
||||
<q-item-label></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section class="q-ma-sm">
|
||||
<q-input v-model.number="product.orderedQuantity" @change="quantityChanged(product)" type="number"
|
||||
rounded outlined min="1" :max="product.quantity"></q-input>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label><strong>{{ formatCurrency(product.price * product.orderedQuantity, product.currency)}}
|
||||
</strong></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div>
|
||||
<q-btn flat dense round icon="delete" @click="removeProduct(product.stall_id, product.id)" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
|
||||
<q-card-actions align="right">
|
||||
|
||||
Total: <strong class="q-ma-md"> {{cartTotalFormatted(cart)}} </strong>
|
||||
<q-btn @click="proceedToCheckout(cart)" flat color="primary">
|
||||
Proceed to Checkout
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
async function shoppingCartList(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('shopping-cart-list', {
|
||||
name: 'shopping-cart-list',
|
||||
template,
|
||||
|
||||
props: ['carts'],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
formatCurrency: function (value, unit) {
|
||||
return formatCurrency(value, unit)
|
||||
},
|
||||
cartTotalFormatted(cart) {
|
||||
if (!cart.products?.length) return ""
|
||||
const total = cart.products.reduce((t, p) => p.price + t, 0)
|
||||
return formatCurrency(total, cart.products[0].currency)
|
||||
},
|
||||
removeProduct: function (stallId, productId) {
|
||||
this.$emit('remove-from-cart', { stallId, productId })
|
||||
},
|
||||
removeCart: function (stallId) {
|
||||
this.$emit('remove-cart', stallId)
|
||||
},
|
||||
quantityChanged: function (product) {
|
||||
this.$emit('add-to-cart', product)
|
||||
},
|
||||
proceedToCheckout: function(cart){
|
||||
this.$emit('checkout-cart', cart)
|
||||
}
|
||||
},
|
||||
created() { }
|
||||
})
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<q-btn dense round flat icon="shopping_cart">
|
||||
<q-badge v-if="cart.size" color="red" class="text-bold" floating>
|
||||
{{ cart.size }}
|
||||
</q-badge>
|
||||
<q-menu v-if="cart.size">
|
||||
<q-list style="min-width: 100px">
|
||||
<q-item :id="p.id" :key="p.id" v-for="p in cartMenu">
|
||||
<q-item-section style="flex-flow: row">
|
||||
<q-btn color="red" size="xs" icon="remove" @click="remove(p.id)" />
|
||||
<q-input
|
||||
v-model.number="p.quantity"
|
||||
@change="addQty(p.id, p.quantity)"
|
||||
type="number"
|
||||
dense
|
||||
standout
|
||||
:max="products.find(pr => pr.id == p.id).quantity"
|
||||
></q-input>
|
||||
|
||||
<q-btn color="green" size="xs" icon="add" @click="add(p.id)" />
|
||||
</q-item-section>
|
||||
<q-item-section avatar v-if="p.image">
|
||||
<q-avatar color="primary">
|
||||
<img :src="p.image" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section top class="q-mx-sm">
|
||||
<q-item-label>{{ p.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<span>
|
||||
{{p.currency != 'sat' ? p.formatedPrice : p.price + 'sats'}}
|
||||
<q-btn
|
||||
class="q-ml-md"
|
||||
round
|
||||
color="red"
|
||||
size="xs"
|
||||
icon="close"
|
||||
@click="removeProduct(p.id)"
|
||||
/>
|
||||
</span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</q-list>
|
||||
<div class="row q-pa-md q-gutter-md">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon-right="checkout"
|
||||
label="Checkout"
|
||||
@click="$emit('open-checkout')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
class="q-ml-auto"
|
||||
flat
|
||||
color="primary"
|
||||
label="Reset"
|
||||
@click="$emit('reset-cart')"
|
||||
></q-btn></div></q-menu
|
||||
></q-btn>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
async function shoppingCart(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
Vue.component('shopping-cart', {
|
||||
name: 'shopping-cart',
|
||||
template,
|
||||
|
||||
props: [
|
||||
'cart',
|
||||
'cart-menu',
|
||||
'add-to-cart',
|
||||
'remove-from-cart',
|
||||
'update-qty',
|
||||
'reset-cart',
|
||||
'products'
|
||||
],
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
add(id) {
|
||||
this.$emit(
|
||||
'add-to-cart',
|
||||
this.products.find(p => p.id == id)
|
||||
)
|
||||
},
|
||||
remove(id) {
|
||||
this.$emit(
|
||||
'remove-from-cart',
|
||||
this.products.find(p => p.id == id)
|
||||
)
|
||||
},
|
||||
removeProduct(id) {
|
||||
this.$emit(
|
||||
'remove-from-cart',
|
||||
this.products.find(p => p.id == id),
|
||||
true
|
||||
)
|
||||
},
|
||||
addQty(id, qty) {
|
||||
if (qty == 0) {
|
||||
return this.removeProduct(id)
|
||||
}
|
||||
let product = this.products.find(p => p.id == id)
|
||||
if (product.quantity < qty) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: `${product.name} only has ${product.quantity} units!`,
|
||||
icon: 'production_quantity_limits'
|
||||
})
|
||||
let objIdx = this.cartMenu.findIndex(pr => pr.id == id)
|
||||
this.cartMenu[objIdx].quantity = this.cart.products.get(id).quantity
|
||||
return
|
||||
}
|
||||
this.$emit('update-qty', id, qty)
|
||||
}
|
||||
},
|
||||
created() {}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
async function stallDetails(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
|
||||
const pica = window.pica()
|
||||
|
||||
Vue.component('stall-details', {
|
||||
name: 'stall-details',
|
||||
template,
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
<q-card>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-y-md">
|
||||
User Chat
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</q-card>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
async function userChat(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('user-chat', {
|
||||
name: 'user-chat',
|
||||
props: ['user',],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
},
|
||||
created: async function () {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<q-card>
|
||||
<q-card-section v-if="account">
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<q-input v-model="account.npub" readonly disbled outlined :hint="account.pubkey" type="text" label="Public Key"
|
||||
class="q-mb-md">
|
||||
<template v-slot:append>
|
||||
<q-btn @click="copyText(account.npub)" icon="content_copy" label="Npub" flat
|
||||
color="gray float-right q-mt-sm"></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-2 auto-width">
|
||||
<q-btn @click="copyText(account.pubkey)" icon="content_copy" label="Hex" flat
|
||||
color="gray float-right q-mt-sm"></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<q-input v-model="account.nsec" readonly disbled outlined type="password" label="Private Key" class="q-mb-md">
|
||||
<template v-slot:append>
|
||||
<q-btn @click="copyText(account.nsec)" icon="content_copy" label="Nsec" flat
|
||||
color="gray float-right q-mt-sm"></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-2 auto-width">
|
||||
<q-btn @click="copyText(account.privkey)" icon="content_copy" label="Hex" flat
|
||||
color="gray float-right q-mt-sm"></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section>
|
||||
<div v-if="account" class="float-right">
|
||||
<q-btn @click="logout" flat label="Logout" icon="logout" class="q-ml-lg" color="primary"></q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<strong>No Account</strong>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section></q-card-section>
|
||||
|
||||
</q-card>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
async function userConfig(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('user-config', {
|
||||
name: 'user-config',
|
||||
props: ['account'],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logout: async function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Please make sure you save your private key! You will not be able to recover it later!'
|
||||
)
|
||||
.onOk(async () => {
|
||||
this.$emit('logout')
|
||||
})
|
||||
},
|
||||
copyText(text) {
|
||||
this.$emit('copy-text', text)
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue