Merge branch 'main' into fix/orders-by-customer-pubkey
This commit is contained in:
commit
e2150edc10
18 changed files with 2348 additions and 418 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -23,3 +23,6 @@ node_modules
|
||||||
*.pyo
|
*.pyo
|
||||||
*.pyc
|
*.pyc
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
|
# Claude Code config
|
||||||
|
CLAUDE.md
|
||||||
104
CLAUDE.md
Normal file
104
CLAUDE.md
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Nostr Market is an LNbits extension implementing NIP-15 (decentralized marketplace protocol) on Nostr. It enables merchants to create webshops (stalls) and sell products with Lightning Network payments, featuring encrypted customer-merchant communication via NIP-04.
|
||||||
|
|
||||||
|
**Prerequisites:** Requires the LNbits [nostrclient](https://github.com/lnbits/nostrclient) extension to be installed and configured.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
All commands are in the Makefile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make format # Run prettier, black, and ruff formatters
|
||||||
|
make check # Run mypy, pyright, black check, ruff check, prettier check
|
||||||
|
make test # Run pytest with debug mode
|
||||||
|
make all # Run format and check
|
||||||
|
```
|
||||||
|
|
||||||
|
Individual tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make black # Format Python files
|
||||||
|
make ruff # Check and fix Python linting
|
||||||
|
make mypy # Static type checking
|
||||||
|
make pyright # Python static type checker
|
||||||
|
make prettier # Format JS/HTML/CSS files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Development Setup
|
||||||
|
|
||||||
|
To run checks locally, install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Python autotools dependencies (needed for secp256k1)
|
||||||
|
sudo apt-get install -y automake autoconf libtool
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Install Node dependencies (for prettier)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run all checks
|
||||||
|
make check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Layers
|
||||||
|
|
||||||
|
1. **API Layer** (`views_api.py`) - REST endpoints for merchants, stalls, products, zones, orders, direct messages
|
||||||
|
2. **Business Logic** (`services.py`) - Order processing, Nostr event signing/publishing, message routing, invoice handling
|
||||||
|
3. **Data Layer** (`crud.py`) - Async SQLite operations via LNbits db module
|
||||||
|
4. **Models** (`models.py`) - Pydantic models for all entities
|
||||||
|
|
||||||
|
### Nostr Integration (`nostr/`)
|
||||||
|
|
||||||
|
- `nostr_client.py` - WebSocket client connecting to nostrclient extension for relay communication
|
||||||
|
- `event.py` - Nostr event model, serialization, ID computation (SHA256), Schnorr signatures
|
||||||
|
|
||||||
|
### Background Tasks (`__init__.py`, `tasks.py`)
|
||||||
|
|
||||||
|
Three permanent async tasks:
|
||||||
|
|
||||||
|
- `wait_for_paid_invoices()` - Lightning payment listener
|
||||||
|
- `wait_for_nostr_events()` - Incoming Nostr message processor
|
||||||
|
- `_subscribe_to_nostr_client()` - WebSocket connection manager
|
||||||
|
|
||||||
|
### Frontend (`static/`, `templates/`)
|
||||||
|
|
||||||
|
- Merchant dashboard: `templates/nostrmarket/index.html`
|
||||||
|
- Customer marketplace: `templates/nostrmarket/market.html` with Vue.js/Quasar in `static/market/`
|
||||||
|
- Use Quasar UI components when possible: https://quasar.dev/components
|
||||||
|
|
||||||
|
### Key Data Models
|
||||||
|
|
||||||
|
- **Merchant** - Shop owner with Nostr keypair, handles event signing and DM encryption
|
||||||
|
- **Stall** - Individual shop with products and shipping zones (kind 30017)
|
||||||
|
- **Product** - Items for sale with categories, images, quantity (kind 30018)
|
||||||
|
- **Zone** - Shipping configuration by region
|
||||||
|
- **Order** - Customer purchases with Lightning invoice tracking
|
||||||
|
- **DirectMessage** - Encrypted chat (NIP-04)
|
||||||
|
- **Customer** - Buyer profile with Nostr pubkey
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
- **Nostrable Interface** - Base class for models convertible to Nostr events (`to_nostr_event()`, `to_nostr_delete_event()`)
|
||||||
|
- **Parameterized Replaceable Events** - Stalls (kind 30017) and Products (kind 30018) per NIP-33
|
||||||
|
- **AES-256 Encryption** - Customer-merchant DMs use shared secret from ECDH
|
||||||
|
- **JSON Meta Fields** - Complex data (zones, items, config) stored as JSON in database
|
||||||
|
|
||||||
|
### Cryptography (`helpers.py`)
|
||||||
|
|
||||||
|
- Schnorr signatures for Nostr events
|
||||||
|
- NIP-04 encryption/decryption
|
||||||
|
- Key derivation and bech32 encoding (npub/nsec)
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
- Always check GitHub Actions after pushing to verify CI passes
|
||||||
|
- Run `make check` locally before pushing to catch issues early
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Nostr Market",
|
"name": "Nostr Market",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"short_description": "Nostr Webshop/market on LNbits",
|
"short_description": "Nostr Webshop/market on LNbits",
|
||||||
"tile": "/nostrmarket/static/images/bitcoin-shop.png",
|
"tile": "/nostrmarket/static/images/nostr-market.png",
|
||||||
"min_lnbits_version": "1.4.0",
|
"min_lnbits_version": "1.4.0",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
261
static/components/product-list.js
Normal file
261
static/components/product-list.js
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
window.app.component('product-list', {
|
||||||
|
name: 'product-list',
|
||||||
|
template: '#product-list',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
props: ['adminkey', 'inkey', 'stall-filter'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
filter: '',
|
||||||
|
stalls: [],
|
||||||
|
products: [],
|
||||||
|
pendingProducts: [],
|
||||||
|
selectedStall: null,
|
||||||
|
productDialog: {
|
||||||
|
showDialog: false,
|
||||||
|
showRestore: false,
|
||||||
|
data: null
|
||||||
|
},
|
||||||
|
productsTable: {
|
||||||
|
columns: [
|
||||||
|
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||||
|
{name: 'stall', align: 'left', label: 'Stall', field: 'stall_id'},
|
||||||
|
{name: 'price', align: 'left', label: 'Price', field: 'price'},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Quantity',
|
||||||
|
field: 'quantity'
|
||||||
|
},
|
||||||
|
{name: 'actions', align: 'right', label: 'Actions', field: ''}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
stallOptions: function () {
|
||||||
|
return this.stalls.map(s => ({
|
||||||
|
label: s.name,
|
||||||
|
value: s.id
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
filteredProducts: function () {
|
||||||
|
if (!this.selectedStall) {
|
||||||
|
return this.products
|
||||||
|
}
|
||||||
|
return this.products.filter(p => p.stall_id === this.selectedStall)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
stallFilter: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.selectedStall = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getStalls: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/stall?pending=false',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.stalls = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getProducts: async function () {
|
||||||
|
try {
|
||||||
|
// Fetch products from all stalls
|
||||||
|
const allProducts = []
|
||||||
|
for (const stall of this.stalls) {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/stall/product/${stall.id}?pending=false`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
allProducts.push(...data)
|
||||||
|
}
|
||||||
|
this.products = allProducts
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPendingProducts: async function () {
|
||||||
|
try {
|
||||||
|
// Fetch pending products from all stalls
|
||||||
|
const allPending = []
|
||||||
|
for (const stall of this.stalls) {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/stall/product/${stall.id}?pending=true`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
allPending.push(...data)
|
||||||
|
}
|
||||||
|
this.pendingProducts = allPending
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getStallName: function (stallId) {
|
||||||
|
const stall = this.stalls.find(s => s.id === stallId)
|
||||||
|
return stall ? stall.name : 'Unknown'
|
||||||
|
},
|
||||||
|
getStallCurrency: function (stallId) {
|
||||||
|
const stall = this.stalls.find(s => s.id === stallId)
|
||||||
|
return stall ? stall.currency : 'sat'
|
||||||
|
},
|
||||||
|
getStall: function (stallId) {
|
||||||
|
return this.stalls.find(s => s.id === stallId)
|
||||||
|
},
|
||||||
|
newEmptyProductData: function () {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
stall_id: this.stalls.length ? this.stalls[0].id : null,
|
||||||
|
name: '',
|
||||||
|
categories: [],
|
||||||
|
images: [],
|
||||||
|
image: null,
|
||||||
|
price: 0,
|
||||||
|
quantity: 0,
|
||||||
|
config: {
|
||||||
|
description: '',
|
||||||
|
use_autoreply: false,
|
||||||
|
autoreply_message: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showNewProductDialog: function () {
|
||||||
|
this.productDialog.data = this.newEmptyProductData()
|
||||||
|
this.productDialog.showDialog = true
|
||||||
|
},
|
||||||
|
editProduct: function (product) {
|
||||||
|
this.productDialog.data = {...product, image: null}
|
||||||
|
if (!this.productDialog.data.config) {
|
||||||
|
this.productDialog.data.config = {description: ''}
|
||||||
|
}
|
||||||
|
this.productDialog.showDialog = true
|
||||||
|
},
|
||||||
|
sendProductFormData: async function () {
|
||||||
|
const data = {
|
||||||
|
stall_id: this.productDialog.data.stall_id,
|
||||||
|
id: this.productDialog.data.id,
|
||||||
|
name: this.productDialog.data.name,
|
||||||
|
images: this.productDialog.data.images || [],
|
||||||
|
price: this.productDialog.data.price,
|
||||||
|
quantity: this.productDialog.data.quantity,
|
||||||
|
categories: this.productDialog.data.categories || [],
|
||||||
|
config: this.productDialog.data.config
|
||||||
|
}
|
||||||
|
this.productDialog.showDialog = false
|
||||||
|
|
||||||
|
if (this.productDialog.data.id) {
|
||||||
|
data.pending = false
|
||||||
|
await this.updateProduct(data)
|
||||||
|
} else {
|
||||||
|
await this.createProduct(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createProduct: async function (payload) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/product',
|
||||||
|
this.adminkey,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
this.products.unshift(data)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product Created'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateProduct: async function (product) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PATCH',
|
||||||
|
'/nostrmarket/api/v1/product/' + product.id,
|
||||||
|
this.adminkey,
|
||||||
|
product
|
||||||
|
)
|
||||||
|
const index = this.products.findIndex(p => p.id === product.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.products.splice(index, 1, data)
|
||||||
|
} else {
|
||||||
|
this.products.unshift(data)
|
||||||
|
}
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product Updated'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteProduct: function (product) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(`Are you sure you want to delete "${product.name}"?`)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostrmarket/api/v1/product/' + product.id,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.products = this.products.filter(p => p.id !== product.id)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product Deleted'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggleProductActive: async function (product) {
|
||||||
|
await this.updateProduct({...product, active: !product.active})
|
||||||
|
},
|
||||||
|
addProductImage: function () {
|
||||||
|
if (!this.productDialog.data.image) return
|
||||||
|
if (!this.productDialog.data.images) {
|
||||||
|
this.productDialog.data.images = []
|
||||||
|
}
|
||||||
|
this.productDialog.data.images.push(this.productDialog.data.image)
|
||||||
|
this.productDialog.data.image = null
|
||||||
|
},
|
||||||
|
removeProductImage: function (imageUrl) {
|
||||||
|
const index = this.productDialog.data.images.indexOf(imageUrl)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.productDialog.data.images.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openSelectPendingProductDialog: async function () {
|
||||||
|
await this.getPendingProducts()
|
||||||
|
this.productDialog.showRestore = true
|
||||||
|
},
|
||||||
|
openRestoreProductDialog: function (pendingProduct) {
|
||||||
|
pendingProduct.pending = true
|
||||||
|
this.productDialog.data = {...pendingProduct, image: null}
|
||||||
|
this.productDialog.showDialog = true
|
||||||
|
},
|
||||||
|
shortLabel: function (value = '') {
|
||||||
|
if (value.length <= 44) return value
|
||||||
|
return value.substring(0, 40) + '...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getStalls()
|
||||||
|
await this.getProducts()
|
||||||
|
}
|
||||||
|
})
|
||||||
209
static/components/shipping-zones-list.js
Normal file
209
static/components/shipping-zones-list.js
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
window.app.component('shipping-zones-list', {
|
||||||
|
name: 'shipping-zones-list',
|
||||||
|
props: ['adminkey', 'inkey'],
|
||||||
|
template: '#shipping-zones-list',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
zones: [],
|
||||||
|
filter: '',
|
||||||
|
zoneDialog: {
|
||||||
|
showDialog: false,
|
||||||
|
data: {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
countries: [],
|
||||||
|
cost: 0,
|
||||||
|
currency: 'sat'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currencies: [],
|
||||||
|
shippingZoneOptions: [
|
||||||
|
'Free (digital)',
|
||||||
|
'Worldwide',
|
||||||
|
'Europe',
|
||||||
|
'Australia',
|
||||||
|
'Austria',
|
||||||
|
'Belgium',
|
||||||
|
'Brazil',
|
||||||
|
'Canada',
|
||||||
|
'China',
|
||||||
|
'Denmark',
|
||||||
|
'Finland',
|
||||||
|
'France',
|
||||||
|
'Germany',
|
||||||
|
'Greece',
|
||||||
|
'Hong Kong',
|
||||||
|
'Hungary',
|
||||||
|
'Indonesia',
|
||||||
|
'Ireland',
|
||||||
|
'Israel',
|
||||||
|
'Italy',
|
||||||
|
'Japan',
|
||||||
|
'Kazakhstan',
|
||||||
|
'Korea',
|
||||||
|
'Luxembourg',
|
||||||
|
'Malaysia',
|
||||||
|
'Mexico',
|
||||||
|
'Netherlands',
|
||||||
|
'New Zealand',
|
||||||
|
'Norway',
|
||||||
|
'Poland',
|
||||||
|
'Portugal',
|
||||||
|
'Romania',
|
||||||
|
'Russia',
|
||||||
|
'Saudi Arabia',
|
||||||
|
'Singapore',
|
||||||
|
'Spain',
|
||||||
|
'Sweden',
|
||||||
|
'Switzerland',
|
||||||
|
'Thailand',
|
||||||
|
'Turkey',
|
||||||
|
'Ukraine',
|
||||||
|
'United Kingdom',
|
||||||
|
'United States',
|
||||||
|
'Vietnam'
|
||||||
|
],
|
||||||
|
zonesTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Name',
|
||||||
|
field: 'name',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'countries',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Countries',
|
||||||
|
field: 'countries',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cost',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Cost',
|
||||||
|
field: 'cost',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currency',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Currency',
|
||||||
|
field: 'currency',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
align: 'right',
|
||||||
|
label: 'Actions',
|
||||||
|
field: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10,
|
||||||
|
sortBy: 'name',
|
||||||
|
descending: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openZoneDialog: function (data) {
|
||||||
|
data = data || {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
countries: [],
|
||||||
|
cost: 0,
|
||||||
|
currency: 'sat'
|
||||||
|
}
|
||||||
|
this.zoneDialog.data = {...data}
|
||||||
|
this.zoneDialog.showDialog = true
|
||||||
|
},
|
||||||
|
getZones: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.zones = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendZoneFormData: async function () {
|
||||||
|
this.zoneDialog.showDialog = false
|
||||||
|
if (this.zoneDialog.data.id) {
|
||||||
|
await this.updateShippingZone(this.zoneDialog.data)
|
||||||
|
} else {
|
||||||
|
await this.createShippingZone(this.zoneDialog.data)
|
||||||
|
}
|
||||||
|
await this.getZones()
|
||||||
|
},
|
||||||
|
createShippingZone: async function (newZone) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.adminkey,
|
||||||
|
newZone
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Zone created!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateShippingZone: async function (updatedZone) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'PATCH',
|
||||||
|
`/nostrmarket/api/v1/zone/${updatedZone.id}`,
|
||||||
|
this.adminkey,
|
||||||
|
updatedZone
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Zone updated!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmDeleteZone: function (zone) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(`Are you sure you want to delete zone "${zone.name}"?`)
|
||||||
|
.onOk(async () => {
|
||||||
|
await this.deleteShippingZone(zone.id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteShippingZone: async function (zoneId) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
`/nostrmarket/api/v1/zone/${zoneId}`,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Zone deleted!'
|
||||||
|
})
|
||||||
|
await this.getZones()
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCurrencies() {
|
||||||
|
const currencies = window.g.allowedCurrencies || []
|
||||||
|
this.currencies = ['sat', ...currencies]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getZones()
|
||||||
|
this.getCurrencies()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -19,7 +19,6 @@ window.app.component('shipping-zones', {
|
||||||
currencies: [],
|
currencies: [],
|
||||||
shippingZoneOptions: [
|
shippingZoneOptions: [
|
||||||
'Free (digital)',
|
'Free (digital)',
|
||||||
'Flat rate',
|
|
||||||
'Worldwide',
|
'Worldwide',
|
||||||
'Europe',
|
'Europe',
|
||||||
'Australia',
|
'Australia',
|
||||||
|
|
@ -27,6 +26,7 @@ window.app.component('shipping-zones', {
|
||||||
'Belgium',
|
'Belgium',
|
||||||
'Brazil',
|
'Brazil',
|
||||||
'Canada',
|
'Canada',
|
||||||
|
'China',
|
||||||
'Denmark',
|
'Denmark',
|
||||||
'Finland',
|
'Finland',
|
||||||
'France',
|
'France',
|
||||||
|
|
@ -34,8 +34,8 @@ window.app.component('shipping-zones', {
|
||||||
'Greece',
|
'Greece',
|
||||||
'Hong Kong',
|
'Hong Kong',
|
||||||
'Hungary',
|
'Hungary',
|
||||||
'Ireland',
|
|
||||||
'Indonesia',
|
'Indonesia',
|
||||||
|
'Ireland',
|
||||||
'Israel',
|
'Israel',
|
||||||
'Italy',
|
'Italy',
|
||||||
'Japan',
|
'Japan',
|
||||||
|
|
@ -59,10 +59,9 @@ window.app.component('shipping-zones', {
|
||||||
'Thailand',
|
'Thailand',
|
||||||
'Turkey',
|
'Turkey',
|
||||||
'Ukraine',
|
'Ukraine',
|
||||||
'United Kingdom**',
|
'United Kingdom',
|
||||||
'United States***',
|
'United States',
|
||||||
'Vietnam',
|
'Vietnam'
|
||||||
'China'
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -162,22 +161,13 @@ window.app.component('shipping-zones', {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getCurrencies() {
|
getCurrencies() {
|
||||||
try {
|
const currencies = window.g.allowedCurrencies || []
|
||||||
const {data} = await LNbits.api.request(
|
this.currencies = ['sat', ...currencies]
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/currencies',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
|
|
||||||
this.currencies = ['sat', ...data]
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.getZones()
|
await this.getZones()
|
||||||
await this.getCurrencies()
|
this.getCurrencies()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ window.app.component('stall-list', {
|
||||||
name: 'stall-list',
|
name: 'stall-list',
|
||||||
template: '#stall-list',
|
template: '#stall-list',
|
||||||
delimiters: ['${', '}'],
|
delimiters: ['${', '}'],
|
||||||
props: [`adminkey`, 'inkey', 'wallet-options'],
|
props: ['adminkey', 'inkey', 'wallet-options'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
filter: '',
|
filter: '',
|
||||||
|
|
@ -20,21 +20,21 @@ window.app.component('stall-list', {
|
||||||
shippingZones: []
|
shippingZones: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
editDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
wallet: null,
|
||||||
|
currency: 'sat',
|
||||||
|
shippingZones: []
|
||||||
|
}
|
||||||
|
},
|
||||||
zoneOptions: [],
|
zoneOptions: [],
|
||||||
stallsTable: {
|
stallsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||||
name: '',
|
|
||||||
align: 'left',
|
|
||||||
label: '',
|
|
||||||
field: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'id',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'id'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'currency',
|
name: 'currency',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|
@ -45,14 +45,15 @@ window.app.component('stall-list', {
|
||||||
name: 'description',
|
name: 'description',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Description',
|
label: 'Description',
|
||||||
field: 'description'
|
field: row => row.config?.description || ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'shippingZones',
|
name: 'shippingZones',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Shipping Zones',
|
label: 'Shipping Zones',
|
||||||
field: 'shippingZones'
|
field: row => row.shipping_zones?.map(z => z.name).join(', ') || ''
|
||||||
}
|
},
|
||||||
|
{name: 'actions', align: 'right', label: 'Actions', field: ''}
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
|
|
@ -65,6 +66,11 @@ window.app.component('stall-list', {
|
||||||
return this.zoneOptions.filter(
|
return this.zoneOptions.filter(
|
||||||
z => z.currency === this.stallDialog.data.currency
|
z => z.currency === this.stallDialog.data.currency
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
editFilteredZoneOptions: function () {
|
||||||
|
return this.zoneOptions.filter(
|
||||||
|
z => z.currency === this.editDialog.data.currency
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -94,7 +100,6 @@ window.app.component('stall-list', {
|
||||||
stall
|
stall
|
||||||
)
|
)
|
||||||
this.stallDialog.show = false
|
this.stallDialog.show = false
|
||||||
data.expanded = false
|
|
||||||
this.stalls.unshift(data)
|
this.stalls.unshift(data)
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
|
|
@ -114,7 +119,6 @@ window.app.component('stall-list', {
|
||||||
stallData
|
stallData
|
||||||
)
|
)
|
||||||
this.stallDialog.show = false
|
this.stallDialog.show = false
|
||||||
data.expanded = false
|
|
||||||
this.stalls.unshift(data)
|
this.stalls.unshift(data)
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
|
|
@ -124,44 +128,66 @@ window.app.component('stall-list', {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteStall: async function (pendingStall) {
|
updateStall: async function () {
|
||||||
LNbits.utils
|
try {
|
||||||
.confirmDialog(
|
const stallData = {
|
||||||
`
|
id: this.editDialog.data.id,
|
||||||
Are you sure you want to delete this pending stall '${pendingStall.name}'?
|
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 {
|
try {
|
||||||
await LNbits.api.request(
|
await LNbits.api.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/nostrmarket/api/v1/stall/' + pendingStall.id,
|
'/nostrmarket/api/v1/stall/' + stall.id,
|
||||||
this.adminkey
|
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({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Pending Stall Deleted',
|
message: 'Stall deleted'
|
||||||
timeout: 5000
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(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 () {
|
getCurrencies: function () {
|
||||||
try {
|
const currencies = window.g.allowedCurrencies || []
|
||||||
const {data} = await LNbits.api.request(
|
return ['sat', ...currencies]
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/currencies',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
|
|
||||||
return ['sat', ...data]
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
},
|
||||||
getStalls: async function (pending = false) {
|
getStalls: async function (pending = false) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -170,7 +196,7 @@ window.app.component('stall-list', {
|
||||||
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
||||||
this.inkey
|
this.inkey
|
||||||
)
|
)
|
||||||
return data.map(s => ({...s, expanded: false}))
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
|
|
@ -194,20 +220,8 @@ window.app.component('stall-list', {
|
||||||
}
|
}
|
||||||
return []
|
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) {
|
openCreateStallDialog: async function (stallData) {
|
||||||
this.currencies = await this.getCurrencies()
|
this.currencies = this.getCurrencies()
|
||||||
this.zoneOptions = await this.getZones()
|
this.zoneOptions = await this.getZones()
|
||||||
if (!this.zoneOptions || !this.zoneOptions.length) {
|
if (!this.zoneOptions || !this.zoneOptions.length) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
|
|
@ -225,6 +239,24 @@ window.app.component('stall-list', {
|
||||||
}
|
}
|
||||||
this.stallDialog.show = true
|
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 () {
|
openSelectPendingStallDialog: async function () {
|
||||||
this.stallDialog.showRestore = true
|
this.stallDialog.showRestore = true
|
||||||
this.pendingStalls = await this.getStalls(true)
|
this.pendingStalls = await this.getStalls(true)
|
||||||
|
|
@ -246,8 +278,11 @@ window.app.component('stall-list', {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
customerSelectedForOrder: function (customerPubkey) {
|
goToProducts: function (stall) {
|
||||||
this.$emit('customer-selected-for-order', customerPubkey)
|
this.$emit('go-to-products', stall.id)
|
||||||
|
},
|
||||||
|
goToOrders: function (stall) {
|
||||||
|
this.$emit('go-to-orders', stall.id)
|
||||||
},
|
},
|
||||||
shortLabel(value = '') {
|
shortLabel(value = '') {
|
||||||
if (value.length <= 64) return value
|
if (value.length <= 64) return value
|
||||||
|
|
@ -256,7 +291,7 @@ window.app.component('stall-list', {
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
this.stalls = await this.getStalls()
|
this.stalls = await this.getStalls()
|
||||||
this.currencies = await this.getCurrencies()
|
this.currencies = this.getCurrencies()
|
||||||
this.zoneOptions = await this.getZones()
|
this.zoneOptions = await this.getZones()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
123
static/images/generate_logo.py
Normal file
123
static/images/generate_logo.py
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate the Nostr Market logo.
|
||||||
|
Requires: pip install Pillow
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw # type: ignore[import-not-found]
|
||||||
|
|
||||||
|
# Render at 4x size for antialiasing
|
||||||
|
scale = 4
|
||||||
|
size = 128 * scale
|
||||||
|
final_size = 128
|
||||||
|
|
||||||
|
# Consistent color scheme with Nostr Proxy
|
||||||
|
dark_purple = (80, 40, 120)
|
||||||
|
light_purple = (140, 100, 180)
|
||||||
|
white = (255, 255, 255)
|
||||||
|
|
||||||
|
margin = 4 * scale
|
||||||
|
|
||||||
|
swoosh_center = ((128 + 100) * scale, -90 * scale)
|
||||||
|
swoosh_radius = 220 * scale
|
||||||
|
|
||||||
|
# Create rounded rectangle mask
|
||||||
|
mask = Image.new("L", (size, size), 0)
|
||||||
|
mask_draw = ImageDraw.Draw(mask)
|
||||||
|
corner_radius = 20 * scale
|
||||||
|
mask_draw.rounded_rectangle(
|
||||||
|
[margin, margin, size - margin, size - margin],
|
||||||
|
radius=corner_radius,
|
||||||
|
fill=255,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create background with swoosh
|
||||||
|
bg = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
bg_draw = ImageDraw.Draw(bg)
|
||||||
|
bg_draw.rounded_rectangle(
|
||||||
|
[margin, margin, size - margin, size - margin],
|
||||||
|
radius=corner_radius,
|
||||||
|
fill=dark_purple,
|
||||||
|
)
|
||||||
|
bg_draw.ellipse(
|
||||||
|
[
|
||||||
|
swoosh_center[0] - swoosh_radius,
|
||||||
|
swoosh_center[1] - swoosh_radius,
|
||||||
|
swoosh_center[0] + swoosh_radius,
|
||||||
|
swoosh_center[1] + swoosh_radius,
|
||||||
|
],
|
||||||
|
fill=light_purple,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply rounded rectangle mask
|
||||||
|
final = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
final.paste(bg, mask=mask)
|
||||||
|
draw = ImageDraw.Draw(final)
|
||||||
|
|
||||||
|
center_x, center_y = size // 2, size // 2
|
||||||
|
|
||||||
|
# Shop/storefront - wider and shorter for shop look
|
||||||
|
shop_width = 80 * scale
|
||||||
|
awning_height = 18 * scale
|
||||||
|
body_height = 45 * scale
|
||||||
|
total_height = awning_height + body_height
|
||||||
|
|
||||||
|
shop_left = center_x - shop_width // 2
|
||||||
|
shop_right = center_x + shop_width // 2
|
||||||
|
|
||||||
|
# Center vertically
|
||||||
|
awning_top = center_y - total_height // 2
|
||||||
|
awning_bottom = awning_top + awning_height
|
||||||
|
shop_bottom = awning_bottom + body_height
|
||||||
|
awning_extend = 5 * scale
|
||||||
|
|
||||||
|
# Draw awning background (white base)
|
||||||
|
draw.rectangle(
|
||||||
|
[shop_left - awning_extend, awning_top, shop_right + awning_extend, awning_bottom],
|
||||||
|
fill=white,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Vertical stripes on awning (alternating dark purple)
|
||||||
|
stripe_count = 8
|
||||||
|
stripe_width = (shop_width + 2 * awning_extend) // stripe_count
|
||||||
|
for i in range(1, stripe_count, 2):
|
||||||
|
x_left = shop_left - awning_extend + i * stripe_width
|
||||||
|
draw.rectangle(
|
||||||
|
[x_left, awning_top, x_left + stripe_width, awning_bottom],
|
||||||
|
fill=dark_purple,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Shop body (below awning)
|
||||||
|
draw.rectangle(
|
||||||
|
[shop_left, awning_bottom, shop_right, shop_bottom],
|
||||||
|
fill=white,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Large display window (shop style)
|
||||||
|
window_margin = 8 * scale
|
||||||
|
window_top = awning_bottom + 6 * scale
|
||||||
|
window_bottom = shop_bottom - 6 * scale
|
||||||
|
# Left display window
|
||||||
|
draw.rectangle(
|
||||||
|
[shop_left + window_margin, window_top, center_x - 10 * scale, window_bottom],
|
||||||
|
fill=dark_purple,
|
||||||
|
)
|
||||||
|
# Right display window
|
||||||
|
draw.rectangle(
|
||||||
|
[center_x + 10 * scale, window_top, shop_right - window_margin, window_bottom],
|
||||||
|
fill=dark_purple,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Door (center, dark purple cutout)
|
||||||
|
door_width = 14 * scale
|
||||||
|
door_left = center_x - door_width // 2
|
||||||
|
draw.rectangle(
|
||||||
|
[door_left, window_top, door_left + door_width, shop_bottom],
|
||||||
|
fill=dark_purple,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Downscale with LANCZOS for antialiasing
|
||||||
|
final = final.resize((final_size, final_size), Image.LANCZOS)
|
||||||
|
|
||||||
|
final.save("nostr-market.png")
|
||||||
|
print("Logo saved to nostr-market.png")
|
||||||
BIN
static/images/nostr-market.png
Normal file
BIN
static/images/nostr-market.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
|
|
@ -5,6 +5,8 @@ window.app = Vue.createApp({
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
activeTab: 'merchant',
|
||||||
|
selectedStallFilter: null,
|
||||||
merchant: {},
|
merchant: {},
|
||||||
shippingZones: [],
|
shippingZones: [],
|
||||||
activeChatCustomer: '',
|
activeChatCustomer: '',
|
||||||
|
|
@ -16,7 +18,26 @@ window.app = Vue.createApp({
|
||||||
privateKey: null
|
privateKey: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
wsConnection: null
|
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: {
|
methods: {
|
||||||
|
|
@ -196,10 +217,132 @@ window.app = Vue.createApp({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
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) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 3000,
|
||||||
|
type: this.nostrStatus.connected ? 'positive' : 'warning',
|
||||||
|
message: this.nostrStatus.connected ? 'Connected' : 'Disconnected',
|
||||||
|
caption: this.nostrStatus.error || undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} 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) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'negative',
|
||||||
|
message: this.nostrStatus.error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
restartNostrConnection: async function () {
|
restartNostrConnection: async function () {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog(
|
.confirmDialog(
|
||||||
'Are you sure you want to reconnect to the nostrcient extension?'
|
'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(
|
||||||
|
'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 () => {
|
.onOk(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -208,14 +351,39 @@ window.app = Vue.createApp({
|
||||||
'/nostrmarket/api/v1/restart',
|
'/nostrmarket/api/v1/restart',
|
||||||
this.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Refreshing NIP-15 data from Nostr...'
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(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 () {
|
created: async function () {
|
||||||
await this.getMerchant()
|
await this.getMerchant()
|
||||||
|
await this.checkNostrStatus()
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
if (
|
if (
|
||||||
!this.wsConnection ||
|
!this.wsConnection ||
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,213 @@
|
||||||
|
<q-expansion-item
|
||||||
|
icon="help_outline"
|
||||||
|
label="What is Nostr?"
|
||||||
|
header-class="text-weight-medium"
|
||||||
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section class="text-body2">
|
||||||
<p>
|
<p>
|
||||||
Nostr Market<br />
|
<strong>Nostr</strong> (Notes and Other Stuff Transmitted by Relays) is
|
||||||
<small>
|
a decentralized protocol for censorship-resistant communication. Unlike
|
||||||
Created by,
|
traditional platforms, your identity and data aren't controlled by any
|
||||||
<a
|
single company.
|
||||||
class="text-secondary"
|
</p>
|
||||||
target="_blank"
|
<p class="q-mb-none">
|
||||||
style="color: unset"
|
Your Nostr identity is a cryptographic key pair - a public key (npub)
|
||||||
href="https://github.com/talvasconcelos"
|
that others use to find you, and a private key (nsec) that proves you
|
||||||
>Tal Vasconcelos</a
|
are you. Keep your nsec safe and never share it!
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
target="_blank"
|
|
||||||
style="color: unset"
|
|
||||||
href="https://github.com/benarc"
|
|
||||||
>Ben Arc</a
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
target="_blank"
|
|
||||||
style="color: unset"
|
|
||||||
href="https://github.com/motorina0"
|
|
||||||
>motorina0</a
|
|
||||||
></small
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
target="_blank"
|
|
||||||
href="/docs#/nostrmarket"
|
|
||||||
class="text-white"
|
|
||||||
>Swagger REST API Documentation</a
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<a class="text-secondary" target="_blank" href="/nostrmarket/market"
|
|
||||||
><q-tooltip>Visit the market client</q-tooltip
|
|
||||||
><q-icon name="storefront" class="q-mr-sm"></q-icon>Market client</a
|
|
||||||
>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
icon="flag"
|
||||||
|
label="Getting Started"
|
||||||
|
header-class="text-weight-medium"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-body2">
|
||||||
|
<p><strong>1. Generate or Import Keys</strong></p>
|
||||||
|
<p class="q-mb-md">
|
||||||
|
Create a new Nostr identity or import an existing one using your nsec.
|
||||||
|
Your keys are used to sign all marketplace events.
|
||||||
|
</p>
|
||||||
|
<p><strong>2. Create a Stall</strong></p>
|
||||||
|
<p class="q-mb-md">
|
||||||
|
A stall is your shop. Give it a name, description, and configure
|
||||||
|
shipping zones for delivery.
|
||||||
|
</p>
|
||||||
|
<p><strong>3. Add Products</strong></p>
|
||||||
|
<p class="q-mb-md">
|
||||||
|
List items for sale with images, descriptions, and prices in your
|
||||||
|
preferred currency.
|
||||||
|
</p>
|
||||||
|
<p><strong>4. Publish to Nostr</strong></p>
|
||||||
|
<p class="q-mb-none">
|
||||||
|
Your stall and products are published to Nostr relays where customers
|
||||||
|
can discover them using any compatible marketplace client.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
icon="storefront"
|
||||||
|
label="For Merchants"
|
||||||
|
header-class="text-weight-medium"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-body2">
|
||||||
|
<p>
|
||||||
|
<strong>Decentralized Commerce</strong> - Your shop exists on Nostr
|
||||||
|
relays, not a single server. No platform fees, no deplatforming risk.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Lightning Payments</strong> - Accept instant, low-fee Bitcoin
|
||||||
|
payments via the Lightning Network.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Encrypted Messages</strong> - Communicate privately with
|
||||||
|
customers using NIP-04 encrypted direct messages.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Portable Identity</strong> - Your merchant reputation travels
|
||||||
|
with your Nostr keys across any compatible marketplace.
|
||||||
|
</p>
|
||||||
|
<p class="q-mb-none">
|
||||||
|
<strong>Global Reach</strong> - Your stalls and products are
|
||||||
|
automatically visible on any Nostr marketplace client that supports
|
||||||
|
NIP-15, including Amethyst, Plebeian Market, and others.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
icon="shopping_cart"
|
||||||
|
label="For Customers"
|
||||||
|
header-class="text-weight-medium"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-body2">
|
||||||
|
<p>
|
||||||
|
<strong>Browse the Market</strong> - Use the Market Client to discover
|
||||||
|
stalls and products from merchants around the world.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Pay with Lightning</strong> - Fast, private payments with
|
||||||
|
minimal fees using Bitcoin's Lightning Network.
|
||||||
|
</p>
|
||||||
|
<p class="q-mb-none">
|
||||||
|
<strong>Direct Communication</strong> - Message merchants directly via
|
||||||
|
encrypted Nostr DMs for questions, custom orders, or support.
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
icon="people"
|
||||||
|
label="Contributors"
|
||||||
|
header-class="text-weight-medium"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-body2">
|
||||||
|
<p class="q-mb-sm">This extension was created by:</p>
|
||||||
|
<div class="q-gutter-sm">
|
||||||
|
<a
|
||||||
|
href="https://github.com/talvasconcelos"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none"
|
||||||
|
>
|
||||||
|
<q-chip clickable icon="person">Tal Vasconcelos</q-chip>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/arcbtc"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none"
|
||||||
|
>
|
||||||
|
<q-chip clickable icon="person">Ben Arc</q-chip>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/motorina0"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none"
|
||||||
|
>
|
||||||
|
<q-chip clickable icon="person">motorina0</q-chip>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/BenGWeeks"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none"
|
||||||
|
>
|
||||||
|
<q-chip clickable icon="person">Ben Weeks</q-chip>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-separator></q-separator>
|
||||||
|
|
||||||
|
<q-item clickable tag="a" target="_blank" href="/nostrmarket/market">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="storefront" color="primary"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Market Client</q-item-label>
|
||||||
|
<q-item-label caption>Browse and shop from stalls</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon name="open_in_new" size="xs"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item clickable tag="a" target="_blank" href="/docs#/nostrmarket">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="api" color="primary"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>API Documentation</q-item-label>
|
||||||
|
<q-item-label caption>Swagger REST API reference</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon name="open_in_new" size="xs"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
tag="a"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/nostr-protocol/nips/blob/master/15.md"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="description" color="secondary"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>NIP-15 Specification</q-item-label>
|
||||||
|
<q-item-label caption>Nostr Marketplace protocol</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon name="open_in_new" size="xs"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
tag="a"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/lnbits/nostrmarket/issues"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="bug_report" color="warning"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Report Issues / Feedback</q-item-label>
|
||||||
|
<q-item-label caption>GitHub Issues</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon name="open_in_new" size="xs"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
|
||||||
104
templates/nostrmarket/components/merchant-tab.html
Normal file
104
templates/nostrmarket/components/merchant-tab.html
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<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>
|
||||||
309
templates/nostrmarket/components/product-list.html
Normal file
309
templates/nostrmarket/components/product-list.html
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
<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>
|
||||||
136
templates/nostrmarket/components/shipping-zones-list.html
Normal file
136
templates/nostrmarket/components/shipping-zones-list.html
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<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 zones..."
|
||||||
|
style="min-width: 200px"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<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="cost" :props="props">
|
||||||
|
<span v-text="props.row.cost"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="currency" :props="props">
|
||||||
|
<span v-text="props.row.currency"></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>
|
||||||
|
|
@ -48,6 +48,23 @@
|
||||||
label="Countries"
|
label="Countries"
|
||||||
v-model="zoneDialog.data.countries"
|
v-model="zoneDialog.data.countries"
|
||||||
></q-select>
|
></q-select>
|
||||||
|
<div class="row items-start">
|
||||||
|
<div class="col q-mr-sm">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Default shipping cost"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
:step="zoneDialog.data.currency != 'sat' ? '0.01' : '1'"
|
||||||
|
type="number"
|
||||||
|
v-model.trim="zoneDialog.data.cost"
|
||||||
|
:error="(zoneDialog.data.currency === 'sat' && zoneDialog.data.cost % 1 !== 0) || (zoneDialog.data.currency !== 'sat' && (zoneDialog.data.cost * 100) % 1 !== 0)"
|
||||||
|
:error-message="zoneDialog.data.currency === 'sat' ? 'Satoshis must be whole numbers' : 'Maximum 2 decimal places allowed'"
|
||||||
|
hint="Additional costs can be set per product"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
<q-select
|
<q-select
|
||||||
:disabled="!!zoneDialog.data.id"
|
:disabled="!!zoneDialog.data.id"
|
||||||
:readonly="!!zoneDialog.data.id"
|
:readonly="!!zoneDialog.data.id"
|
||||||
|
|
@ -55,19 +72,12 @@
|
||||||
dense
|
dense
|
||||||
v-model="zoneDialog.data.currency"
|
v-model="zoneDialog.data.currency"
|
||||||
type="text"
|
type="text"
|
||||||
label="Unit"
|
label="Currency"
|
||||||
:options="currencies"
|
:options="currencies"
|
||||||
|
style="min-width: 100px"
|
||||||
></q-select>
|
></q-select>
|
||||||
<q-input
|
</div>
|
||||||
filled
|
</div>
|
||||||
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 class="row q-mt-lg">
|
||||||
<div v-if="zoneDialog.data.id">
|
<div v-if="zoneDialog.data.id">
|
||||||
<q-btn unelevated color="primary" type="submit">Update</q-btn>
|
<q-btn unelevated color="primary" type="submit">Update</q-btn>
|
||||||
|
|
@ -83,7 +93,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
:disable="!zoneDialog.data.countries || !zoneDialog.data.countries.length"
|
:disable="!zoneDialog.data.name || !zoneDialog.data.countries || !zoneDialog.data.countries.length || (zoneDialog.data.currency === 'sat' && zoneDialog.data.cost % 1 !== 0) || (zoneDialog.data.currency !== 'sat' && (zoneDialog.data.cost * 100) % 1 !== 0)"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Create Shipping Zone</q-btn
|
>Create Shipping Zone</q-btn
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,38 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
<div class="row items-center q-mb-md q-col-gutter-sm justify-end">
|
||||||
<div class="col q-pr-lg">
|
<div class="col-auto">
|
||||||
<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>
|
|
||||||
<q-input
|
<q-input
|
||||||
borderless
|
|
||||||
dense
|
dense
|
||||||
debounce="300"
|
debounce="300"
|
||||||
v-model="filter"
|
v-model="filter"
|
||||||
placeholder="Search"
|
placeholder="Search by name, currency..."
|
||||||
class="float-right"
|
style="min-width: 250px"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:prepend>
|
||||||
<q-icon name="search"></q-icon>
|
<q-icon name="search"></q-icon>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
|
|
@ -51,57 +46,67 @@
|
||||||
>
|
>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width>
|
<q-td key="name" :props="props">
|
||||||
<q-btn
|
<span v-text="shortLabel(props.row.name)"></span>
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
@click="props.row.expanded= !props.row.expanded"
|
|
||||||
:icon="props.row.expanded? 'remove' : 'add'"
|
|
||||||
/>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
|
<q-td key="currency" :props="props">
|
||||||
<q-td key="id" :props="props"
|
<span v-text="props.row.currency"></span>
|
||||||
><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>
|
</q-td>
|
||||||
<q-td key="description" :props="props">
|
<q-td key="description" :props="props">
|
||||||
<span v-text="shortLabel(props.row.config.description)"></span>
|
<span v-text="shortLabel(props.row.config.description)"></span>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td key="shippingZones" :props="props">
|
<q-td key="shippingZones" :props="props">
|
||||||
<div>
|
|
||||||
<span
|
<span
|
||||||
v-text="shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))"
|
v-text="shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
<q-td key="actions" :props="props">
|
||||||
<q-tr v-if="props.row.expanded" :props="props">
|
<q-btn
|
||||||
<q-td colspan="100%">
|
size="sm"
|
||||||
<div class="row items-center q-mb-lg">
|
color="primary"
|
||||||
<div class="col-12">
|
dense
|
||||||
<stall-details
|
flat
|
||||||
:stall-id="props.row.id"
|
icon="edit"
|
||||||
:adminkey="adminkey"
|
@click="openEditStallDialog(props.row)"
|
||||||
:inkey="inkey"
|
>
|
||||||
:wallet-options="walletOptions"
|
<q-tooltip>Edit stall</q-tooltip>
|
||||||
:zone-options="zoneOptions"
|
</q-btn>
|
||||||
:currencies="currencies"
|
<q-btn
|
||||||
@stall-deleted="handleStallDeleted"
|
size="sm"
|
||||||
@stall-updated="handleStallUpdated"
|
color="secondary"
|
||||||
@customer-selected-for-order="customerSelectedForOrder"
|
dense
|
||||||
></stall-details>
|
flat
|
||||||
</div>
|
icon="inventory_2"
|
||||||
</div>
|
@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-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
|
|
||||||
<div>
|
<!-- Create Stall Dialog -->
|
||||||
<q-dialog v-model="stallDialog.show" position="top">
|
<q-dialog v-model="stallDialog.show" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
<q-form @submit="sendStallFormData" class="q-gutter-md">
|
<q-form @submit="sendStallFormData" class="q-gutter-md">
|
||||||
|
|
@ -164,6 +169,75 @@
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</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-dialog v-model="stallDialog.showRestore" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
<div v-if="pendingStalls && pendingStalls.length" class="row q-mt-lg">
|
<div v-if="pendingStalls && pendingStalls.length" class="row q-mt-lg">
|
||||||
|
|
@ -211,4 +285,3 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,211 @@
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}
|
%} {% block page %}
|
||||||
<div class="row q-col-gutter-md">
|
<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">
|
<div v-if="merchant && merchant.id">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<div class="row items-center no-wrap">
|
||||||
<div class="row items-center q-col-gutter-sm">
|
<q-tabs
|
||||||
<div class="col-12 col-sm-auto">
|
v-model="activeTab"
|
||||||
<merchant-details
|
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"
|
:merchant-id="merchant.id"
|
||||||
:inkey="g.user.wallets[0].inkey"
|
:inkey="g.user.wallets[0].inkey"
|
||||||
:adminkey="g.user.wallets[0].adminkey"
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
:show-keys="showKeys"
|
:show-keys="showKeys"
|
||||||
@toggle-show-keys="toggleShowKeys"
|
:merchant-active="merchant.config.active"
|
||||||
@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
|
|
||||||
:public-key="merchant.public_key"
|
:public-key="merchant.public_key"
|
||||||
:private-key="merchant.private_key"
|
:private-key="merchant.private_key"
|
||||||
></key-pair>
|
:is-admin="g.user.admin"
|
||||||
</div>
|
@toggle-show-keys="toggleShowKeys"
|
||||||
</div>
|
@hide-keys="showKeys = false"
|
||||||
</q-card-section>
|
@merchant-deleted="handleMerchantDeleted"
|
||||||
</q-card>
|
@toggle-merchant-state="toggleMerchantState"
|
||||||
<q-card class="q-mt-lg">
|
@restart-nostr-connection="restartNostrConnection"
|
||||||
<q-card-section>
|
></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
|
<stall-list
|
||||||
:adminkey="g.user.wallets[0].adminkey"
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
:inkey="g.user.wallets[0].inkey"
|
:inkey="g.user.wallets[0].inkey"
|
||||||
:wallet-options="g.user.walletOptions"
|
:wallet-options="g.user.walletOptions"
|
||||||
@customer-selected-for-order="customerSelectedForOrder"
|
@customer-selected-for-order="customerSelectedForOrder"
|
||||||
|
@go-to-products="goToProducts"
|
||||||
|
@go-to-orders="goToOrders"
|
||||||
></stall-list>
|
></stall-list>
|
||||||
</q-card-section>
|
</q-tab-panel>
|
||||||
</q-card>
|
|
||||||
<q-card class="q-mt-lg">
|
<!-- Products Tab -->
|
||||||
<q-card-section>
|
<q-tab-panel name="products">
|
||||||
<div class="row">
|
<product-list
|
||||||
<div class="col-12">
|
: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
|
<order-list
|
||||||
ref="orderListRef"
|
ref="orderListRef"
|
||||||
:adminkey="g.user.wallets[0].adminkey"
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
|
|
@ -85,9 +213,8 @@
|
||||||
:customer-pubkey-filter="orderPubkey"
|
:customer-pubkey-filter="orderPubkey"
|
||||||
@customer-selected="customerSelectedForOrder"
|
@customer-selected="customerSelectedForOrder"
|
||||||
></order-list>
|
></order-list>
|
||||||
</div>
|
</q-tab-panel>
|
||||||
</div>
|
</q-tab-panels>
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<q-card v-else>
|
<q-card v-else>
|
||||||
|
|
@ -151,28 +278,88 @@
|
||||||
<div v-if="g.user.admin" class="col-12 q-mb-lg">
|
<div v-if="g.user.admin" class="col-12 q-mb-lg">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section class="q-pa-md">
|
<q-card-section class="q-pa-md">
|
||||||
<q-btn
|
<q-btn-dropdown
|
||||||
label="Restart Nostr Connection"
|
:color="nostrStatusColor"
|
||||||
color="grey"
|
:label="nostrStatusLabel"
|
||||||
outline
|
icon="sync"
|
||||||
|
split
|
||||||
@click="restartNostrConnection"
|
@click="restartNostrConnection"
|
||||||
>
|
>
|
||||||
<q-tooltip>
|
<q-list>
|
||||||
Restart the connection to the nostrclient extension
|
<q-item clickable v-close-popup @click="restartNostrConnection">
|
||||||
</q-tooltip>
|
<q-item-section avatar>
|
||||||
</q-btn>
|
<q-icon name="refresh" color="primary"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Restart Connection</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
Reconnect to the nostrclient extension
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="checkNostrStatus(true)">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="wifi_find" color="primary"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Check Status</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
Check connection to nostrclient
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>
|
||||||
|
<strong>Status:</strong>
|
||||||
|
<q-badge
|
||||||
|
:color="nostrStatus.connected ? 'green' : 'red'"
|
||||||
|
class="q-ml-xs"
|
||||||
|
v-text="nostrStatus.connected ? 'Connected' : 'Disconnected'"
|
||||||
|
></q-badge>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
v-if="nostrStatus.relays_total > 0"
|
||||||
|
caption
|
||||||
|
class="q-mt-xs"
|
||||||
|
>
|
||||||
|
<strong>Relays:</strong>
|
||||||
|
<span v-text="nostrStatus.relays_connected"></span>
|
||||||
|
of
|
||||||
|
<span v-text="nostrStatus.relays_total"></span>
|
||||||
|
connected
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
v-if="nostrStatus.error"
|
||||||
|
caption
|
||||||
|
class="text-negative q-mt-xs"
|
||||||
|
v-text="nostrStatus.error"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-card>
|
<q-card>
|
||||||
|
<q-img
|
||||||
|
src="/nostrmarket/static/market/images/nostr-cover.png"
|
||||||
|
:ratio="3"
|
||||||
|
fit="cover"
|
||||||
|
></q-img>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-my-none">
|
<div class="text-h6 q-mb-sm">Nostr Market</div>
|
||||||
{{SITE_TITLE}} Nostr Market Extension
|
<div class="text-body2 text-grey">
|
||||||
</h6>
|
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.
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-list> {% include "nostrmarket/_api_docs.html" %} </q-list>
|
<q-list> {% include "nostrmarket/_api_docs.html" %} </q-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -257,6 +444,15 @@
|
||||||
<template id="merchant-details"
|
<template id="merchant-details"
|
||||||
>{% include("nostrmarket/components/merchant-details.html") %}</template
|
>{% 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/nostr.bundle.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
||||||
|
|
@ -269,5 +465,8 @@
|
||||||
<script src="{{ static_url_for('nostrmarket/static', 'components/order-list.js') }}"></script>
|
<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/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-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 %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue