Update
@@ -93,7 +82,7 @@
Create Shipping Zone
diff --git a/static/components/shipping-zones/shipping-zones.js b/static/components/shipping-zones/shipping-zones.js
new file mode 100644
index 0000000..173d713
--- /dev/null
+++ b/static/components/shipping-zones/shipping-zones.js
@@ -0,0 +1,186 @@
+async function shippingZones(path) {
+ const template = await loadTemplateAsync(path)
+ Vue.component('shipping-zones', {
+ name: 'shipping-zones',
+ props: ['adminkey', 'inkey'],
+ template,
+
+ 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'
+ ]
+ }
+ },
+ methods: {
+ openZoneDialog: function (data) {
+ data = data || {
+ id: null,
+ name: '',
+ countries: [],
+ cost: 0,
+ currency: 'sat'
+ }
+ this.zoneDialog.data = data
+
+ this.zoneDialog.showDialog = true
+ },
+ createZone: async function () {
+ try {
+ const {data} = await LNbits.api.request(
+ 'POST',
+ '/nostrmarket/api/v1/zone',
+ this.adminkey,
+ {}
+ )
+ this.zones = data
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ 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)
+ }
+ },
+ deleteShippingZone: async function () {
+ try {
+ await LNbits.api.request(
+ 'DELETE',
+ `/nostrmarket/api/v1/zone/${this.zoneDialog.data.id}`,
+ this.adminkey
+ )
+ this.$q.notify({
+ type: 'positive',
+ message: 'Zone deleted!'
+ })
+ await this.getZones()
+ this.zoneDialog.showDialog = false
+ } catch (error) {
+ 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)
+ }
+ }
+ },
+ created: async function () {
+ await this.getZones()
+ await this.getCurrencies()
+ }
+ })
+}
diff --git a/static/components/stall-details.js b/static/components/stall-details.js
deleted file mode 100644
index be0633b..0000000
--- a/static/components/stall-details.js
+++ /dev/null
@@ -1,338 +0,0 @@
-window.app.component('stall-details', {
- name: 'stall-details',
- template: '#stall-details',
- delimiters: ['${', '}'],
- props: [
- 'stall-id',
- 'adminkey',
- 'inkey',
- 'wallet-options',
- 'zone-options',
- 'currencies'
- ],
- data: function () {
- return {
- tab: 'products',
- stall: null,
- products: [],
- pendingProducts: [],
- productDialog: {
- showDialog: false,
- showRestore: false,
- url: true,
- data: null
- },
- productsFilter: '',
- productsTable: {
- columns: [
- {
- name: 'delete',
- align: 'left',
- label: '',
- field: ''
- },
- {
- name: 'edit',
- align: 'left',
- label: '',
- field: ''
- },
- {
- name: 'activate',
- align: 'left',
- label: '',
- field: ''
- },
-
- {
- name: 'id',
- align: 'left',
- label: 'ID',
- field: 'id'
- },
- {
- name: 'name',
- align: 'left',
- label: 'Name',
- field: 'name'
- },
- {
- name: 'price',
- align: 'left',
- label: 'Price',
- field: 'price'
- },
- {
- name: 'quantity',
- align: 'left',
- label: 'Quantity',
- field: 'quantity'
- }
- ],
- pagination: {
- rowsPerPage: 10
- }
- }
- }
- },
- computed: {
- filteredZoneOptions: function () {
- if (!this.stall) return []
- return this.zoneOptions.filter(z => z.currency === this.stall.currency)
- }
- },
- methods: {
- mapStall: function (stall) {
- stall.shipping_zones.forEach(
- z =>
- (z.label = z.name
- ? `${z.name} (${z.countries.join(', ')})`
- : z.countries.join(', '))
- )
- return stall
- },
- newEmtpyProductData: function () {
- return {
- id: null,
- name: '',
- categories: [],
- images: [],
- image: null,
- price: 0,
-
- quantity: 0,
- config: {
- description: '',
- use_autoreply: false,
- autoreply_message: '',
- shipping: (this.stall.shipping_zones || []).map(z => ({
- id: z.id,
- name: z.name,
- cost: 0
- }))
- }
- }
- },
- getStall: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/nostrmarket/api/v1/stall/' + this.stallId,
- this.inkey
- )
- this.stall = this.mapStall(data)
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- updateStall: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'PUT',
- '/nostrmarket/api/v1/stall/' + this.stallId,
- this.adminkey,
- this.stall
- )
- this.stall = this.mapStall(data)
- this.$emit('stall-updated', this.stall)
- this.$q.notify({
- type: 'positive',
- message: 'Stall Updated',
- timeout: 5000
- })
- } catch (error) {
- console.warn(error)
- LNbits.utils.notifyApiError(error)
- }
- },
- deleteStall: function () {
- LNbits.utils
- .confirmDialog(
- `
- Products and orders will be deleted also!
- Are you sure you want to delete this stall?
- `
- )
- .onOk(async () => {
- try {
- await LNbits.api.request(
- 'DELETE',
- '/nostrmarket/api/v1/stall/' + this.stallId,
- this.adminkey
- )
- this.$emit('stall-deleted', this.stallId)
- this.$q.notify({
- type: 'positive',
- message: 'Stall Deleted',
- timeout: 5000
- })
- } catch (error) {
- console.warn(error)
- LNbits.utils.notifyApiError(error)
- }
- })
- },
- addProductImage: function () {
- if (!isValidImageUrl(this.productDialog.data.image)) {
- this.$q.notify({
- type: 'warning',
- message: 'Not a valid image URL',
- timeout: 5000
- })
- return
- }
- 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)
- }
- },
- getProducts: async function (pending = false) {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- `/nostrmarket/api/v1/stall/product/${this.stall.id}?pending=${pending}`,
- this.inkey
- )
- return data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- sendProductFormData: function () {
- const data = {
- stall_id: this.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
- this.updateProduct(data)
- } else {
- this.createProduct(data)
- }
- },
- 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(r => r.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',
- timeout: 5000
- })
- } catch (error) {
- console.warn(error)
- LNbits.utils.notifyApiError(error)
- }
- },
- 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',
- timeout: 5000
- })
- } catch (error) {
- console.warn(error)
- LNbits.utils.notifyApiError(error)
- }
- },
- editProduct: async function (product) {
- const emptyShipping = this.newEmtpyProductData().config.shipping
- this.productDialog.data = {...product}
- this.productDialog.data.config.shipping = emptyShipping.map(
- shippingZone => {
- const existingShippingCost = (product.config.shipping || []).find(
- ps => ps.id === shippingZone.id
- )
- shippingZone.cost = existingShippingCost?.cost || 0
- return shippingZone
- }
- )
-
- this.productDialog.showDialog = true
- },
- deleteProduct: async function (productId) {
- LNbits.utils
- .confirmDialog('Are you sure you want to delete this product?')
- .onOk(async () => {
- try {
- await LNbits.api.request(
- 'DELETE',
- '/nostrmarket/api/v1/product/' + productId,
- this.adminkey
- )
- this.products = _.reject(this.products, function (obj) {
- return obj.id === productId
- })
- this.$q.notify({
- type: 'positive',
- message: 'Product deleted',
- timeout: 5000
- })
- } catch (error) {
- console.warn(error)
- LNbits.utils.notifyApiError(error)
- }
- })
- },
- showNewProductDialog: async function (data) {
- this.productDialog.data = data || this.newEmtpyProductData()
- this.productDialog.showDialog = true
- },
- openSelectPendingProductDialog: async function () {
- this.productDialog.showRestore = true
- this.pendingProducts = await this.getProducts(true)
- },
- openRestoreProductDialog: async function (pendingProduct) {
- pendingProduct.pending = true
- await this.showNewProductDialog(pendingProduct)
- },
- restoreAllPendingProducts: async function () {
- for (const p of this.pendingProducts) {
- p.pending = false
- await this.updateProduct(p)
- }
- },
- customerSelectedForOrder: function (customerPubkey) {
- this.$emit('customer-selected-for-order', customerPubkey)
- },
- shortLabel(value = '') {
- if (value.length <= 44) return value
- return value.substring(0, 40) + '...'
- }
- },
- created: async function () {
- await this.getStall()
- this.products = await this.getProducts()
- this.productDialog.data = this.newEmtpyProductData()
- }
-})
diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html
new file mode 100644
index 0000000..1a16461
--- /dev/null
+++ b/static/components/stall-details/stall-details.html
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Shipping Zones:
+
+
+
+
+
+
+
+
+ Update Stall
+
+
+ Delete Stall
+
+
+
+
+
+
+
+
+
+
+
+ New Product
+ Create a new product
+
+
+
+
+ Restore Product
+ Restore existing product from Nostr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{props.row.id}}
+ {{shortLabel(props.row.name)}}
+ {{props.row.price}}
+
+ {{props.row.quantity}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create Product
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Restore
+
+
+
+
+
+
+
+ There are no products to be restored.
+
+
+ Restore All
+ Close
+
+
+
+
\ No newline at end of file
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js
new file mode 100644
index 0000000..32fcad5
--- /dev/null
+++ b/static/components/stall-details/stall-details.js
@@ -0,0 +1,328 @@
+async function stallDetails(path) {
+ const template = await loadTemplateAsync(path)
+
+ Vue.component('stall-details', {
+ name: 'stall-details',
+ template,
+
+ props: [
+ 'stall-id',
+ 'adminkey',
+ 'inkey',
+ 'wallet-options',
+ 'zone-options',
+ 'currencies'
+ ],
+ data: function () {
+ return {
+ tab: 'products',
+ stall: null,
+ products: [],
+ pendingProducts: [],
+ productDialog: {
+ showDialog: false,
+ showRestore: false,
+ url: true,
+ data: null
+ },
+ productsFilter: '',
+ productsTable: {
+ columns: [
+ {
+ name: 'delete',
+ align: 'left',
+ label: '',
+ field: ''
+ },
+ {
+ name: 'edit',
+ align: 'left',
+ label: '',
+ field: ''
+ },
+
+ {
+ name: 'id',
+ align: 'left',
+ label: 'ID',
+ field: 'id'
+ },
+ {
+ name: 'name',
+ align: 'left',
+ label: 'Name',
+ field: 'name'
+ },
+ {
+ name: 'price',
+ align: 'left',
+ label: 'Price',
+ field: 'price'
+ },
+ {
+ name: 'quantity',
+ align: 'left',
+ label: 'Quantity',
+ field: 'quantity'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10
+ }
+ }
+ }
+ },
+ computed: {
+ filteredZoneOptions: function () {
+ if (!this.stall) return []
+ return this.zoneOptions.filter(z => z.currency === this.stall.currency)
+ }
+ },
+ methods: {
+ mapStall: function (stall) {
+ stall.shipping_zones.forEach(
+ z =>
+ (z.label = z.name
+ ? `${z.name} (${z.countries.join(', ')})`
+ : z.countries.join(', '))
+ )
+ return stall
+ },
+ newEmtpyProductData: function() {
+ return {
+ id: null,
+ name: '',
+ categories: [],
+ images: [],
+ image: null,
+ price: 0,
+
+ quantity: 0,
+ config: {
+ description: '',
+ use_autoreply: false,
+ autoreply_message: '',
+ shipping: (this.stall.shipping_zones || []).map(z => ({id: z.id, name: z.name, cost: 0}))
+ }
+ }
+ },
+ getStall: async function () {
+ try {
+ const { data } = await LNbits.api.request(
+ 'GET',
+ '/nostrmarket/api/v1/stall/' + this.stallId,
+ this.inkey
+ )
+ this.stall = this.mapStall(data)
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ updateStall: async function () {
+ try {
+ const { data } = await LNbits.api.request(
+ 'PUT',
+ '/nostrmarket/api/v1/stall/' + this.stallId,
+ this.adminkey,
+ this.stall
+ )
+ this.stall = this.mapStall(data)
+ this.$emit('stall-updated', this.stall)
+ this.$q.notify({
+ type: 'positive',
+ message: 'Stall Updated',
+ timeout: 5000
+ })
+ } catch (error) {
+ console.warn(error)
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ deleteStall: function () {
+ LNbits.utils
+ .confirmDialog(
+ `
+ Products and orders will be deleted also!
+ Are you sure you want to delete this stall?
+ `
+ )
+ .onOk(async () => {
+ try {
+ await LNbits.api.request(
+ 'DELETE',
+ '/nostrmarket/api/v1/stall/' + this.stallId,
+ this.adminkey
+ )
+ this.$emit('stall-deleted', this.stallId)
+ this.$q.notify({
+ type: 'positive',
+ message: 'Stall Deleted',
+ timeout: 5000
+ })
+ } catch (error) {
+ console.warn(error)
+ LNbits.utils.notifyApiError(error)
+ }
+ })
+ },
+ addProductImage: function () {
+ if (!isValidImageUrl(this.productDialog.data.image)) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Not a valid image URL',
+ timeout: 5000
+ })
+ return
+ }
+ 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)
+ }
+ },
+ getProducts: async function (pending = false) {
+ try {
+ const { data } = await LNbits.api.request(
+ 'GET',
+ `/nostrmarket/api/v1/stall/product/${this.stall.id}?pending=${pending}`,
+ this.inkey
+ )
+ return data
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ sendProductFormData: function () {
+ const data = {
+ stall_id: this.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
+ this.updateProduct(data)
+ } else {
+ this.createProduct(data)
+ }
+ },
+ 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(r => r.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',
+ timeout: 5000
+ })
+ } catch (error) {
+ console.warn(error)
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ 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',
+ timeout: 5000
+ })
+ } catch (error) {
+ console.warn(error)
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ editProduct: async function (product) {
+ const emptyShipping = this.newEmtpyProductData().config.shipping
+ this.productDialog.data = { ...product }
+ this.productDialog.data.config.shipping = emptyShipping.map(shippingZone => {
+ const existingShippingCost = (product.config.shipping || []).find(ps => ps.id === shippingZone.id)
+ shippingZone.cost = existingShippingCost?.cost || 0
+ return shippingZone
+ })
+
+ this.productDialog.showDialog = true
+ },
+ deleteProduct: async function (productId) {
+ LNbits.utils
+ .confirmDialog('Are you sure you want to delete this product?')
+ .onOk(async () => {
+ try {
+ await LNbits.api.request(
+ 'DELETE',
+ '/nostrmarket/api/v1/product/' + productId,
+ this.adminkey
+ )
+ this.products = _.reject(this.products, function (obj) {
+ return obj.id === productId
+ })
+ this.$q.notify({
+ type: 'positive',
+ message: 'Product deleted',
+ timeout: 5000
+ })
+ } catch (error) {
+ console.warn(error)
+ LNbits.utils.notifyApiError(error)
+ }
+ })
+ },
+ showNewProductDialog: async function (data) {
+ this.productDialog.data = data || this.newEmtpyProductData()
+ this.productDialog.showDialog = true
+ },
+ openSelectPendingProductDialog: async function () {
+ this.productDialog.showRestore = true
+ this.pendingProducts = await this.getProducts(true)
+ },
+ openRestoreProductDialog: async function (pendingProduct) {
+ pendingProduct.pending = true
+ await this.showNewProductDialog(pendingProduct)
+ },
+ restoreAllPendingProducts: async function () {
+ for (const p of this.pendingProducts){
+ p.pending = false
+ await this.updateProduct(p)
+ }
+ },
+ customerSelectedForOrder: function (customerPubkey) {
+ this.$emit('customer-selected-for-order', customerPubkey)
+ },
+ shortLabel(value = ''){
+ if (value.length <= 44) return value
+ return value.substring(0, 40) + '...'
+ }
+ },
+ created: async function () {
+ await this.getStall()
+ this.products = await this.getProducts()
+ this.productDialog.data = this.newEmtpyProductData()
+ }
+ })
+}
diff --git a/static/components/stall-list.js b/static/components/stall-list.js
deleted file mode 100644
index 220b5c1..0000000
--- a/static/components/stall-list.js
+++ /dev/null
@@ -1,305 +0,0 @@
-window.app.component('stall-list', {
- name: 'stall-list',
- template: '#stall-list',
- delimiters: ['${', '}'],
- props: ['adminkey', 'inkey', 'wallet-options'],
- data: function () {
- return {
- filter: '',
- stalls: [],
- pendingStalls: [],
- currencies: [],
- stallDialog: {
- show: false,
- showRestore: false,
- data: {
- name: '',
- description: '',
- wallet: null,
- currency: 'sat',
- shippingZones: []
- }
- },
- editDialog: {
- show: false,
- data: {
- id: '',
- name: '',
- description: '',
- wallet: null,
- currency: 'sat',
- shippingZones: []
- }
- },
- zoneOptions: [],
- stallsTable: {
- columns: [
- {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
- }
- }
- }
- },
- computed: {
- filteredZoneOptions: function () {
- 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: {
- emitStallCount: function () {
- this.$emit('stalls-updated', this.stalls.length)
- },
- sendStallFormData: async function () {
- const stallData = {
- name: this.stallDialog.data.name,
- wallet: this.stallDialog.data.wallet,
- currency: this.stallDialog.data.currency,
- shipping_zones: this.stallDialog.data.shippingZones,
- config: {
- description: this.stallDialog.data.description
- }
- }
- if (this.stallDialog.data.id) {
- stallData.id = this.stallDialog.data.id
- await this.restoreStall(stallData)
- } else {
- await this.createStall(stallData)
- }
- },
- createStall: async function (stall) {
- try {
- const {data} = await LNbits.api.request(
- 'POST',
- '/nostrmarket/api/v1/stall',
- this.adminkey,
- stall
- )
- this.stallDialog.show = false
- this.stalls.unshift(data)
- this.emitStallCount()
- this.$q.notify({
- type: 'positive',
- message: 'Stall created!'
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- restoreStall: async function (stallData) {
- try {
- stallData.pending = false
- const {data} = await LNbits.api.request(
- 'PUT',
- `/nostrmarket/api/v1/stall/${stallData.id}`,
- this.adminkey,
- stallData
- )
- this.stallDialog.show = false
- this.stalls.unshift(data)
- this.emitStallCount()
- this.$q.notify({
- type: 'positive',
- message: 'Stall restored!'
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- 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
- )
- this.editDialog.show = false
- const index = this.stalls.findIndex(s => s.id === data.id)
- if (index !== -1) {
- this.stalls.splice(index, 1, data)
- }
- this.emitStallCount()
- 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/' + 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.emitStallCount()
- 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 {
- const {data} = await LNbits.api.request(
- 'GET',
- `/nostrmarket/api/v1/stall?pending=${pending}`,
- this.inkey
- )
- return data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- return []
- },
- getZones: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/nostrmarket/api/v1/zone',
- this.inkey
- )
- return data.map(z => ({
- ...z,
- label: z.name
- ? `${z.name} (${z.countries.join(', ')})`
- : z.countries.join(', ')
- }))
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- return []
- },
- openCreateStallDialog: async function (stallData) {
- this.currencies = this.getCurrencies()
- this.zoneOptions = await this.getZones()
- if (!this.zoneOptions || !this.zoneOptions.length) {
- this.$q.notify({
- type: 'warning',
- message: 'Please create a Shipping Zone first!'
- })
- return
- }
- this.stallDialog.data = stallData || {
- name: '',
- description: '',
- wallet: null,
- currency: 'sat',
- shippingZones: []
- }
- 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)
- },
- openRestoreStallDialog: async function (pendingStall) {
- const shippingZonesIds = this.zoneOptions.map(z => z.id)
- await this.openCreateStallDialog({
- id: pendingStall.id,
- name: pendingStall.name,
- description: pendingStall.config?.description,
- currency: pendingStall.currency,
- shippingZones: (pendingStall.shipping_zones || [])
- .filter(z => shippingZonesIds.indexOf(z.id) !== -1)
- .map(z => ({
- ...z,
- label: z.name
- ? `${z.name} (${z.countries.join(', ')})`
- : z.countries.join(', ')
- }))
- })
- },
- 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
- return value.substring(0, 60) + '...'
- }
- },
- created: async function () {
- this.stalls = await this.getStalls()
- this.emitStallCount()
- this.currencies = this.getCurrencies()
- this.zoneOptions = await this.getZones()
- }
-})
diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html
new file mode 100644
index 0000000..8981355
--- /dev/null
+++ b/static/components/stall-list/stall-list.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+ New Stall
+ Create a new stall
+
+
+
+
+ Restore Stall
+ Restore existing stall from Nostr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{shortLabel(props.row.name)}}
+ {{props.row.currency}}
+
+ {{shortLabel(props.row.config.description)}}
+
+
+
+ {{shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Restore
+
+
+
+
+
+
+
+ There are no stalls to be restored.
+
+
+ Close
+
+
+
+
+
\ No newline at end of file
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js
new file mode 100644
index 0000000..e384895
--- /dev/null
+++ b/static/components/stall-list/stall-list.js
@@ -0,0 +1,266 @@
+async function stallList(path) {
+ const template = await loadTemplateAsync(path)
+ Vue.component('stall-list', {
+ name: 'stall-list',
+ template,
+
+ props: [`adminkey`, 'inkey', 'wallet-options'],
+ data: function () {
+ return {
+ filter: '',
+ stalls: [],
+ pendingStalls: [],
+ currencies: [],
+ stallDialog: {
+ show: false,
+ showRestore: false,
+ data: {
+ 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'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10
+ }
+ }
+ }
+ },
+ computed: {
+ filteredZoneOptions: function () {
+ return this.zoneOptions.filter(
+ z => z.currency === this.stallDialog.data.currency
+ )
+ }
+ },
+ methods: {
+ sendStallFormData: async function () {
+ const stallData = {
+ name: this.stallDialog.data.name,
+ wallet: this.stallDialog.data.wallet,
+ currency: this.stallDialog.data.currency,
+ shipping_zones: this.stallDialog.data.shippingZones,
+ config: {
+ description: this.stallDialog.data.description
+ }
+ }
+ if (this.stallDialog.data.id) {
+ stallData.id = this.stallDialog.data.id
+ await this.restoreStall(stallData)
+ } else {
+ await this.createStall(stallData)
+ }
+
+ },
+ createStall: async function (stall) {
+ try {
+ const { data } = await LNbits.api.request(
+ 'POST',
+ '/nostrmarket/api/v1/stall',
+ this.adminkey,
+ stall
+ )
+ this.stallDialog.show = false
+ data.expanded = false
+ this.stalls.unshift(data)
+ this.$q.notify({
+ type: 'positive',
+ message: 'Stall created!'
+ })
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ restoreStall: async function (stallData) {
+ try {
+ stallData.pending = false
+ const { data } = await LNbits.api.request(
+ 'PUT',
+ `/nostrmarket/api/v1/stall/${stallData.id}`,
+ this.adminkey,
+ stallData
+ )
+ this.stallDialog.show = false
+ data.expanded = false
+ this.stalls.unshift(data)
+ this.$q.notify({
+ type: 'positive',
+ message: 'Stall restored!'
+ })
+ } catch (error) {
+ 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 () {
+ try {
+ const { data } = await LNbits.api.request(
+ 'GET',
+ '/nostrmarket/api/v1/currencies',
+ this.inkey
+ )
+
+ return ['sat', ...data]
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ return []
+ },
+ getStalls: async function (pending = false) {
+ try {
+ const { data } = await LNbits.api.request(
+ 'GET',
+ `/nostrmarket/api/v1/stall?pending=${pending}`,
+ this.inkey
+ )
+ return data.map(s => ({ ...s, expanded: false }))
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ return []
+ },
+ getZones: async function () {
+ try {
+ const { data } = await LNbits.api.request(
+ 'GET',
+ '/nostrmarket/api/v1/zone',
+ this.inkey
+ )
+ return data.map(z => ({
+ ...z,
+ label: z.name
+ ? `${z.name} (${z.countries.join(', ')})`
+ : z.countries.join(', ')
+ }))
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ 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.zoneOptions = await this.getZones()
+ if (!this.zoneOptions || !this.zoneOptions.length) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Please create a Shipping Zone first!'
+ })
+ return
+ }
+ this.stallDialog.data = stallData || {
+ name: '',
+ description: '',
+ wallet: null,
+ currency: 'sat',
+ shippingZones: []
+ }
+ this.stallDialog.show = true
+ },
+ openSelectPendingStallDialog: async function () {
+ this.stallDialog.showRestore = true
+ this.pendingStalls = await this.getStalls(true)
+ },
+ openRestoreStallDialog: async function (pendingStall) {
+ const shippingZonesIds = this.zoneOptions.map(z => z.id)
+ await this.openCreateStallDialog({
+ id: pendingStall.id,
+ name: pendingStall.name,
+ description: pendingStall.config?.description,
+ currency: pendingStall.currency,
+ shippingZones: (pendingStall.shipping_zones || [])
+ .filter(z => shippingZonesIds.indexOf(z.id) !== -1)
+ .map(z => ({
+ ...z,
+ label: z.name
+ ? `${z.name} (${z.countries.join(', ')})`
+ : z.countries.join(', ')
+ }))
+ })
+ },
+ customerSelectedForOrder: function (customerPubkey) {
+ this.$emit('customer-selected-for-order', customerPubkey)
+ },
+ shortLabel(value = ''){
+ if (value.length <= 64) return value
+ return value.substring(0, 60) + '...'
+ }
+ },
+ created: async function () {
+ this.stalls = await this.getStalls()
+ this.currencies = await this.getCurrencies()
+ this.zoneOptions = await this.getZones()
+ }
+ })
+}
diff --git a/static/images/1.png b/static/images/1.png
deleted file mode 100644
index b52baa8..0000000
Binary files a/static/images/1.png and /dev/null differ
diff --git a/static/images/2.png b/static/images/2.png
deleted file mode 100644
index 2c67a46..0000000
Binary files a/static/images/2.png and /dev/null differ
diff --git a/static/images/3.png b/static/images/3.png
deleted file mode 100644
index 833b0f6..0000000
Binary files a/static/images/3.png and /dev/null differ
diff --git a/static/images/4.png b/static/images/4.png
deleted file mode 100644
index 854e734..0000000
Binary files a/static/images/4.png and /dev/null differ
diff --git a/static/images/5.png b/static/images/5.png
deleted file mode 100644
index 71b04dd..0000000
Binary files a/static/images/5.png and /dev/null differ
diff --git a/static/images/6.jpg b/static/images/6.jpg
deleted file mode 100644
index 5c3f576..0000000
Binary files a/static/images/6.jpg and /dev/null differ
diff --git a/static/images/generate_logo.py b/static/images/generate_logo.py
deleted file mode 100644
index cb66c59..0000000
--- a/static/images/generate_logo.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/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")
diff --git a/static/images/nostr-market.png b/static/images/nostr-market.png
deleted file mode 100644
index 3e924b5..0000000
Binary files a/static/images/nostr-market.png and /dev/null differ
diff --git a/static/js/index.js b/static/js/index.js
index f5d2e62..72f1607 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -1,391 +1,241 @@
-const nostr = window.NostrTools
+const merchant = async () => {
+ Vue.component(VueQrcode.name, VueQrcode)
-window.app = Vue.createApp({
- el: '#vue',
- mixins: [window.windowMixin],
- data: function () {
- return {
- activeTab: 'orders',
- selectedStallFilter: null,
- merchant: {},
- shippingZones: [],
- activeChatCustomer: '',
- orderPubkey: null,
- showKeys: false,
- stallCount: 0,
- wsConnection: null,
- nostrStatus: {
- connected: false,
- error: null,
- relays_connected: 0,
- relays_total: 0
- }
- }
- },
- computed: {
- nostrStatusColor: function () {
- if (this.nostrStatus.connected) {
- return 'green'
- } else if (this.nostrStatus.warning) {
- return 'orange'
- }
- return 'red'
- },
- nostrStatusLabel: function () {
- return 'Connect'
- }
- },
- methods: {
- migrateKeys: async function () {
- LNbits.utils
- .confirmDialog(
- 'This will update your merchant to use your current account Nostr keypair ' +
- 'and republish all stalls and products under the new identity. ' +
- 'Existing orders and messages are preserved. Continue?'
- )
- .onOk(async () => {
- try {
- const {data} = await LNbits.api.request(
- 'POST',
- `/nostrmarket/api/v1/merchant/${this.merchant.id}/migrate-keys`,
- this.g.user.wallets[0].adminkey
- )
- this.merchant = data
- this.$q.notify({
- type: 'positive',
- message: 'Merchant keys migrated and stalls republished'
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
+ await keyPair('static/components/key-pair/key-pair.html')
+ await shippingZones('static/components/shipping-zones/shipping-zones.html')
+ await stallDetails('static/components/stall-details/stall-details.html')
+ await stallList('static/components/stall-list/stall-list.html')
+ await orderList('static/components/order-list/order-list.html')
+ await directMessages('static/components/direct-messages/direct-messages.html')
+ await merchantDetails(
+ 'static/components/merchant-details/merchant-details.html'
+ )
+
+ const nostr = window.NostrTools
+
+ new Vue({
+ el: '#vue',
+ mixins: [windowMixin],
+ data: function () {
+ return {
+ merchant: {},
+ shippingZones: [],
+ activeChatCustomer: '',
+ orderPubkey: null,
+ showKeys: false,
+ importKeyDialog: {
+ show: false,
+ data: {
+ privateKey: null
}
- })
- },
- toggleShowKeys: function () {
- this.showKeys = !this.showKeys
- },
- toggleMerchantState: async function () {
- const merchant = await this.getMerchant()
- if (!merchant) {
- this.$q.notify({
- timeout: 5000,
- type: 'warning',
- message: 'Cannot fetch merchant!'
- })
- return
- }
- const message = merchant.config.active
- ? 'New orders will not be processed. Are you sure you want to deactivate?'
- : merchant.config.restore_in_progress
- ? 'Merchant restore from nostr in progress. Please wait!! ' +
- 'Activating now can lead to duplicate order processing. Click "OK" if you want to activate anyway?'
- : 'Are you sure you want activate this merchant?'
-
- LNbits.utils.confirmDialog(message).onOk(async () => {
- await this.toggleMerchant()
- })
- },
- toggleMerchant: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'PUT',
- `/nostrmarket/api/v1/merchant/${this.merchant.id}/toggle`,
- this.g.user.wallets[0].adminkey
- )
- const state = data.config.active ? 'activated' : 'disabled'
- this.merchant = data
- this.$q.notify({
- type: 'positive',
- message: `'Merchant ${state}`,
- timeout: 5000
- })
- } catch (error) {
- console.warn(error)
- LNbits.utils.notifyApiError(error)
+ },
+ wsConnection: null
}
},
- handleMerchantDeleted: function () {
- this.merchant = null
- this.shippingZones = []
- this.activeChatCustomer = ''
- this.showKeys = false
- this.stallCount = 0
- },
- createMerchant: async function () {
- try {
- const payload = {
- config: {}
+ methods: {
+ generateKeys: async function () {
+ const privateKey = nostr.generatePrivateKey()
+ await this.createMerchant(privateKey)
+ },
+ importKeys: async function () {
+ this.importKeyDialog.show = false
+ let privateKey = this.importKeyDialog.data.privateKey
+ if (!privateKey) {
+ return
}
- const {data} = await LNbits.api.request(
- 'POST',
- '/nostrmarket/api/v1/merchant',
- this.g.user.wallets[0].adminkey,
- payload
- )
- this.merchant = data
- this.$q.notify({
- type: 'positive',
- message: 'Merchant Created!'
- })
- this.waitForNotifications()
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- getMerchant: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/nostrmarket/api/v1/merchant',
- this.g.user.wallets[0].inkey
- )
- this.merchant = data
- return data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- customerSelectedForOrder: function (customerPubkey) {
- this.activeChatCustomer = customerPubkey
- },
- filterOrdersForCustomer: function (customerPubkey) {
- this.orderPubkey = customerPubkey
- },
- showOrderDetails: async function (orderData) {
- await this.$refs.orderListRef.orderSelected(
- orderData.orderId,
- orderData.eventId
- )
- },
- waitForNotifications: async function () {
- if (!this.merchant) return
- try {
- const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
- const port = location.port ? `:${location.port}` : ''
- const wsUrl = `${scheme}://${document.domain}${port}/api/v1/ws/${this.merchant.id}`
- console.log('Reconnecting to websocket: ', wsUrl)
- this.wsConnection = new WebSocket(wsUrl)
- this.wsConnection.onmessage = async e => {
- const data = JSON.parse(e.data)
- if (data.type === 'dm:0') {
- this.$q.notify({
- timeout: 5000,
- type: 'positive',
- message: 'New Order'
- })
-
- await this.$refs.directMessagesRef.handleNewMessage(data)
- return
+ try {
+ if (privateKey.toLowerCase().startsWith('nsec')) {
+ privateKey = nostr.nip19.decode(privateKey).data
}
- if (data.type === 'dm:1') {
- await this.$refs.directMessagesRef.handleNewMessage(data)
- await this.$refs.orderListRef.addOrder(data)
- return
- }
- if (data.type === 'dm:2') {
- const orderStatus = JSON.parse(data.dm.message)
- this.$q.notify({
- timeout: 5000,
- type: 'positive',
- message: orderStatus.message
- })
- if (orderStatus.paid) {
- await this.$refs.orderListRef.orderPaid(orderStatus.id)
- }
- await this.$refs.directMessagesRef.handleNewMessage(data)
- return
- }
- if (data.type === 'dm:-1') {
- await this.$refs.directMessagesRef.handleNewMessage(data)
- }
- // order paid
- // order shipped
- }
- } catch (error) {
- this.$q.notify({
- timeout: 5000,
- type: 'warning',
- message: 'Failed to watch for updates',
- caption: `${error}`
- })
- }
- },
- checkNostrStatus: async function (showNotification = false) {
- try {
- const response = await fetch('/nostrclient/api/v1/relays')
- const body = await response.json()
-
- if (response.status === 200) {
- const relaysConnected = body.filter(r => r.connected).length
- if (body.length === 0) {
- this.nostrStatus = {
- connected: false,
- error: 'No relays configured in Nostr Client',
- relays_connected: 0,
- relays_total: 0,
- warning: true
- }
- } else {
- this.nostrStatus = {
- connected: true,
- error: null,
- relays_connected: relaysConnected,
- relays_total: body.length
- }
- }
- } else {
- this.nostrStatus = {
- connected: false,
- error: body.detail,
- relays_connected: 0,
- relays_total: 0
- }
- }
-
- if (showNotification) {
+ } catch (error) {
this.$q.notify({
- timeout: 3000,
- type: this.nostrStatus.connected ? 'positive' : 'warning',
- message: this.nostrStatus.connected ? 'Connected' : 'Disconnected',
- caption: this.nostrStatus.error || undefined
+ type: 'negative',
+ message: `${error}`
})
}
- } catch (error) {
- console.error('Failed to check nostr status:', error)
- this.nostrStatus = {
- connected: false,
- error: error.message,
- relays_connected: 0,
- relays_total: 0
- }
- if (showNotification) {
+ await this.createMerchant(privateKey)
+ },
+ showImportKeysDialog: async function () {
+ this.importKeyDialog.show = true
+ },
+ toggleMerchantKeys: function (value) {
+ this.showKeys = value
+ },
+ toggleMerchantState: async function () {
+ const merchant = await this.getMerchant()
+ if (!merchant) {
this.$q.notify({
timeout: 5000,
- type: 'negative',
- message: this.nostrStatus.error
+ type: 'warning',
+ message: "Cannot fetch merchant!"
})
+ return
}
- }
- },
- restartNostrConnection: async function () {
- LNbits.utils
- .confirmDialog(
- 'Are you sure you want to reconnect to the nostrclient extension?'
- )
- .onOk(async () => {
- try {
- this.$q.notify({
- timeout: 2000,
- type: 'info',
- message: 'Reconnecting...'
- })
- await LNbits.api.request(
- 'PUT',
- '/nostrmarket/api/v1/restart',
- this.g.user.wallets[0].adminkey
- )
- // Check status after restart (give time for websocket to reconnect)
- setTimeout(() => this.checkNostrStatus(true), 3000)
- } catch (error) {
- 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(
+ const message = merchant.config.active ?
+ 'New orders will not be processed. Are you sure you want to deactivate?' :
+ merchant.config.restore_in_progress ?
+ 'Merchant restore from nostr in progress. Please wait!! ' +
+ 'Activating now can lead to duplicate order processing. Click "OK" if you want to activate anyway?' :
+ 'Are you sure you want activate this merchant?'
+
+ LNbits.utils
+ .confirmDialog(message)
+ .onOk(async () => {
+ await this.toggleMerchant()
+ })
+ },
+ toggleMerchant: async function () {
+ try {
+ const { data } = await LNbits.api.request(
'PUT',
- `/nostrmarket/api/v1/stall/${stall.id}`,
+ `/nostrmarket/api/v1/merchant/${this.merchant.id}/toggle`,
this.g.user.wallets[0].adminkey,
- stall
)
+ const state = data.config.active ? 'activated' : 'disabled'
+ this.merchant = data
+ this.$q.notify({
+ type: 'positive',
+ message: `'Merchant ${state}`,
+ timeout: 5000
+ })
+ } catch (error) {
+ console.warn(error)
+ LNbits.utils.notifyApiError(error)
}
- // Fetch products from all stalls
- let productCount = 0
- for (const stall of stalls) {
- const {data: products} = await LNbits.api.request(
+ },
+ handleMerchantDeleted: function () {
+ this.merchant = null
+ this.shippingZones = []
+ this.activeChatCustomer = ''
+ this.showKeys = false
+ },
+ createMerchant: async function (privateKey) {
+ try {
+ const pubkey = nostr.getPublicKey(privateKey)
+ const payload = {
+ private_key: privateKey,
+ public_key: pubkey,
+ config: {}
+ }
+ const { data } = await LNbits.api.request(
+ 'POST',
+ '/nostrmarket/api/v1/merchant',
+ this.g.user.wallets[0].adminkey,
+ payload
+ )
+ this.merchant = data
+ this.$q.notify({
+ type: 'positive',
+ message: 'Merchant Created!'
+ })
+ this.waitForNotifications()
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ getMerchant: async function () {
+ try {
+ const { data } = await LNbits.api.request(
'GET',
- `/nostrmarket/api/v1/stall/product/${stall.id}?pending=false`,
+ '/nostrmarket/api/v1/merchant',
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.merchant = data
+ return data
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
}
- 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)
+ },
+ customerSelectedForOrder: function (customerPubkey) {
+ this.activeChatCustomer = customerPubkey
+ },
+ filterOrdersForCustomer: function (customerPubkey) {
+ this.orderPubkey = customerPubkey
+ },
+ showOrderDetails: async function (orderData) {
+ await this.$refs.orderListRef.orderSelected(orderData.orderId, orderData.eventId)
+ },
+ waitForNotifications: async function () {
+ if (!this.merchant) return
+ try {
+ const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
+ const port = location.port ? `:${location.port}` : ''
+ const wsUrl = `${scheme}://${document.domain}${port}/api/v1/ws/${this.merchant.id}`
+ console.log('Reconnecting to websocket: ', wsUrl)
+ this.wsConnection = new WebSocket(wsUrl)
+ this.wsConnection.onmessage = async e => {
+ const data = JSON.parse(e.data)
+ if (data.type === 'dm:0') {
+ this.$q.notify({
+ timeout: 5000,
+ type: 'positive',
+ message: 'New Order'
+ })
+
+ await this.$refs.directMessagesRef.handleNewMessage(data)
+ return
+ }
+ if (data.type === 'dm:1') {
+ await this.$refs.directMessagesRef.handleNewMessage(data)
+ await this.$refs.orderListRef.addOrder(data)
+ return
+ }
+ if (data.type === 'dm:2') {
+ const orderStatus = JSON.parse(data.dm.message)
+ this.$q.notify({
+ timeout: 5000,
+ type: 'positive',
+ message: orderStatus.message
+ })
+ if (orderStatus.paid) {
+ await this.$refs.orderListRef.orderPaid(orderStatus.id)
+ }
+ await this.$refs.directMessagesRef.handleNewMessage(data)
+ return
+ }
+ if (data.type === 'dm:-1') {
+ await this.$refs.directMessagesRef.handleNewMessage(data)
+ }
+ // order paid
+ // order shipped
}
- })
- },
- 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 () => {
+
+ } catch (error) {
this.$q.notify({
- type: 'info',
- message: 'Delete NIP-15 from Nostr not yet implemented'
+ timeout: 5000,
+ type: 'warning',
+ message: 'Failed to watch for updates',
+ caption: `${error}`
+ })
+ }
+ },
+ restartNostrConnection: async function () {
+ LNbits.utils
+ .confirmDialog(
+ 'Are you sure you want to reconnect to the nostrcient extension?'
+ )
+ .onOk(async () => {
+ try {
+ await LNbits.api.request(
+ 'PUT',
+ '/nostrmarket/api/v1/restart',
+ this.g.user.wallets[0].adminkey
+ )
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
})
- })
- },
- goToProducts: function (stallId) {
- this.selectedStallFilter = stallId
- this.activeTab = 'products'
- },
- goToOrders: function (stallId) {
- this.selectedStallFilter = stallId
- }
- },
- created: async function () {
- const merchant = await this.getMerchant()
- if (!merchant) {
- // Auto-create merchant using the account's existing Nostr keypair
- await this.createMerchant()
- }
- await this.checkNostrStatus()
- setInterval(async () => {
- if (
- !this.wsConnection ||
- this.wsConnection.readyState !== WebSocket.OPEN
- ) {
- await this.waitForNotifications()
}
- }, 1000)
- }
-})
+ },
+ created: async function () {
+ await this.getMerchant()
+ setInterval(async () => {
+ if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) {
+ await this.waitForNotifications()
+ }
+ }, 1000)
+ }
+ })
+}
+
+merchant()
diff --git a/static/js/utils.js b/static/js/utils.js
index b244fce..a28c455 100644
--- a/static/js/utils.js
+++ b/static/js/utils.js
@@ -23,7 +23,7 @@ function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) {
maxWidth / img.naturalWidth,
maxHeight / img.naturalHeight
)
- return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio}
+ return { width: img.naturalWidth * ratio, height: img.naturalHeight * ratio }
}
async function hash(string) {
@@ -125,7 +125,7 @@ function isValidImageUrl(string) {
function isValidKey(key, prefix = 'n') {
try {
if (key && key.startsWith(prefix)) {
- let {_, data} = NostrTools.nip19.decode(key)
+ let { _, data } = NostrTools.nip19.decode(key)
key = data
}
return isValidKeyHex(key)
@@ -143,4 +143,4 @@ function formatCurrency(value, currency) {
style: 'currency',
currency: currency
}).format(value)
-}
+}
\ No newline at end of file
diff --git a/static/market/index.html b/static/market/index.html
index 381d551..4b4e656 100644
--- a/static/market/index.html
+++ b/static/market/index.html
@@ -11,11 +11,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/static/market/js/utils.js b/static/market/js/utils.js
index 8a3a98b..2e41b49 100644
--- a/static/market/js/utils.js
+++ b/static/market/js/utils.js
@@ -1,43 +1,5 @@
var NostrTools = window.NostrTools
-;(function ensureRandomUUID() {
- if (!globalThis.crypto) {
- globalThis.crypto = {}
- }
- if (!globalThis.crypto.randomUUID) {
- globalThis.crypto.randomUUID = function () {
- const getRandomValues = globalThis.crypto.getRandomValues
- if (getRandomValues) {
- const bytes = new Uint8Array(16)
- getRandomValues.call(globalThis.crypto, bytes)
- bytes[6] = (bytes[6] & 0x0f) | 0x40
- bytes[8] = (bytes[8] & 0x3f) | 0x80
- const hex = Array.from(bytes, b =>
- b.toString(16).padStart(2, '0')
- ).join('')
- return (
- hex.slice(0, 8) +
- '-' +
- hex.slice(8, 12) +
- '-' +
- hex.slice(12, 16) +
- '-' +
- hex.slice(16, 20) +
- '-' +
- hex.slice(20)
- )
- }
-
- let d = Date.now()
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
- const r = (d + Math.random() * 16) % 16 | 0
- d = Math.floor(d / 16)
- return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
- })
- }
- }
-})()
-
var defaultRelays = [
'wss://relay.damus.io',
'wss://relay.snort.social',
@@ -82,24 +44,13 @@ function confirm(message) {
async function hash(string) {
- const subtle = globalThis.crypto && globalThis.crypto.subtle
- if (subtle && subtle.digest) {
- const utf8 = new TextEncoder().encode(string)
- const hashBuffer = await subtle.digest('SHA-256', utf8)
- const hashArray = Array.from(new Uint8Array(hashBuffer))
- return hashArray.map(bytes => bytes.toString(16).padStart(2, '0')).join('')
- }
-
- // Fallback for non-secure contexts where crypto.subtle is unavailable.
- return fallbackHash(string)
-}
-
-function fallbackHash(string) {
- let hash = 5381
- for (let i = 0; i < string.length; i++) {
- hash = ((hash << 5) + hash) + string.charCodeAt(i)
- }
- return (hash >>> 0).toString(16).padStart(8, '0')
+ const utf8 = new TextEncoder().encode(string)
+ const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
+ const hashHex = hashArray
+ .map(bytes => bytes.toString(16).padStart(2, '0'))
+ .join('')
+ return hashHex
}
function isJson(str) {
diff --git a/tasks.py b/tasks.py
index c147936..4f50ebe 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,22 +1,18 @@
-import asyncio
-import time
from asyncio import Queue
+import asyncio
+
+from loguru import logger
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
-from loguru import logger
from .nostr.nostr_client import NostrClient
from .services import (
handle_order_paid,
process_nostr_message,
- resubscribe_to_all_merchants,
subscribe_to_all_merchants,
)
-HEALTH_CHECK_INTERVAL = 30 # seconds between health checks
-STALE_THRESHOLD = 120 # seconds without events before resubscribing
-
async def wait_for_paid_invoices():
invoice_queue = Queue()
@@ -40,38 +36,13 @@ async def on_invoice_paid(payment: Payment) -> None:
async def wait_for_nostr_events(nostr_client: NostrClient):
- logger.info("[NOSTRMARKET] Starting wait_for_nostr_events task")
while True:
try:
- logger.info("[NOSTRMARKET] Subscribing to all merchants...")
await subscribe_to_all_merchants()
while True:
message = await nostr_client.get_event()
await process_nostr_message(message)
except Exception as e:
- logger.warning(f"[NOSTRMARKET] Subscription failed. Retrying in 10s: {e}")
+ logger.warning(f"Subcription failed. Will retry in one minute: {e}")
await asyncio.sleep(10)
-
-
-async def subscription_health_monitor(nostr_client: NostrClient):
- """
- Periodically check if events are flowing. If no events have been
- received for STALE_THRESHOLD seconds, force a resubscription with
- overlap to catch any missed events.
- """
- logger.info("[NOSTRMARKET] Starting subscription health monitor")
- while True:
- await asyncio.sleep(HEALTH_CHECK_INTERVAL)
- try:
- if not nostr_client.is_websocket_connected:
- continue
-
- elapsed = time.time() - nostr_client.last_event_at
- if nostr_client.last_event_at > 0 and elapsed > STALE_THRESHOLD:
- logger.warning(
- f"[NOSTRMARKET] ⚠️ No events for {elapsed:.0f}s, resubscribing..."
- )
- await resubscribe_to_all_merchants()
- except Exception as e:
- logger.error(f"[NOSTRMARKET] Health monitor error: {e}")
diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html
index 664d6ba..6bce480 100644
--- a/templates/nostrmarket/_api_docs.html
+++ b/templates/nostrmarket/_api_docs.html
@@ -1,213 +1,44 @@
-
-
-
-
- Nostr (Notes and Other Stuff Transmitted by Relays) is
- a decentralized protocol for censorship-resistant communication. Unlike
- traditional platforms, your identity and data aren't controlled by any
- single company.
-
-
- Your Nostr identity is a cryptographic key pair - a public key (npub)
- that others use to find you, and a private key (nsec) that proves you
- are you. Keep your nsec safe and never share it!
-
-
-
-
-
-
-
-
- 1. Generate or Import Keys
-
- Create a new Nostr identity or import an existing one using your nsec.
- Your keys are used to sign all marketplace events.
-
- 2. Create a Stall
-
- A stall is your shop. Give it a name, description, and configure
- shipping zones for delivery.
-
- 3. Add Products
-
- List items for sale with images, descriptions, and prices in your
- preferred currency.
-
- 4. Publish to Nostr
-
- Your stall and products are published to Nostr relays where customers
- can discover them using any compatible marketplace client.
-
-
-
-
-
-
-
-
-
- Decentralized Commerce - Your shop exists on Nostr
- relays, not a single server. No platform fees, no deplatforming risk.
-
-
- Lightning Payments - Accept instant, low-fee Bitcoin
- payments via the Lightning Network.
-
-
- Encrypted Messages - Communicate privately with
- customers using NIP-04 encrypted direct messages.
-
-
- Portable Identity - Your merchant reputation travels
- with your Nostr keys across any compatible marketplace.
-
-
- Global Reach - Your stalls and products are
- automatically visible on any Nostr marketplace client that supports
- NIP-15, including Amethyst, Plebeian Market, and others.
-
-
-
-
-
-
-
-
-
- Browse the Market - Use the Market Client to discover
- stalls and products from merchants around the world.
-
-
- Pay with Lightning - Fast, private payments with
- minimal fees using Bitcoin's Lightning Network.
-
-
- Direct Communication - Message merchants directly via
- encrypted Nostr DMs for questions, custom orders, or support.
-
-
-
-
-
-
-
-
- This extension was created by:
-
-
-
-
-
-
-
-
-
-
-
-
- Market Client
- Browse and shop from stalls
-
-
-
-
-
-
-
-
-
-
-
- API Documentation
- Swagger REST API reference
-
-
-
-
-
-
-
-
-
-
-
- NIP-15 Specification
- Nostr Marketplace protocol
-
-
-
-
-
-
-
-
-
-
-
- Report Issues / Feedback
- GitHub Issues
-
-
-
-
-
+ >motorina0
+
+
Swagger REST API Documentation
+
+
+ Visit the market client Market client
+
+
diff --git a/templates/nostrmarket/components/direct-messages.html b/templates/nostrmarket/components/direct-messages.html
deleted file mode 100644
index 602eb8b..0000000
--- a/templates/nostrmarket/components/direct-messages.html
+++ /dev/null
@@ -1,191 +0,0 @@
-
-
-
-
-
-
- new
-
-
- Client Orders
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add a public key to chat with
-
-
-
-
-
-
-
-
-
-
-
- New order:
-
-
- Reply sent for order:
-
-
- Paid
-
- Shipped
-
-
-
-
-
-
-
-
-
...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add
- Cancel
-
-
-
-
-
-
-
-
-
- Close
-
-
-
-
-
-
diff --git a/templates/nostrmarket/components/edit-profile-dialog.html b/templates/nostrmarket/components/edit-profile-dialog.html
deleted file mode 100644
index a447237..0000000
--- a/templates/nostrmarket/components/edit-profile-dialog.html
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
- Edit Profile
-
-
-
-
-
-
-
-
-
- Save & Publish
- Cancel
-
-
-
-
diff --git a/templates/nostrmarket/components/merchant-tab.html b/templates/nostrmarket/components/merchant-tab.html
deleted file mode 100644
index 497478a..0000000
--- a/templates/nostrmarket/components/merchant-tab.html
+++ /dev/null
@@ -1,271 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- View Keys
- Show public/private keys
-
-
-
- Saved Profiles
-
-
-
-
-
-
-
-
-
-
-
-
- Remove profile
-
-
-
-
-
-
-
-
-
-
-
-
-
- Accepting Orders
- Orders Paused
-
- New orders will be processed
-
-
- New orders will be ignored
-
-
-
-
-
-
-
-
-
- Pause Orders
- Resume Orders
-
- Stop accepting new orders
-
-
- Start accepting new orders
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- (No display name set)
-
-
-
-
-
-
- 0 Following
- Not implemented yet
-
-
- 0 Followers
- Not implemented yet
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- New Post (Coming soon)
-
-
-
-
-
-
-
-
Coming Soon
-
- Merchant posts will appear here
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/templates/nostrmarket/components/nostr-keys-dialog.html b/templates/nostrmarket/components/nostr-keys-dialog.html
deleted file mode 100644
index dde7069..0000000
--- a/templates/nostrmarket/components/nostr-keys-dialog.html
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
- Nostr Keys
-
-
-
-
-
-
-
-
-
-
- Public Key (npub)
-
-
-
-
- Copy npub
-
-
-
-
-
-
-
- Private Key (nsec)
-
-
-
-
-
-
-
- Copy nsec
-
-
-
-
-
- Never share your private key!
-
-
-
-
-
-
-
diff --git a/templates/nostrmarket/components/order-list.html b/templates/nostrmarket/components/order-list.html
deleted file mode 100644
index 13e684f..0000000
--- a/templates/nostrmarket/components/order-list.html
+++ /dev/null
@@ -1,369 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Restore Orders
- Restore previous orders from Nostr
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- new
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Products:
-
-
-
Quantity
-
-
Name
-
Price
-
-
-
-
-
-
-
-
-
-
-
-
-
Shipping Cost
-
-
-
-
-
-
-
-
-
-
-
Exchange Rate (1 BTC):
-
-
-
-
-
-
-
-
-
-
Customer Public Key:
-
-
-
-
-
-
-
-
-
-
-
-
Shipping Zone:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-
diff --git a/templates/nostrmarket/components/product-list.html b/templates/nostrmarket/components/product-list.html
deleted file mode 100644
index 6d24b84..0000000
--- a/templates/nostrmarket/components/product-list.html
+++ /dev/null
@@ -1,309 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- All Stalls
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
No stalls found. Please create a stall first in the Stalls tab.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Product is active - click to deactivate
- Product is inactive - click to activate
-
-
- Edit product
-
-
- Delete product
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create Product
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Restore
-
-
-
- There are no products to be restored.
-
- Close
-
-
-
-
diff --git a/templates/nostrmarket/components/shipping-zones-list.html b/templates/nostrmarket/components/shipping-zones-list.html
deleted file mode 100644
index 111ba90..0000000
--- a/templates/nostrmarket/components/shipping-zones-list.html
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Edit zone
-
-
- Delete zone
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Update
-
-
- Create Shipping Zone
-
-
-
Cancel
-
-
-
-
-
diff --git a/templates/nostrmarket/components/stall-details.html b/templates/nostrmarket/components/stall-details.html
deleted file mode 100644
index aa67673..0000000
--- a/templates/nostrmarket/components/stall-details.html
+++ /dev/null
@@ -1,466 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Shipping Zones:
-
-
-
-
-
-
-
-
- Update Stall
-
-
- Delete Stall
-
-
-
-
-
-
-
-
-
-
- New Product
- Create a new product
-
-
-
-
- Restore Product
- Restore existing product from Nostr
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create Product
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Restore
-
-
-
-
-
-
- There are no products to be restored.
-
- Restore All
- Close
-
-
-
-
diff --git a/templates/nostrmarket/components/stall-list.html b/templates/nostrmarket/components/stall-list.html
deleted file mode 100644
index a680a50..0000000
--- a/templates/nostrmarket/components/stall-list.html
+++ /dev/null
@@ -1,287 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Edit stall
-
-
- View products
-
-
- View orders
-
-
- Delete stall
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Restore
-
-
-
-
-
-
- There are no stalls to be restored.
-
- Close
-
-
-
-
diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html
index ee3460c..c39eb32 100644
--- a/templates/nostrmarket/index.html
+++ b/templates/nostrmarket/index.html
@@ -1,80 +1,78 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
-
+
-
-
-
-
- Your account Nostr keypair has changed since this merchant was created.
- The merchant is still using the old key. Migrate to republish your
- stalls and products under the new identity.
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
- Orders
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
- Setting up Nostr Market...
+
+ Welcome to Nostr Market!
+ In Nostr Market, merchant and customer communicate via NOSTR relays, so
+ loss of money, product information, and reputation become far less
+ likely if attacked.
+
+
+ Terms
+
+
+ merchant - seller of products with
+ NOSTR key-pair
+
+
+ customer - buyer of products with
+ NOSTR key-pair
+
+
+ product - item for sale by the
+ merchant
+
+
+ stall - list of products controlled
+ by merchant (a merchant can have multiple stalls)
+
+
+ marketplace - clientside software for
+ searching stalls and purchasing products
+
+
+
+
+
+
+
+ Use an existing private key (hex or npub)
+
+
+ A new key pair will be generated for you
+
+
+
-
+
-
-
-
-
-
-
-
-
-
- Restart Connection
-
- Reconnect to the nostrclient extension
-
-
-
-
-
-
-
-
- Check Status
-
- Check connection to nostrclient
-
-
-
-
-
-
-
- Status:
-
-
-
- Relays:
-
- of
-
- connected
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Publish NIP-15
- Publish stalls and products
-
-
-
-
-
-
-
- Publish NIP-99
- Classified listings (coming soon)
-
-
-
-
-
-
-
-
- Refresh NIP-15 from Nostr
- Sync stalls and products
-
-
-
-
-
-
-
- Refresh NIP-99 from Nostr
- Classified listings (coming soon)
-
-
-
-
-
-
-
-
- Delete NIP-15 from Nostr
- Remove stalls and products
-
-
-
-
-
-
-
- Delete NIP-99 from Nostr
- Classified listings (coming soon)
-
-
-
-
-
- First create a stall and add products.
-
-
-
-
+
+
+ Restart the connection to the nostrclient extension
+
+
+
+
+
+
+
+
+
+ {{SITE_TITLE}} Nostr Market Extension
+
+
+
+
+ {% include "nostrmarket/_api_docs.html" %}
@@ -335,36 +184,34 @@
>
-
-
-
-
-
- Nostr Market
-
- A decentralized marketplace extension for LNbits implementing the
- NIP-15 protocol. Create stalls, list products, and accept
- Lightning payments while communicating with customers via
- encrypted Nostr direct messages.
-
-
-
- {% include "nostrmarket/_api_docs.html" %}
-
-
-
-
+
+
+
+
+
+
+ Import
+ Cancel
+
+
+
+
+
{% endblock%}{% block scripts %} {{ window_vars(user) }}
@@ -382,66 +229,17 @@
margin-left: auto;
width: 100%;
}
-
- .profile-avatar {
- border: 3px solid var(--q-dark-page);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
- }
-
- .profile-avatar .q-avatar__content {
- overflow: hidden;
- border-radius: 50%;
- }
-
{% include("nostrmarket/components/nostr-keys-dialog.html") %}
-
{% include("nostrmarket/components/edit-profile-dialog.html") %}
-
{% include("nostrmarket/components/shipping-zones.html") %}
-
{% include("nostrmarket/components/stall-details.html") %}
-
{% include("nostrmarket/components/stall-list.html") %}
-
{% include("nostrmarket/components/order-list.html") %} {% include("nostrmarket/components/direct-messages.html") %}
-
-
{% include("nostrmarket/components/merchant-details.html") %}
-
{% include("nostrmarket/components/merchant-tab.html") %}
-
{% include("nostrmarket/components/shipping-zones-list.html") %}
-
{% include("nostrmarket/components/product-list.html") %}
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
{% endblock %}
diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html
index 4c72d1a..b03a7a4 100644
--- a/templates/nostrmarket/market.html
+++ b/templates/nostrmarket/market.html
@@ -1,59 +1,36 @@
-
+
-
+
+
Nostr Market App
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+