feat: restructure UI with tab-based navigation (#119)
Reorganizes the merchant dashboard into a tab-based layout: - Tabs: Merchant | Shipping | Stalls | Products | Messages | Orders - Publish dropdown with NIP-15 options (NIP-99 disabled/coming soon) - Consistent UI patterns across all tabs: - Search/filter/buttons aligned right - Actions column on right side of tables - Equal-width tabs - Stalls tab: popup edit dialog, navigation to Products/Orders - Products tab: filter icon dropdown for stall filtering - Modular component structure for each tab section - Fixed product API calls to use correct endpoints Closes #119 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
17d13dbe6b
commit
71f458b9b9
10 changed files with 1605 additions and 384 deletions
37
static/components/merchant-tab.js
Normal file
37
static/components/merchant-tab.js
Normal 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')
|
||||
}
|
||||
}
|
||||
})
|
||||
256
static/components/product-list.js
Normal file
256
static/components/product-list.js
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
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()
|
||||
}
|
||||
})
|
||||
203
static/components/shipping-zones-list.js
Normal file
203
static/components/shipping-zones-list.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
window.app.component('shipping-zones-list', {
|
||||
name: 'shipping-zones-list',
|
||||
props: ['adminkey', 'inkey'],
|
||||
template: '#shipping-zones-list',
|
||||
delimiters: ['${', '}'],
|
||||
data: function () {
|
||||
return {
|
||||
zones: [],
|
||||
zoneDialog: {
|
||||
showDialog: false,
|
||||
data: {
|
||||
id: null,
|
||||
name: '',
|
||||
countries: [],
|
||||
cost: 0,
|
||||
currency: 'sat'
|
||||
}
|
||||
},
|
||||
currencies: [],
|
||||
shippingZoneOptions: [
|
||||
'Free (digital)',
|
||||
'Flat rate',
|
||||
'Worldwide',
|
||||
'Europe',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Belgium',
|
||||
'Brazil',
|
||||
'Canada',
|
||||
'Denmark',
|
||||
'Finland',
|
||||
'France',
|
||||
'Germany',
|
||||
'Greece',
|
||||
'Hong Kong',
|
||||
'Hungary',
|
||||
'Ireland',
|
||||
'Indonesia',
|
||||
'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',
|
||||
'China'
|
||||
],
|
||||
zonesTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
align: 'left',
|
||||
label: 'Name',
|
||||
field: 'name'
|
||||
},
|
||||
{
|
||||
name: 'countries',
|
||||
align: 'left',
|
||||
label: 'Countries',
|
||||
field: 'countries'
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
align: 'left',
|
||||
label: 'Currency',
|
||||
field: 'currency'
|
||||
},
|
||||
{
|
||||
name: 'cost',
|
||||
align: 'left',
|
||||
label: 'Cost',
|
||||
field: 'cost'
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
align: 'right',
|
||||
label: 'Actions',
|
||||
field: ''
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
|
@ -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,39 +20,25 @@ 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: 'currency',
|
||||
align: 'left',
|
||||
label: 'Currency',
|
||||
field: 'currency'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
label: 'Description',
|
||||
field: 'description'
|
||||
},
|
||||
{
|
||||
name: 'shippingZones',
|
||||
align: 'left',
|
||||
label: 'Shipping Zones',
|
||||
field: 'shippingZones'
|
||||
}
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'currency', align: 'left', label: 'Currency', field: 'currency'},
|
||||
{name: 'description', align: 'left', label: 'Description', field: row => row.config?.description || ''},
|
||||
{name: 'shippingZones', align: 'left', label: 'Shipping Zones', field: row => row.shipping_zones?.map(z => z.name).join(', ') || ''},
|
||||
{name: 'actions', align: 'right', label: 'Actions', field: ''}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
|
|
@ -65,6 +51,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 +85,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 +104,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 +113,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}'?
|
||||
`
|
||||
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(
|
||||
'PUT',
|
||||
`/nostrmarket/api/v1/stall/${stallData.id}`,
|
||||
this.adminkey,
|
||||
stallData
|
||||
)
|
||||
.onOk(async () => {
|
||||
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)
|
||||
}
|
||||
},
|
||||
deleteStall: async function (stall) {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/stall/' + pendingStall.id,
|
||||
'/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: 'Pending Stall Deleted',
|
||||
timeout: 5000
|
||||
message: 'Stall deleted'
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(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: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/currencies',
|
||||
this.inkey
|
||||
)
|
||||
|
||||
return ['sat', ...data]
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
getCurrencies: function () {
|
||||
const currencies = window.g.allowedCurrencies || []
|
||||
return ['sat', ...currencies]
|
||||
},
|
||||
getStalls: async function (pending = false) {
|
||||
try {
|
||||
|
|
@ -170,7 +181,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 +205,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 +224,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 +263,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 +276,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()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ window.app = Vue.createApp({
|
|||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
activeTab: 'merchant',
|
||||
selectedStallFilter: null,
|
||||
merchant: {},
|
||||
shippingZones: [],
|
||||
activeChatCustomer: '',
|
||||
|
|
@ -212,6 +214,86 @@ 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 () {
|
||||
|
|
|
|||
96
templates/nostrmarket/components/merchant-tab.html
Normal file
96
templates/nostrmarket/components/merchant-tab.html
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="row items-center q-col-gutter-sm q-mb-md">
|
||||
<div class="col-12 col-sm-auto">
|
||||
<merchant-details
|
||||
:merchant-id="merchantId"
|
||||
:inkey="inkey"
|
||||
:adminkey="adminkey"
|
||||
:show-keys="showKeys"
|
||||
@toggle-show-keys="toggleShowKeys"
|
||||
@merchant-deleted="handleMerchantDeleted"
|
||||
></merchant-details>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto q-mx-sm">
|
||||
<div class="row items-center no-wrap">
|
||||
<q-toggle
|
||||
:model-value="merchantActive"
|
||||
@update:model-value="toggleMerchantState()"
|
||||
size="md"
|
||||
checked-icon="check"
|
||||
color="primary"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
<span
|
||||
class="q-ml-sm"
|
||||
v-text="merchantActive ? 'Accepting Orders': 'Orders Paused'"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isAdmin" class="col-12 col-sm-auto q-ml-sm-auto">
|
||||
<q-btn
|
||||
label="Restart Nostr Connection"
|
||||
color="grey"
|
||||
outline
|
||||
size="sm"
|
||||
@click="restartNostrConnection"
|
||||
>
|
||||
<q-tooltip>
|
||||
Restart the connection to the nostrclient extension
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showKeys" class="q-mt-md">
|
||||
<div class="row q-mb-md">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="grey"
|
||||
outline
|
||||
@click="hideKeys"
|
||||
class="float-left"
|
||||
>Hide Keys</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<key-pair
|
||||
:public-key="publicKey"
|
||||
:private-key="privateKey"
|
||||
></key-pair>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-card flat bordered>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">Nostr Market Extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
<q-expansion-item group="api" dense icon="info" label="About">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
A decentralized marketplace powered by Nostr and Lightning Network.
|
||||
Create stalls, add products, and start selling with Bitcoin.
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense icon="link" label="Market Client">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<a :href="marketClientUrl" target="_blank">Open Market Client</a>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
289
templates/nostrmarket/components/product-list.html
Normal file
289
templates/nostrmarket/components/product-list.html
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
<div>
|
||||
<div class="row items-center q-mb-md q-col-gutter-sm justify-end">
|
||||
<div class="col-auto">
|
||||
<q-input
|
||||
dense
|
||||
debounce="300"
|
||||
v-model="filter"
|
||||
placeholder="Search by name, stall..."
|
||||
style="min-width: 250px"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn-dropdown
|
||||
flat
|
||||
dense
|
||||
:icon="selectedStall ? 'filter_alt' : 'filter_alt_off'"
|
||||
:color="selectedStall ? 'primary' : 'grey'"
|
||||
>
|
||||
<q-list>
|
||||
<q-item clickable v-close-popup @click="selectedStall = null">
|
||||
<q-item-section>
|
||||
<q-item-label>All Stalls</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side v-if="!selectedStall">
|
||||
<q-icon name="check" color="primary"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator></q-separator>
|
||||
<q-item
|
||||
v-for="stall in stallOptions"
|
||||
:key="stall.value"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="selectedStall = stall.value"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="stall.label"></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side v-if="selectedStall === stall.value">
|
||||
<q-icon name="check" color="primary"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
@click="openSelectPendingProductDialog"
|
||||
outline
|
||||
color="primary"
|
||||
icon="restore"
|
||||
label="Restore Product"
|
||||
:disable="!stalls.length"
|
||||
class="q-px-md"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
@click="showNewProductDialog()"
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="New Product"
|
||||
:disable="!stalls.length"
|
||||
class="q-px-md"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!stalls.length" class="text-center q-pa-lg text-grey">
|
||||
<q-icon name="info" size="md" class="q-mb-sm"></q-icon>
|
||||
<div>No stalls found. Please create a stall first in the Stalls tab.</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
v-else
|
||||
flat
|
||||
dense
|
||||
:rows="filteredProducts"
|
||||
row-key="id"
|
||||
:columns="productsTable.columns"
|
||||
v-model:pagination="productsTable.pagination"
|
||||
:filter="filter"
|
||||
>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="name" :props="props">
|
||||
<span v-text="shortLabel(props.row.name)"></span>
|
||||
</q-td>
|
||||
<q-td key="stall" :props="props">
|
||||
<span v-text="getStallName(props.row.stall_id)"></span>
|
||||
</q-td>
|
||||
<q-td key="price" :props="props">
|
||||
<span v-text="props.row.price"></span>
|
||||
</q-td>
|
||||
<q-td key="quantity" :props="props">
|
||||
<span v-text="props.row.quantity"></span>
|
||||
</q-td>
|
||||
<q-td key="actions" :props="props">
|
||||
<q-toggle
|
||||
@update:model-value="toggleProductActive(props.row)"
|
||||
size="xs"
|
||||
checked-icon="check"
|
||||
:model-value="props.row.active"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
>
|
||||
<q-tooltip v-if="props.row.active">Product is active - click to deactivate</q-tooltip>
|
||||
<q-tooltip v-else>Product is inactive - click to activate</q-tooltip>
|
||||
</q-toggle>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
dense
|
||||
flat
|
||||
@click="editProduct(props.row)"
|
||||
icon="edit"
|
||||
>
|
||||
<q-tooltip>Edit product</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="negative"
|
||||
dense
|
||||
flat
|
||||
@click="deleteProduct(props.row)"
|
||||
icon="delete"
|
||||
>
|
||||
<q-tooltip>Delete product</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<!-- Product Dialog -->
|
||||
<q-dialog v-model="productDialog.showDialog" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendProductFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
v-if="!productDialog.data.id"
|
||||
filled
|
||||
dense
|
||||
v-model="productDialog.data.stall_id"
|
||||
:options="stallOptions"
|
||||
label="Stall *"
|
||||
emit-value
|
||||
map-options
|
||||
></q-select>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="productDialog.data.name"
|
||||
label="Name *"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="productDialog.data.config.description"
|
||||
label="Description"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mb-sm">
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="productDialog.data.price"
|
||||
type="number"
|
||||
:label="'Price (' + getStallCurrency(productDialog.data.stall_id) + ') *'"
|
||||
:step="getStallCurrency(productDialog.data.stall_id) != 'sat' ? '0.01' : '1'"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-ml-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="productDialog.data.quantity"
|
||||
type="number"
|
||||
label="Quantity *"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-expansion-item group="advanced" label="Categories" caption="Add tags to products">
|
||||
<div class="q-pl-sm q-pt-sm">
|
||||
<q-select
|
||||
filled
|
||||
multiple
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="productDialog.data.categories"
|
||||
use-input
|
||||
use-chips
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add-unique"
|
||||
label="Categories (Hit Enter to add)"
|
||||
></q-select>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item group="advanced" label="Images" caption="Add images for product">
|
||||
<div class="q-pl-sm q-pt-sm">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="productDialog.data.image"
|
||||
@keydown.enter.prevent="addProductImage"
|
||||
type="url"
|
||||
label="Image URL"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-btn @click="addProductImage" dense flat icon="add"></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-chip
|
||||
v-for="imageUrl in productDialog.data.images"
|
||||
:key="imageUrl"
|
||||
removable
|
||||
@remove="removeProductImage(imageUrl)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
<span v-text="imageUrl.split('/').pop()"></span>
|
||||
</q-chip>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="productDialog.data.id"
|
||||
type="submit"
|
||||
:label="productDialog.data.pending ? 'Restore Product' : 'Update Product'"
|
||||
unelevated
|
||||
color="primary"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="!productDialog.data.stall_id || !productDialog.data.price || !productDialog.data.name || !productDialog.data.quantity"
|
||||
type="submit"
|
||||
>Create Product</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Restore Dialog -->
|
||||
<q-dialog v-model="productDialog.showRestore" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<div v-if="pendingProducts && pendingProducts.length" class="row q-mt-lg">
|
||||
<q-item
|
||||
v-for="pendingProduct of pendingProducts"
|
||||
:key="pendingProduct.id"
|
||||
tag="label"
|
||||
class="full-width"
|
||||
v-ripple
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label><span v-text="pendingProduct.name"></span></q-item-label>
|
||||
<q-item-label caption><span v-text="pendingProduct.config?.description"></span></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section class="q-pl-xl float-right">
|
||||
<q-btn
|
||||
@click="openRestoreProductDialog(pendingProduct)"
|
||||
v-close-popup
|
||||
flat
|
||||
color="green"
|
||||
class="q-ml-auto float-right"
|
||||
>Restore</q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div v-else>There are no products to be restored.</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
122
templates/nostrmarket/components/shipping-zones-list.html
Normal file
122
templates/nostrmarket/components/shipping-zones-list.html
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<div>
|
||||
<div class="row items-center q-mb-md q-col-gutter-sm justify-end">
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="New Shipping Zone"
|
||||
@click="openZoneDialog()"
|
||||
class="q-px-md"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
flat
|
||||
dense
|
||||
:rows="zones"
|
||||
row-key="id"
|
||||
:columns="zonesTable.columns"
|
||||
v-model:pagination="zonesTable.pagination"
|
||||
>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="name" :props="props">
|
||||
<span v-text="props.row.name || '(unnamed)'"></span>
|
||||
</q-td>
|
||||
<q-td key="countries" :props="props">
|
||||
<span v-text="props.row.countries.join(', ')"></span>
|
||||
</q-td>
|
||||
<q-td key="currency" :props="props">
|
||||
<span v-text="props.row.currency"></span>
|
||||
</q-td>
|
||||
<q-td key="cost" :props="props">
|
||||
<span v-text="props.row.cost"></span>
|
||||
</q-td>
|
||||
<q-td key="actions" :props="props">
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
dense
|
||||
flat
|
||||
icon="edit"
|
||||
@click="openZoneDialog(props.row)"
|
||||
>
|
||||
<q-tooltip>Edit zone</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="negative"
|
||||
dense
|
||||
flat
|
||||
icon="delete"
|
||||
@click="confirmDeleteZone(props.row)"
|
||||
>
|
||||
<q-tooltip>Delete zone</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-dialog v-model="zoneDialog.showDialog" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendZoneFormData" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
label="Zone Name"
|
||||
type="text"
|
||||
v-model.trim="zoneDialog.data.name"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
multiple
|
||||
:options="shippingZoneOptions"
|
||||
label="Countries"
|
||||
v-model="zoneDialog.data.countries"
|
||||
></q-select>
|
||||
<q-select
|
||||
:disabled="!!zoneDialog.data.id"
|
||||
:readonly="!!zoneDialog.data.id"
|
||||
filled
|
||||
dense
|
||||
v-model="zoneDialog.data.currency"
|
||||
type="text"
|
||||
label="Unit"
|
||||
:options="currencies"
|
||||
></q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
:label="'Cost of Shipping (' + zoneDialog.data.currency + ') *'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:step="zoneDialog.data.currency != 'sat' ? '0.01' : '1'"
|
||||
type="number"
|
||||
v-model.trim="zoneDialog.data.cost"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<div v-if="zoneDialog.data.id">
|
||||
<q-btn unelevated color="primary" type="submit">Update</q-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="!zoneDialog.data.countries || !zoneDialog.data.countries.length"
|
||||
type="submit"
|
||||
>Create Shipping Zone</q-btn
|
||||
>
|
||||
</div>
|
||||
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
|
@ -1,43 +1,38 @@
|
|||
<div>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col q-pr-lg">
|
||||
<q-btn-dropdown
|
||||
@click="openCreateStallDialog()"
|
||||
outline
|
||||
unelevated
|
||||
split
|
||||
class="float-left"
|
||||
color="primary"
|
||||
label="New Stall (Store)"
|
||||
>
|
||||
<q-item @click="openCreateStallDialog()" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>New Stall</q-item-label>
|
||||
<q-item-label caption>Create a new stall</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="openSelectPendingStallDialog" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Restore Stall</q-item-label>
|
||||
<q-item-label caption
|
||||
>Restore existing stall from Nostr</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-btn-dropdown>
|
||||
<div class="row items-center q-mb-md q-col-gutter-sm justify-end">
|
||||
<div class="col-auto">
|
||||
<q-input
|
||||
borderless
|
||||
dense
|
||||
debounce="300"
|
||||
v-model="filter"
|
||||
placeholder="Search"
|
||||
class="float-right"
|
||||
placeholder="Search by name, currency..."
|
||||
style="min-width: 250px"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
@click="openSelectPendingStallDialog"
|
||||
outline
|
||||
color="primary"
|
||||
icon="restore"
|
||||
label="Restore Stall"
|
||||
class="q-px-md"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
@click="openCreateStallDialog()"
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="New Stall"
|
||||
class="q-px-md"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
|
|
@ -51,57 +46,65 @@
|
|||
>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
round
|
||||
dense
|
||||
@click="props.row.expanded= !props.row.expanded"
|
||||
:icon="props.row.expanded? 'remove' : 'add'"
|
||||
/>
|
||||
<q-td key="name" :props="props">
|
||||
<span v-text="shortLabel(props.row.name)"></span>
|
||||
</q-td>
|
||||
|
||||
<q-td key="id" :props="props"
|
||||
><span v-text="shortLabel(props.row.name)"></span
|
||||
></q-td>
|
||||
<q-td key="currency" :props="props"
|
||||
><span v-text="props.row.currency"></span>
|
||||
<q-td key="currency" :props="props">
|
||||
<span v-text="props.row.currency"></span>
|
||||
</q-td>
|
||||
<q-td key="description" :props="props">
|
||||
<span v-text="shortLabel(props.row.config.description)"></span>
|
||||
</q-td>
|
||||
<q-td key="shippingZones" :props="props">
|
||||
<div>
|
||||
<span
|
||||
v-text="shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))"
|
||||
></span>
|
||||
</div>
|
||||
<span v-text="shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))"></span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
<q-tr v-if="props.row.expanded" :props="props">
|
||||
<q-td colspan="100%">
|
||||
<div class="row items-center q-mb-lg">
|
||||
<div class="col-12">
|
||||
<stall-details
|
||||
:stall-id="props.row.id"
|
||||
:adminkey="adminkey"
|
||||
:inkey="inkey"
|
||||
:wallet-options="walletOptions"
|
||||
:zone-options="zoneOptions"
|
||||
:currencies="currencies"
|
||||
@stall-deleted="handleStallDeleted"
|
||||
@stall-updated="handleStallUpdated"
|
||||
@customer-selected-for-order="customerSelectedForOrder"
|
||||
></stall-details>
|
||||
</div>
|
||||
</div>
|
||||
<q-td key="actions" :props="props">
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="primary"
|
||||
dense
|
||||
flat
|
||||
icon="edit"
|
||||
@click="openEditStallDialog(props.row)"
|
||||
>
|
||||
<q-tooltip>Edit stall</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="secondary"
|
||||
dense
|
||||
flat
|
||||
icon="inventory_2"
|
||||
@click="goToProducts(props.row)"
|
||||
>
|
||||
<q-tooltip>View products</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="accent"
|
||||
dense
|
||||
flat
|
||||
icon="receipt"
|
||||
@click="goToOrders(props.row)"
|
||||
>
|
||||
<q-tooltip>View orders</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="negative"
|
||||
dense
|
||||
flat
|
||||
icon="delete"
|
||||
@click="confirmDeleteStall(props.row)"
|
||||
>
|
||||
<q-tooltip>Delete stall</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<div>
|
||||
<!-- Create Stall Dialog -->
|
||||
<q-dialog v-model="stallDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendStallFormData" class="q-gutter-md">
|
||||
|
|
@ -164,6 +167,75 @@
|
|||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Edit Stall Dialog -->
|
||||
<q-dialog v-model="editDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="updateStall" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="editDialog.data.id"
|
||||
label="ID"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="editDialog.data.name"
|
||||
label="Name"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="editDialog.data.description"
|
||||
type="textarea"
|
||||
rows="3"
|
||||
label="Description"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="editDialog.data.wallet"
|
||||
:options="walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="editDialog.data.currency"
|
||||
type="text"
|
||||
label="Unit"
|
||||
:options="currencies"
|
||||
></q-select>
|
||||
<q-select
|
||||
:options="editFilteredZoneOptions"
|
||||
filled
|
||||
dense
|
||||
multiple
|
||||
v-model.trim="editDialog.data.shippingZones"
|
||||
label="Shipping Zones"
|
||||
></q-select>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
label="Update Stall"
|
||||
></q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Restore Stall Dialog -->
|
||||
<q-dialog v-model="stallDialog.showRestore" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<div v-if="pendingStalls && pendingStalls.length" class="row q-mt-lg">
|
||||
|
|
@ -211,4 +283,3 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,83 +1,161 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<div class="col-12">
|
||||
<div v-if="merchant && merchant.id">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center q-col-gutter-sm">
|
||||
<div class="col-12 col-sm-auto">
|
||||
<merchant-details
|
||||
<div class="row items-center no-wrap">
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
class="text-grey col"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="left"
|
||||
>
|
||||
<q-tab name="merchant" label="Merchant" icon="person" style="min-width: 120px"></q-tab>
|
||||
<q-tab name="shipping" label="Shipping" icon="local_shipping" style="min-width: 120px"></q-tab>
|
||||
<q-tab name="stalls" label="Stalls" icon="store" style="min-width: 120px"></q-tab>
|
||||
<q-tab name="products" label="Products" icon="inventory_2" style="min-width: 120px"></q-tab>
|
||||
<q-tab name="messages" label="Messages" icon="chat" style="min-width: 120px"></q-tab>
|
||||
<q-tab name="orders" label="Orders" icon="receipt" style="min-width: 120px"></q-tab>
|
||||
</q-tabs>
|
||||
<div class="col-auto q-mr-md">
|
||||
<q-btn-dropdown
|
||||
color="primary"
|
||||
label="Publish"
|
||||
icon="publish"
|
||||
unelevated
|
||||
>
|
||||
<q-list>
|
||||
<q-item clickable v-close-popup @click="publishNip15">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="store" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Publish NIP-15</q-item-label>
|
||||
<q-item-label caption>Publish stalls and products</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item disable>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="sell" color="grey" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey">Publish NIP-99</q-item-label>
|
||||
<q-item-label caption>Classified listings (coming soon)</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup @click="refreshNip15">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="refresh" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Refresh NIP-15 from Nostr</q-item-label>
|
||||
<q-item-label caption>Sync stalls and products</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item disable>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="refresh" color="grey" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey">Refresh NIP-99 from Nostr</q-item-label>
|
||||
<q-item-label caption>Classified listings (coming soon)</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup @click="deleteNip15">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="delete_forever" color="negative" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-negative">Delete NIP-15 from Nostr</q-item-label>
|
||||
<q-item-label caption>Remove stalls and products</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item disable>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="delete_forever" color="grey" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey">Delete NIP-99 from Nostr</q-item-label>
|
||||
<q-item-label caption>Classified listings (coming soon)</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-tab-panels v-model="activeTab" animated>
|
||||
<!-- Merchant Tab -->
|
||||
<q-tab-panel name="merchant">
|
||||
<merchant-tab
|
||||
:merchant-id="merchant.id"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:show-keys="showKeys"
|
||||
@toggle-show-keys="toggleShowKeys"
|
||||
@merchant-deleted="handleMerchantDeleted"
|
||||
></merchant-details>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto q-mx-sm">
|
||||
<div class="row items-center no-wrap">
|
||||
<q-toggle
|
||||
@update:model-value="toggleMerchantState()"
|
||||
size="md"
|
||||
checked-icon="check"
|
||||
v-model="merchant.config.active"
|
||||
color="primary"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
<span
|
||||
class="q-ml-sm"
|
||||
v-text="merchant.config.active ? 'Accepting Orders': 'Orders Paused'"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto q-ml-sm-auto">
|
||||
<shipping-zones
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
></shipping-zones>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="showKeys">
|
||||
<div class="row q-mb-md">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="grey"
|
||||
outline
|
||||
@click="showKeys = false"
|
||||
class="float-left"
|
||||
>Hide Keys</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<key-pair
|
||||
:merchant-active="merchant.config.active"
|
||||
:public-key="merchant.public_key"
|
||||
:private-key="merchant.private_key"
|
||||
></key-pair>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="q-mt-lg">
|
||||
<q-card-section>
|
||||
:is-admin="g.user.admin"
|
||||
@toggle-show-keys="toggleShowKeys"
|
||||
@hide-keys="showKeys = false"
|
||||
@merchant-deleted="handleMerchantDeleted"
|
||||
@toggle-merchant-state="toggleMerchantState"
|
||||
@restart-nostr-connection="restartNostrConnection"
|
||||
></merchant-tab>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Shipping Tab -->
|
||||
<q-tab-panel name="shipping">
|
||||
<shipping-zones-list
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
></shipping-zones-list>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Stalls Tab -->
|
||||
<q-tab-panel name="stalls">
|
||||
<stall-list
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:wallet-options="g.user.walletOptions"
|
||||
@customer-selected-for-order="customerSelectedForOrder"
|
||||
@go-to-products="goToProducts"
|
||||
@go-to-orders="goToOrders"
|
||||
></stall-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="q-mt-lg">
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Products Tab -->
|
||||
<q-tab-panel name="products">
|
||||
<product-list
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:stall-filter="selectedStallFilter"
|
||||
@clear-filter="selectedStallFilter = null"
|
||||
></product-list>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Messages Tab -->
|
||||
<q-tab-panel name="messages">
|
||||
<direct-messages
|
||||
ref="directMessagesRef"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:active-chat-customer="activeChatCustomer"
|
||||
:merchant-id="merchant.id"
|
||||
@customer-selected="filterOrdersForCustomer"
|
||||
@order-selected="showOrderDetails"
|
||||
>
|
||||
</direct-messages>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Orders Tab -->
|
||||
<q-tab-panel name="orders">
|
||||
<order-list
|
||||
ref="orderListRef"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
|
|
@ -85,9 +163,8 @@
|
|||
:customer-pubkey-filter="orderPubkey"
|
||||
@customer-selected="customerSelectedForOrder"
|
||||
></order-list>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-card v-else>
|
||||
|
|
@ -147,50 +224,6 @@
|
|||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<div v-if="g.user.admin" class="col-12 q-mb-lg">
|
||||
<q-card>
|
||||
<q-card-section class="q-pa-md">
|
||||
<q-btn
|
||||
label="Restart Nostr Connection"
|
||||
color="grey"
|
||||
outline
|
||||
@click="restartNostrConnection"
|
||||
>
|
||||
<q-tooltip>
|
||||
Restart the connection to the nostrclient extension
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} Nostr Market Extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "nostrmarket/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div v-if="merchant && merchant.id" class="col-12">
|
||||
<direct-messages
|
||||
ref="directMessagesRef"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:active-chat-customer="activeChatCustomer"
|
||||
:merchant-id="merchant.id"
|
||||
@customer-selected="filterOrdersForCustomer"
|
||||
@order-selected="showOrderDetails"
|
||||
>
|
||||
</direct-messages>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<q-dialog v-model="importKeyDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
|
|
@ -257,6 +290,15 @@
|
|||
<template id="merchant-details"
|
||||
>{% include("nostrmarket/components/merchant-details.html") %}</template
|
||||
>
|
||||
<template id="merchant-tab"
|
||||
>{% include("nostrmarket/components/merchant-tab.html") %}</template
|
||||
>
|
||||
<template id="shipping-zones-list"
|
||||
>{% include("nostrmarket/components/shipping-zones-list.html") %}</template
|
||||
>
|
||||
<template id="product-list"
|
||||
>{% include("nostrmarket/components/product-list.html") %}</template
|
||||
>
|
||||
|
||||
<script src="{{ url_for('nostrmarket_static', path='js/nostr.bundle.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
||||
|
|
@ -269,5 +311,8 @@
|
|||
<script src="{{ static_url_for('nostrmarket/static', 'components/order-list.js') }}"></script>
|
||||
<script src="{{ static_url_for('nostrmarket/static', 'components/direct-messages.js') }}"></script>
|
||||
<script src="{{ static_url_for('nostrmarket/static', 'components/merchant-details.js') }}"></script>
|
||||
<script src="{{ static_url_for('nostrmarket/static', 'components/merchant-tab.js') }}"></script>
|
||||
<script src="{{ static_url_for('nostrmarket/static', 'components/shipping-zones-list.js') }}"></script>
|
||||
<script src="{{ static_url_for('nostrmarket/static', 'components/product-list.js') }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue