feat: add merchant profile panel to Merchant tab

Display merchant profile information with:
- Banner image or grey placeholder
- Profile avatar with shadow
- Display name (or name fallback)
- About description
- NIP-05 verified identity indicator
- Lightning address (LUD16)

Extends MerchantProfile model with new fields:
display_name, banner, nip05, lud16

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ben Weeks 2025-12-24 11:10:39 +00:00
parent 053dcd1785
commit c3dea9f01d
6 changed files with 89 additions and 163 deletions

View file

@ -33,8 +33,12 @@ class Nostrable:
class MerchantProfile(BaseModel):
name: str | None = None
display_name: str | None = None
about: str | None = None
picture: str | None = None
banner: str | None = None
nip05: str | None = None
lud16: str | None = None
class MerchantConfig(MerchantProfile):

View file

@ -2,21 +2,10 @@ window.app.component('key-pair', {
name: 'key-pair',
template: '#key-pair',
delimiters: ['${', '}'],
props: ['public-key', 'private-key'],
data: function () {
return {
showPrivateKey: false
}
},
props: ['public-key', 'private-key', 'merchant-config'],
methods: {
copyText: function (text, message, position) {
var notify = this.$q.notify
Quasar.copyToClipboard(text).then(function () {
notify({
message: message || 'Copied to clipboard!',
position: position || 'bottom'
})
})
handleImageError: function (event) {
event.target.style.display = 'none'
}
}
})

View file

@ -10,7 +10,8 @@ window.app.component('merchant-tab', {
'merchant-active',
'public-key',
'private-key',
'is-admin'
'is-admin',
'merchant-config'
],
computed: {
marketClientUrl: function () {

View file

@ -1,93 +1,82 @@
<div>
<q-separator></q-separator>
<q-card v-if="publicKey" flat bordered>
<!-- Banner Section -->
<div
v-if="merchantConfig && merchantConfig.banner"
class="banner-container"
:style="{
height: '120px',
backgroundSize: 'cover',
backgroundPosition: 'center',
borderRadius: '4px 4px 0 0',
backgroundImage: 'url(' + merchantConfig.banner + ')'
}"
></div>
<div
v-else
class="banner-placeholder bg-grey-9"
style="height: 120px; border-radius: 4px 4px 0 0"
></div>
<!-- Header with toggle -->
<div class="row items-center justify-between q-mt-md q-px-md">
<div class="text-subtitle2">Keys</div>
<q-toggle
v-model="showPrivateKey"
color="primary"
label="Show Private Key"
/>
</div>
<!-- QR Codes Container -->
<div class="row q-col-gutter-md q-pa-md">
<!-- Public Key QR -->
<div class="col-12" :class="showPrivateKey ? 'col-sm-6' : ''">
<q-card flat bordered>
<q-card-section class="text-center">
<div class="text-subtitle2 q-mb-sm">Public Key</div>
<div
class="cursor-pointer q-mx-auto"
style="max-width: 200px"
@click="copyText(publicKey)"
>
<q-responsive :ratio="1">
<lnbits-qrcode
:value="publicKey"
:options="{width: 200}"
:show-buttons="false"
class="rounded-borders"
></lnbits-qrcode>
</q-responsive>
</div>
<div class="q-mt-md text-caption text-mono" style="padding: 0 16px">
<span v-text="publicKey.substring(0, 8)"></span>...<span
v-text="publicKey.substring(publicKey.length - 8)"
></span>
</div>
<q-btn
flat
dense
size="sm"
icon="content_copy"
label="Click to copy"
@click="copyText(publicKey)"
class="q-mt-xs"
<!-- Profile Section -->
<q-card-section class="q-pt-none" style="margin-top: -50px">
<div class="row">
<!-- Profile Image -->
<div class="col-auto">
<q-avatar size="100px" class="shadow-2">
<img
v-if="merchantConfig && merchantConfig.picture"
:src="merchantConfig.picture"
@error="handleImageError"
/>
</q-card-section>
</q-card>
</div>
<q-icon
v-else
name="person"
size="60px"
color="grey-5"
style="background: var(--q-dark); border-radius: 50%"
/>
</q-avatar>
</div>
<!-- Private Key QR (conditional) -->
<div v-if="showPrivateKey" class="col-12 col-sm-6">
<q-card flat bordered>
<q-card-section class="text-center">
<div class="text-subtitle2 q-mb-sm text-warning">
<q-icon name="warning"></q-icon>
Private Key (Keep Secret!)
<!-- Name, About and NIP-05 -->
<div class="col q-pl-md" style="padding-top: 50px">
<div class="row items-center">
<div class="col">
<div
class="text-h6"
v-if="merchantConfig && (merchantConfig.display_name || merchantConfig.name)"
>
<span
v-text="merchantConfig.display_name || merchantConfig.name"
></span>
</div>
<div class="text-caption text-grey" v-else>(No name set)</div>
</div>
</div>
<div
class="text-body2 text-grey q-mt-xs"
v-if="merchantConfig && merchantConfig.about"
style="max-width: 400px"
>
<span v-text="merchantConfig.about"></span>
</div>
<div class="row q-mt-xs q-gutter-sm">
<div
class="text-caption text-grey-5"
v-if="merchantConfig && merchantConfig.nip05"
>
<q-icon name="verified" color="primary" size="14px"></q-icon>
<span v-text="merchantConfig.nip05" class="q-ml-xs"></span>
</div>
<div
class="cursor-pointer q-mx-auto"
style="max-width: 200px"
@click="copyText(privateKey)"
class="text-caption text-grey-5"
v-if="merchantConfig && merchantConfig.lud16"
>
<q-responsive :ratio="1">
<lnbits-qrcode
:value="privateKey"
:options="{width: 200}"
:show-buttons="false"
class="rounded-borders"
></lnbits-qrcode>
</q-responsive>
<q-icon name="bolt" color="warning" size="14px"></q-icon>
<span v-text="merchantConfig.lud16" class="q-ml-xs"></span>
</div>
<div class="q-mt-md text-caption text-mono" style="padding: 0 16px">
<span v-text="privateKey.substring(0, 8)"></span>...<span
v-text="privateKey.substring(privateKey.length - 8)"
></span>
</div>
<q-btn
flat
dense
size="sm"
icon="content_copy"
label="Click to copy"
@click="copyText(privateKey)"
class="q-mt-xs"
/>
</q-card-section>
</q-card>
</div>
</div>
</div>
</div>
</div>
</q-card-section>
</q-card>

View file

@ -1,69 +1,11 @@
<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>
<key-pair
:public-key="publicKey"
:private-key="privateKey"
:merchant-config="merchantConfig"
></key-pair>
</div>
<div class="col-12 col-md-4">
<q-card flat bordered>

View file

@ -152,6 +152,7 @@
:public-key="merchant.public_key"
:private-key="merchant.private_key"
:is-admin="g.user.admin"
:merchant-config="merchant.config"
@toggle-show-keys="toggleShowKeys"
@hide-keys="showKeys = false"
@merchant-deleted="handleMerchantDeleted"