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:
Vlad Stan 2023-07-31 11:35:50 +03:00 committed by GitHub
parent 8ebe2fe458
commit a3299b63c4
60 changed files with 18008 additions and 3197 deletions

View file

@ -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>

View file

@ -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)
}
})
}

View file

@ -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>

View file

@ -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))
}
})
}

View file

@ -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>

View file

@ -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() {
}
})
}

View file

@ -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">
&nbsp
</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>

View file

@ -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() {
}
})
}

View file

@ -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>

View file

@ -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)
},
}
})
}

View file

@ -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>

View file

@ -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 || {})
}
}
}
}
})
}

View file

@ -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">
&nbsp
</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>

View file

@ -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() {}
})
}

View file

@ -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>

View file

@ -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() {}
})
}

View file

@ -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>

View file

@ -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]
}
}
})
}

View file

@ -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>

View file

@ -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() { }
})
}

View file

@ -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>

View file

@ -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() {}
})
}

View file

@ -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,

View file

@ -1,8 +0,0 @@
<q-card>
<div class="q-pa-md">
<div class="q-gutter-y-md">
User Chat
</div>
</div>
</q-card>

View file

@ -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 () {
}
})
}

View file

@ -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>

View file

@ -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 () {
}
})
}