Merge remote-tracking branch 'origin/main' into feature/nostrclient-status-indicator

This commit is contained in:
Arc 2025-12-24 03:57:27 +00:00
commit 05a23fae0b
18 changed files with 2205 additions and 426 deletions

View file

@ -0,0 +1,37 @@
window.app.component('merchant-tab', {
name: 'merchant-tab',
template: '#merchant-tab',
delimiters: ['${', '}'],
props: [
'merchant-id',
'inkey',
'adminkey',
'show-keys',
'merchant-active',
'public-key',
'private-key',
'is-admin'
],
computed: {
marketClientUrl: function () {
return '/nostrmarket/market'
}
},
methods: {
toggleShowKeys: function () {
this.$emit('toggle-show-keys')
},
hideKeys: function () {
this.$emit('hide-keys')
},
handleMerchantDeleted: function () {
this.$emit('merchant-deleted')
},
toggleMerchantState: function () {
this.$emit('toggle-merchant-state')
},
restartNostrConnection: function () {
this.$emit('restart-nostr-connection')
}
}
})

View file

@ -0,0 +1,261 @@
window.app.component('product-list', {
name: 'product-list',
template: '#product-list',
delimiters: ['${', '}'],
props: ['adminkey', 'inkey', 'stall-filter'],
data: function () {
return {
filter: '',
stalls: [],
products: [],
pendingProducts: [],
selectedStall: null,
productDialog: {
showDialog: false,
showRestore: false,
data: null
},
productsTable: {
columns: [
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'stall', align: 'left', label: 'Stall', field: 'stall_id'},
{name: 'price', align: 'left', label: 'Price', field: 'price'},
{
name: 'quantity',
align: 'left',
label: 'Quantity',
field: 'quantity'
},
{name: 'actions', align: 'right', label: 'Actions', field: ''}
],
pagination: {
rowsPerPage: 10
}
}
}
},
computed: {
stallOptions: function () {
return this.stalls.map(s => ({
label: s.name,
value: s.id
}))
},
filteredProducts: function () {
if (!this.selectedStall) {
return this.products
}
return this.products.filter(p => p.stall_id === this.selectedStall)
}
},
watch: {
stallFilter: {
immediate: true,
handler(newVal) {
if (newVal) {
this.selectedStall = newVal
}
}
}
},
methods: {
getStalls: async function () {
try {
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/stall?pending=false',
this.inkey
)
this.stalls = data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
getProducts: async function () {
try {
// Fetch products from all stalls
const allProducts = []
for (const stall of this.stalls) {
const {data} = await LNbits.api.request(
'GET',
`/nostrmarket/api/v1/stall/product/${stall.id}?pending=false`,
this.inkey
)
allProducts.push(...data)
}
this.products = allProducts
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
getPendingProducts: async function () {
try {
// Fetch pending products from all stalls
const allPending = []
for (const stall of this.stalls) {
const {data} = await LNbits.api.request(
'GET',
`/nostrmarket/api/v1/stall/product/${stall.id}?pending=true`,
this.inkey
)
allPending.push(...data)
}
this.pendingProducts = allPending
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
getStallName: function (stallId) {
const stall = this.stalls.find(s => s.id === stallId)
return stall ? stall.name : 'Unknown'
},
getStallCurrency: function (stallId) {
const stall = this.stalls.find(s => s.id === stallId)
return stall ? stall.currency : 'sat'
},
getStall: function (stallId) {
return this.stalls.find(s => s.id === stallId)
},
newEmptyProductData: function () {
return {
id: null,
stall_id: this.stalls.length ? this.stalls[0].id : null,
name: '',
categories: [],
images: [],
image: null,
price: 0,
quantity: 0,
config: {
description: '',
use_autoreply: false,
autoreply_message: ''
}
}
},
showNewProductDialog: function () {
this.productDialog.data = this.newEmptyProductData()
this.productDialog.showDialog = true
},
editProduct: function (product) {
this.productDialog.data = {...product, image: null}
if (!this.productDialog.data.config) {
this.productDialog.data.config = {description: ''}
}
this.productDialog.showDialog = true
},
sendProductFormData: async function () {
const data = {
stall_id: this.productDialog.data.stall_id,
id: this.productDialog.data.id,
name: this.productDialog.data.name,
images: this.productDialog.data.images || [],
price: this.productDialog.data.price,
quantity: this.productDialog.data.quantity,
categories: this.productDialog.data.categories || [],
config: this.productDialog.data.config
}
this.productDialog.showDialog = false
if (this.productDialog.data.id) {
data.pending = false
await this.updateProduct(data)
} else {
await this.createProduct(data)
}
},
createProduct: async function (payload) {
try {
const {data} = await LNbits.api.request(
'POST',
'/nostrmarket/api/v1/product',
this.adminkey,
payload
)
this.products.unshift(data)
this.$q.notify({
type: 'positive',
message: 'Product Created'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
updateProduct: async function (product) {
try {
const {data} = await LNbits.api.request(
'PATCH',
'/nostrmarket/api/v1/product/' + product.id,
this.adminkey,
product
)
const index = this.products.findIndex(p => p.id === product.id)
if (index !== -1) {
this.products.splice(index, 1, data)
} else {
this.products.unshift(data)
}
this.$q.notify({
type: 'positive',
message: 'Product Updated'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
deleteProduct: function (product) {
LNbits.utils
.confirmDialog(`Are you sure you want to delete "${product.name}"?`)
.onOk(async () => {
try {
await LNbits.api.request(
'DELETE',
'/nostrmarket/api/v1/product/' + product.id,
this.adminkey
)
this.products = this.products.filter(p => p.id !== product.id)
this.$q.notify({
type: 'positive',
message: 'Product Deleted'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
})
},
toggleProductActive: async function (product) {
await this.updateProduct({...product, active: !product.active})
},
addProductImage: function () {
if (!this.productDialog.data.image) return
if (!this.productDialog.data.images) {
this.productDialog.data.images = []
}
this.productDialog.data.images.push(this.productDialog.data.image)
this.productDialog.data.image = null
},
removeProductImage: function (imageUrl) {
const index = this.productDialog.data.images.indexOf(imageUrl)
if (index !== -1) {
this.productDialog.data.images.splice(index, 1)
}
},
openSelectPendingProductDialog: async function () {
await this.getPendingProducts()
this.productDialog.showRestore = true
},
openRestoreProductDialog: function (pendingProduct) {
pendingProduct.pending = true
this.productDialog.data = {...pendingProduct, image: null}
this.productDialog.showDialog = true
},
shortLabel: function (value = '') {
if (value.length <= 44) return value
return value.substring(0, 40) + '...'
}
},
created: async function () {
await this.getStalls()
await this.getProducts()
}
})

View file

@ -0,0 +1,209 @@
window.app.component('shipping-zones-list', {
name: 'shipping-zones-list',
props: ['adminkey', 'inkey'],
template: '#shipping-zones-list',
delimiters: ['${', '}'],
data: function () {
return {
zones: [],
filter: '',
zoneDialog: {
showDialog: false,
data: {
id: null,
name: '',
countries: [],
cost: 0,
currency: 'sat'
}
},
currencies: [],
shippingZoneOptions: [
'Free (digital)',
'Worldwide',
'Europe',
'Australia',
'Austria',
'Belgium',
'Brazil',
'Canada',
'China',
'Denmark',
'Finland',
'France',
'Germany',
'Greece',
'Hong Kong',
'Hungary',
'Indonesia',
'Ireland',
'Israel',
'Italy',
'Japan',
'Kazakhstan',
'Korea',
'Luxembourg',
'Malaysia',
'Mexico',
'Netherlands',
'New Zealand',
'Norway',
'Poland',
'Portugal',
'Romania',
'Russia',
'Saudi Arabia',
'Singapore',
'Spain',
'Sweden',
'Switzerland',
'Thailand',
'Turkey',
'Ukraine',
'United Kingdom',
'United States',
'Vietnam'
],
zonesTable: {
columns: [
{
name: 'name',
align: 'left',
label: 'Name',
field: 'name',
sortable: true
},
{
name: 'countries',
align: 'left',
label: 'Countries',
field: 'countries',
sortable: true
},
{
name: 'cost',
align: 'left',
label: 'Cost',
field: 'cost',
sortable: true
},
{
name: 'currency',
align: 'left',
label: 'Currency',
field: 'currency',
sortable: true
},
{
name: 'actions',
align: 'right',
label: 'Actions',
field: ''
}
],
pagination: {
rowsPerPage: 10,
sortBy: 'name',
descending: false
}
}
}
},
methods: {
openZoneDialog: function (data) {
data = data || {
id: null,
name: '',
countries: [],
cost: 0,
currency: 'sat'
}
this.zoneDialog.data = {...data}
this.zoneDialog.showDialog = true
},
getZones: async function () {
try {
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/zone',
this.inkey
)
this.zones = data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
sendZoneFormData: async function () {
this.zoneDialog.showDialog = false
if (this.zoneDialog.data.id) {
await this.updateShippingZone(this.zoneDialog.data)
} else {
await this.createShippingZone(this.zoneDialog.data)
}
await this.getZones()
},
createShippingZone: async function (newZone) {
try {
await LNbits.api.request(
'POST',
'/nostrmarket/api/v1/zone',
this.adminkey,
newZone
)
this.$q.notify({
type: 'positive',
message: 'Zone created!'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
updateShippingZone: async function (updatedZone) {
try {
await LNbits.api.request(
'PATCH',
`/nostrmarket/api/v1/zone/${updatedZone.id}`,
this.adminkey,
updatedZone
)
this.$q.notify({
type: 'positive',
message: 'Zone updated!'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
confirmDeleteZone: function (zone) {
LNbits.utils
.confirmDialog(`Are you sure you want to delete zone "${zone.name}"?`)
.onOk(async () => {
await this.deleteShippingZone(zone.id)
})
},
deleteShippingZone: async function (zoneId) {
try {
await LNbits.api.request(
'DELETE',
`/nostrmarket/api/v1/zone/${zoneId}`,
this.adminkey
)
this.$q.notify({
type: 'positive',
message: 'Zone deleted!'
})
await this.getZones()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
getCurrencies() {
const currencies = window.g.allowedCurrencies || []
this.currencies = ['sat', ...currencies]
}
},
created: async function () {
await this.getZones()
this.getCurrencies()
}
})

View file

@ -19,7 +19,6 @@ window.app.component('shipping-zones', {
currencies: [],
shippingZoneOptions: [
'Free (digital)',
'Flat rate',
'Worldwide',
'Europe',
'Australia',
@ -27,6 +26,7 @@ window.app.component('shipping-zones', {
'Belgium',
'Brazil',
'Canada',
'China',
'Denmark',
'Finland',
'France',
@ -34,8 +34,8 @@ window.app.component('shipping-zones', {
'Greece',
'Hong Kong',
'Hungary',
'Ireland',
'Indonesia',
'Ireland',
'Israel',
'Italy',
'Japan',
@ -59,10 +59,9 @@ window.app.component('shipping-zones', {
'Thailand',
'Turkey',
'Ukraine',
'United Kingdom**',
'United States***',
'Vietnam',
'China'
'United Kingdom',
'United States',
'Vietnam'
]
}
},
@ -162,22 +161,13 @@ window.app.component('shipping-zones', {
LNbits.utils.notifyApiError(error)
}
},
async getCurrencies() {
try {
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/currencies',
this.inkey
)
this.currencies = ['sat', ...data]
} catch (error) {
LNbits.utils.notifyApiError(error)
}
getCurrencies() {
const currencies = window.g.allowedCurrencies || []
this.currencies = ['sat', ...currencies]
}
},
created: async function () {
await this.getZones()
await this.getCurrencies()
this.getCurrencies()
}
})

View file

@ -2,7 +2,7 @@ window.app.component('stall-list', {
name: 'stall-list',
template: '#stall-list',
delimiters: ['${', '}'],
props: [`adminkey`, 'inkey', 'wallet-options'],
props: ['adminkey', 'inkey', 'wallet-options'],
data: function () {
return {
filter: '',
@ -20,21 +20,21 @@ window.app.component('stall-list', {
shippingZones: []
}
},
editDialog: {
show: false,
data: {
id: '',
name: '',
description: '',
wallet: null,
currency: 'sat',
shippingZones: []
}
},
zoneOptions: [],
stallsTable: {
columns: [
{
name: '',
align: 'left',
label: '',
field: ''
},
{
name: 'id',
align: 'left',
label: 'Name',
field: 'id'
},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'currency',
align: 'left',
@ -45,14 +45,15 @@ window.app.component('stall-list', {
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
field: row => row.config?.description || ''
},
{
name: 'shippingZones',
align: 'left',
label: 'Shipping Zones',
field: 'shippingZones'
}
field: row => row.shipping_zones?.map(z => z.name).join(', ') || ''
},
{name: 'actions', align: 'right', label: 'Actions', field: ''}
],
pagination: {
rowsPerPage: 10
@ -65,6 +66,11 @@ window.app.component('stall-list', {
return this.zoneOptions.filter(
z => z.currency === this.stallDialog.data.currency
)
},
editFilteredZoneOptions: function () {
return this.zoneOptions.filter(
z => z.currency === this.editDialog.data.currency
)
}
},
methods: {
@ -94,7 +100,6 @@ window.app.component('stall-list', {
stall
)
this.stallDialog.show = false
data.expanded = false
this.stalls.unshift(data)
this.$q.notify({
type: 'positive',
@ -114,7 +119,6 @@ window.app.component('stall-list', {
stallData
)
this.stallDialog.show = false
data.expanded = false
this.stalls.unshift(data)
this.$q.notify({
type: 'positive',
@ -124,44 +128,66 @@ window.app.component('stall-list', {
LNbits.utils.notifyApiError(error)
}
},
deleteStall: async function (pendingStall) {
LNbits.utils
.confirmDialog(
`
Are you sure you want to delete this pending stall '${pendingStall.name}'?
`
)
.onOk(async () => {
try {
await LNbits.api.request(
'DELETE',
'/nostrmarket/api/v1/stall/' + pendingStall.id,
this.adminkey
)
this.$q.notify({
type: 'positive',
message: 'Pending Stall Deleted',
timeout: 5000
})
} catch (error) {
console.warn(error)
LNbits.utils.notifyApiError(error)
}
})
},
getCurrencies: async function () {
updateStall: async function () {
try {
const stallData = {
id: this.editDialog.data.id,
name: this.editDialog.data.name,
wallet: this.editDialog.data.wallet,
currency: this.editDialog.data.currency,
shipping_zones: this.editDialog.data.shippingZones,
config: {
description: this.editDialog.data.description
}
}
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/currencies',
this.inkey
'PUT',
`/nostrmarket/api/v1/stall/${stallData.id}`,
this.adminkey,
stallData
)
return ['sat', ...data]
this.editDialog.show = false
const index = this.stalls.findIndex(s => s.id === data.id)
if (index !== -1) {
this.stalls.splice(index, 1, data)
}
this.$q.notify({
type: 'positive',
message: 'Stall updated!'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
return []
},
deleteStall: async function (stall) {
try {
await LNbits.api.request(
'DELETE',
'/nostrmarket/api/v1/stall/' + stall.id,
this.adminkey
)
this.stalls = this.stalls.filter(s => s.id !== stall.id)
this.pendingStalls = this.pendingStalls.filter(s => s.id !== stall.id)
this.$q.notify({
type: 'positive',
message: 'Stall deleted'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
confirmDeleteStall: function (stall) {
LNbits.utils
.confirmDialog(
`Products and orders will be deleted also! Are you sure you want to delete stall "${stall.name}"?`
)
.onOk(async () => {
await this.deleteStall(stall)
})
},
getCurrencies: function () {
const currencies = window.g.allowedCurrencies || []
return ['sat', ...currencies]
},
getStalls: async function (pending = false) {
try {
@ -170,7 +196,7 @@ window.app.component('stall-list', {
`/nostrmarket/api/v1/stall?pending=${pending}`,
this.inkey
)
return data.map(s => ({...s, expanded: false}))
return data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
@ -194,20 +220,8 @@ window.app.component('stall-list', {
}
return []
},
handleStallDeleted: function (stallId) {
this.stalls = _.reject(this.stalls, function (obj) {
return obj.id === stallId
})
},
handleStallUpdated: function (stall) {
const index = this.stalls.findIndex(r => r.id === stall.id)
if (index !== -1) {
stall.expanded = true
this.stalls.splice(index, 1, stall)
}
},
openCreateStallDialog: async function (stallData) {
this.currencies = await this.getCurrencies()
this.currencies = this.getCurrencies()
this.zoneOptions = await this.getZones()
if (!this.zoneOptions || !this.zoneOptions.length) {
this.$q.notify({
@ -225,6 +239,24 @@ window.app.component('stall-list', {
}
this.stallDialog.show = true
},
openEditStallDialog: async function (stall) {
this.currencies = this.getCurrencies()
this.zoneOptions = await this.getZones()
this.editDialog.data = {
id: stall.id,
name: stall.name,
description: stall.config?.description || '',
wallet: stall.wallet,
currency: stall.currency,
shippingZones: (stall.shipping_zones || []).map(z => ({
...z,
label: z.name
? `${z.name} (${z.countries.join(', ')})`
: z.countries.join(', ')
}))
}
this.editDialog.show = true
},
openSelectPendingStallDialog: async function () {
this.stallDialog.showRestore = true
this.pendingStalls = await this.getStalls(true)
@ -246,8 +278,11 @@ window.app.component('stall-list', {
}))
})
},
customerSelectedForOrder: function (customerPubkey) {
this.$emit('customer-selected-for-order', customerPubkey)
goToProducts: function (stall) {
this.$emit('go-to-products', stall.id)
},
goToOrders: function (stall) {
this.$emit('go-to-orders', stall.id)
},
shortLabel(value = '') {
if (value.length <= 64) return value
@ -256,7 +291,7 @@ window.app.component('stall-list', {
},
created: async function () {
this.stalls = await this.getStalls()
this.currencies = await this.getCurrencies()
this.currencies = this.getCurrencies()
this.zoneOptions = await this.getZones()
}
})

View file

@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Generate the Nostr Market logo.
Requires: pip install Pillow
"""
from PIL import Image, ImageDraw # type: ignore[import-not-found]
# Render at 4x size for antialiasing
scale = 4
size = 128 * scale
final_size = 128
# Consistent color scheme with Nostr Proxy
dark_purple = (80, 40, 120)
light_purple = (140, 100, 180)
white = (255, 255, 255)
margin = 4 * scale
swoosh_center = ((128 + 100) * scale, -90 * scale)
swoosh_radius = 220 * scale
# Create rounded rectangle mask
mask = Image.new("L", (size, size), 0)
mask_draw = ImageDraw.Draw(mask)
corner_radius = 20 * scale
mask_draw.rounded_rectangle(
[margin, margin, size - margin, size - margin],
radius=corner_radius,
fill=255,
)
# Create background with swoosh
bg = Image.new("RGBA", (size, size), (0, 0, 0, 0))
bg_draw = ImageDraw.Draw(bg)
bg_draw.rounded_rectangle(
[margin, margin, size - margin, size - margin],
radius=corner_radius,
fill=dark_purple,
)
bg_draw.ellipse(
[
swoosh_center[0] - swoosh_radius,
swoosh_center[1] - swoosh_radius,
swoosh_center[0] + swoosh_radius,
swoosh_center[1] + swoosh_radius,
],
fill=light_purple,
)
# Apply rounded rectangle mask
final = Image.new("RGBA", (size, size), (0, 0, 0, 0))
final.paste(bg, mask=mask)
draw = ImageDraw.Draw(final)
center_x, center_y = size // 2, size // 2
# Shop/storefront - wider and shorter for shop look
shop_width = 80 * scale
awning_height = 18 * scale
body_height = 45 * scale
total_height = awning_height + body_height
shop_left = center_x - shop_width // 2
shop_right = center_x + shop_width // 2
# Center vertically
awning_top = center_y - total_height // 2
awning_bottom = awning_top + awning_height
shop_bottom = awning_bottom + body_height
awning_extend = 5 * scale
# Draw awning background (white base)
draw.rectangle(
[shop_left - awning_extend, awning_top, shop_right + awning_extend, awning_bottom],
fill=white,
)
# Vertical stripes on awning (alternating dark purple)
stripe_count = 8
stripe_width = (shop_width + 2 * awning_extend) // stripe_count
for i in range(1, stripe_count, 2):
x_left = shop_left - awning_extend + i * stripe_width
draw.rectangle(
[x_left, awning_top, x_left + stripe_width, awning_bottom],
fill=dark_purple,
)
# Shop body (below awning)
draw.rectangle(
[shop_left, awning_bottom, shop_right, shop_bottom],
fill=white,
)
# Large display window (shop style)
window_margin = 8 * scale
window_top = awning_bottom + 6 * scale
window_bottom = shop_bottom - 6 * scale
# Left display window
draw.rectangle(
[shop_left + window_margin, window_top, center_x - 10 * scale, window_bottom],
fill=dark_purple,
)
# Right display window
draw.rectangle(
[center_x + 10 * scale, window_top, shop_right - window_margin, window_bottom],
fill=dark_purple,
)
# Door (center, dark purple cutout)
door_width = 14 * scale
door_left = center_x - door_width // 2
draw.rectangle(
[door_left, window_top, door_left + door_width, shop_bottom],
fill=dark_purple,
)
# Downscale with LANCZOS for antialiasing
final = final.resize((final_size, final_size), Image.LANCZOS)
final.save("nostr-market.png")
print("Logo saved to nostr-market.png")

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -5,6 +5,8 @@ window.app = Vue.createApp({
mixins: [window.windowMixin],
data: function () {
return {
activeTab: 'merchant',
selectedStallFilter: null,
merchant: {},
shippingZones: [],
activeChatCustomer: '',
@ -295,6 +297,88 @@ window.app = Vue.createApp({
LNbits.utils.notifyApiError(error)
}
})
},
publishNip15: async function () {
try {
const {data: stalls} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/stall?pending=false',
this.g.user.wallets[0].inkey
)
for (const stall of stalls) {
await LNbits.api.request(
'PUT',
`/nostrmarket/api/v1/stall/${stall.id}`,
this.g.user.wallets[0].adminkey,
stall
)
}
// Fetch products from all stalls
let productCount = 0
for (const stall of stalls) {
const {data: products} = await LNbits.api.request(
'GET',
`/nostrmarket/api/v1/stall/product/${stall.id}?pending=false`,
this.g.user.wallets[0].inkey
)
for (const product of products) {
await LNbits.api.request(
'PATCH',
`/nostrmarket/api/v1/product/${product.id}`,
this.g.user.wallets[0].adminkey,
product
)
productCount++
}
}
this.$q.notify({
type: 'positive',
message: `Published ${stalls.length} stall(s) and ${productCount} product(s) to Nostr (NIP-15)`
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
refreshNip15: async function () {
LNbits.utils
.confirmDialog(
'This will sync your stalls and products from Nostr relays. Continue?'
)
.onOk(async () => {
try {
await LNbits.api.request(
'PUT',
'/nostrmarket/api/v1/restart',
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'positive',
message: 'Refreshing NIP-15 data from Nostr...'
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
})
},
deleteNip15: async function () {
LNbits.utils
.confirmDialog(
'WARNING: This will delete all your stalls and products from Nostr relays. This cannot be undone! Are you sure?'
)
.onOk(async () => {
this.$q.notify({
type: 'info',
message: 'Delete NIP-15 from Nostr not yet implemented'
})
})
},
goToProducts: function (stallId) {
this.selectedStallFilter = stallId
this.activeTab = 'products'
},
goToOrders: function (stallId) {
this.selectedStallFilter = stallId
this.activeTab = 'orders'
}
},
created: async function () {