Refactor WalletPage.vue to streamline layout and enhance user experience
- Removed unused tab components and associated state management to simplify the codebase. - Improved button and QR code display elements for better interaction and visibility. - Adjusted layout for transaction history to enhance readability and organization. - Cleaned up commented-out code and unnecessary variables, improving overall code clarity. These changes contribute to a more efficient and user-friendly wallet interface.
This commit is contained in:
parent
91c76e80e7
commit
112b391a7e
1 changed files with 142 additions and 283 deletions
|
|
@ -3,7 +3,6 @@ import { ref, computed, onMounted } from 'vue'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
|
@ -25,7 +24,6 @@ const showReceiveDialog = ref(false)
|
||||||
const showSendDialog = ref(false)
|
const showSendDialog = ref(false)
|
||||||
const showQRScanner = ref(false)
|
const showQRScanner = ref(false)
|
||||||
const scannedDestination = ref<string>('')
|
const scannedDestination = ref<string>('')
|
||||||
const selectedTab = ref('transactions')
|
|
||||||
const defaultQrCode = ref<string | null>(null)
|
const defaultQrCode = ref<string | null>(null)
|
||||||
const isGeneratingQR = ref(false)
|
const isGeneratingQR = ref(false)
|
||||||
const copiedField = ref<string | null>(null)
|
const copiedField = ref<string | null>(null)
|
||||||
|
|
@ -37,16 +35,9 @@ const error = computed(() => walletService?.error?.value)
|
||||||
// Use PaymentService for centralized balance calculation
|
// Use PaymentService for centralized balance calculation
|
||||||
const totalBalance = computed(() => paymentService?.totalBalance || 0)
|
const totalBalance = computed(() => paymentService?.totalBalance || 0)
|
||||||
|
|
||||||
|
// TODO: clean up; assuming we only ever give the user one auto-generated paylink
|
||||||
const payLinks = computed(() => walletService?.payLinks?.value || [])
|
const payLinks = computed(() => walletService?.payLinks?.value || [])
|
||||||
const firstPayLink = computed(() => payLinks.value[0] || null)
|
const firstPayLink = computed(() => payLinks.value[0] || null)
|
||||||
const baseDomain = computed(() => {
|
|
||||||
try {
|
|
||||||
const baseUrl = paymentService?.config?.baseUrl || 'http://localhost'
|
|
||||||
return new URL(baseUrl).hostname
|
|
||||||
} catch {
|
|
||||||
return 'localhost'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get transactions grouped by date
|
// Get transactions grouped by date
|
||||||
const groupedTransactions = computed(() => {
|
const groupedTransactions = computed(() => {
|
||||||
|
|
@ -190,12 +181,7 @@ onMounted(async () => {
|
||||||
<Wallet class="h-6 w-6 sm:h-8 sm:w-8" />
|
<Wallet class="h-6 w-6 sm:h-8 sm:w-8" />
|
||||||
Wallet
|
Wallet
|
||||||
</h1>
|
</h1>
|
||||||
<Button
|
<Button variant="ghost" size="sm" @click="refresh" :disabled="isLoading">
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
@click="refresh"
|
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': isLoading }" />
|
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': isLoading }" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,34 +199,25 @@ onMounted(async () => {
|
||||||
<!-- Balance Section -->
|
<!-- Balance Section -->
|
||||||
<div class="text-center sm:text-left">
|
<div class="text-center sm:text-left">
|
||||||
<div class="text-3xl sm:text-4xl font-bold">
|
<div class="text-3xl sm:text-4xl font-bold">
|
||||||
{{ Math.floor(totalBalance / 1000).toLocaleString() }} <span class="text-xl sm:text-2xl text-muted-foreground font-normal">sats</span>
|
{{ Math.floor(totalBalance / 1000).toLocaleString() }} <span
|
||||||
|
class="text-xl sm:text-2xl text-muted-foreground font-normal">sats</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex gap-2 sm:gap-3 w-full sm:w-auto">
|
<div class="flex gap-2 sm:gap-3 w-full sm:w-auto">
|
||||||
<Button
|
<Button variant="default" @click="showReceiveDialog = true"
|
||||||
variant="default"
|
class="gap-2 flex-1 sm:flex-none h-10 sm:h-12 text-sm sm:text-base">
|
||||||
@click="showReceiveDialog = true"
|
|
||||||
class="gap-2 flex-1 sm:flex-none h-10 sm:h-12 text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
<QrCode class="h-4 w-4 sm:h-5 sm:w-5" />
|
<QrCode class="h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
Receive
|
Receive
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" @click="showSendDialog = true"
|
||||||
variant="outline"
|
class="gap-2 flex-1 sm:flex-none h-10 sm:h-12 text-sm sm:text-base">
|
||||||
@click="showSendDialog = true"
|
|
||||||
class="gap-2 flex-1 sm:flex-none h-10 sm:h-12 text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
<Send class="h-4 w-4 sm:h-5 sm:w-5" />
|
<Send class="h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" @click="showQRScanner = true" class="h-10 w-10 sm:h-12 sm:w-12 p-0"
|
||||||
variant="outline"
|
title="Scan QR Code">
|
||||||
@click="showQRScanner = true"
|
|
||||||
class="h-10 w-10 sm:h-12 sm:w-12 p-0"
|
|
||||||
title="Scan QR Code"
|
|
||||||
>
|
|
||||||
<ScanLine class="h-4 w-4 sm:h-5 sm:w-5" />
|
<ScanLine class="h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -261,16 +238,14 @@ onMounted(async () => {
|
||||||
<div class="flex flex-col sm:flex-row items-center gap-4 sm:gap-6">
|
<div class="flex flex-col sm:flex-row items-center gap-4 sm:gap-6">
|
||||||
<!-- QR Code -->
|
<!-- QR Code -->
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<div v-if="isGeneratingQR" class="w-48 h-48 sm:w-56 sm:h-56 flex items-center justify-center bg-muted rounded-lg">
|
<div v-if="isGeneratingQR"
|
||||||
|
class="w-48 h-48 sm:w-56 sm:h-56 flex items-center justify-center bg-muted rounded-lg">
|
||||||
<RefreshCw class="h-6 w-6 sm:h-8 sm:w-8 animate-spin text-muted-foreground" />
|
<RefreshCw class="h-6 w-6 sm:h-8 sm:w-8 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="defaultQrCode" class="text-center">
|
<div v-else-if="defaultQrCode" class="text-center">
|
||||||
<div class="bg-white p-2 sm:p-4 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors" @click="handleQRClick" title="Click to copy LNURL">
|
<div class="bg-white p-2 sm:p-4 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
<img
|
@click="handleQRClick" title="Click to copy LNURL">
|
||||||
:src="defaultQrCode"
|
<img :src="defaultQrCode" alt="LNURL QR Code" class="w-48 h-48 sm:w-56 sm:h-56" />
|
||||||
alt="LNURL QR Code"
|
|
||||||
class="w-48 h-48 sm:w-56 sm:h-56"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-48 h-48 sm:w-56 sm:h-56 flex items-center justify-center bg-muted rounded-lg">
|
<div v-else class="w-48 h-48 sm:w-56 sm:h-56 flex items-center justify-center bg-muted rounded-lg">
|
||||||
|
|
@ -293,13 +268,9 @@ onMounted(async () => {
|
||||||
<div class="font-mono text-xs sm:text-sm bg-muted px-2 sm:px-3 py-1.5 sm:py-2 rounded flex-1 break-all">
|
<div class="font-mono text-xs sm:text-sm bg-muted px-2 sm:px-3 py-1.5 sm:py-2 rounded flex-1 break-all">
|
||||||
{{ firstPayLink.lnaddress }}
|
{{ firstPayLink.lnaddress }}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="outline" size="sm"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="copyToClipboard(firstPayLink.lnaddress || '', 'lightning-address')"
|
@click="copyToClipboard(firstPayLink.lnaddress || '', 'lightning-address')"
|
||||||
class="h-auto px-2 shrink-0"
|
class="h-auto px-2 shrink-0" title="Copy Lightning Address">
|
||||||
title="Copy Lightning Address"
|
|
||||||
>
|
|
||||||
<Check v-if="copiedField === 'lightning-address'" class="h-3 w-3 sm:h-4 sm:w-4 text-green-600" />
|
<Check v-if="copiedField === 'lightning-address'" class="h-3 w-3 sm:h-4 sm:w-4 text-green-600" />
|
||||||
<Copy v-else class="h-3 w-3 sm:h-4 sm:w-4" />
|
<Copy v-else class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -311,251 +282,142 @@ onMounted(async () => {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Transaction History -->
|
||||||
<Tabs v-model="selectedTab" class="w-full">
|
<Card>
|
||||||
<TabsList class="grid w-full grid-cols-2 h-9 sm:h-10">
|
<CardHeader class="pb-3 sm:pb-6">
|
||||||
<TabsTrigger value="transactions" class="text-sm sm:text-base">Transaction History</TabsTrigger>
|
<CardTitle class="text-lg sm:text-xl">Recent Transactions</CardTitle>
|
||||||
<TabsTrigger value="addresses" class="text-sm sm:text-base">Receive Addresses</TabsTrigger>
|
<CardDescription class="text-sm">Your payment history</CardDescription>
|
||||||
</TabsList>
|
</CardHeader>
|
||||||
|
<CardContent class="pt-0">
|
||||||
|
<div v-if="error" class="text-center py-6 sm:py-8 text-destructive">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Transactions Tab -->
|
<div v-else-if="transactions.length === 0" class="text-center py-8 sm:py-12 text-muted-foreground">
|
||||||
<TabsContent value="transactions" class="space-y-3 sm:space-y-4 mt-3 sm:mt-4">
|
<Clock class="h-8 w-8 sm:h-12 sm:w-12 mx-auto mb-3 sm:mb-4 opacity-50" />
|
||||||
<Card>
|
<p class="text-sm sm:text-base">No transactions yet</p>
|
||||||
<CardHeader class="pb-3 sm:pb-6">
|
<p class="text-xs sm:text-sm mt-1 sm:mt-2">Your transaction history will appear here</p>
|
||||||
<CardTitle class="text-lg sm:text-xl">Recent Transactions</CardTitle>
|
</div>
|
||||||
<CardDescription class="text-sm">Your payment history</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="pt-0">
|
|
||||||
<div v-if="error" class="text-center py-6 sm:py-8 text-destructive">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="transactions.length === 0" class="text-center py-8 sm:py-12 text-muted-foreground">
|
<ScrollArea v-else class="h-[400px] sm:h-[500px] pr-2 sm:pr-4">
|
||||||
<Clock class="h-8 w-8 sm:h-12 sm:w-12 mx-auto mb-3 sm:mb-4 opacity-50" />
|
<div v-for="(txs, date) in groupedTransactions" :key="date" class="mb-4 sm:mb-6">
|
||||||
<p class="text-sm sm:text-base">No transactions yet</p>
|
<div class="text-xs sm:text-sm font-medium text-muted-foreground mb-2 sm:mb-3">{{ date }}</div>
|
||||||
<p class="text-xs sm:text-sm mt-1 sm:mt-2">Your transaction history will appear here</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollArea v-else class="h-[400px] sm:h-[500px] pr-2 sm:pr-4">
|
<div class="space-y-3 sm:space-y-4">
|
||||||
<div v-for="(txs, date) in groupedTransactions" :key="date" class="mb-4 sm:mb-6">
|
<div v-for="tx in txs" :key="tx.id"
|
||||||
<div class="text-xs sm:text-sm font-medium text-muted-foreground mb-2 sm:mb-3">{{ date }}</div>
|
class="relative p-2.5 sm:p-3 rounded-lg border hover:bg-accent/50 transition-colors">
|
||||||
|
<!-- Tag badge in top-left corner -->
|
||||||
<div class="space-y-3 sm:space-y-4">
|
<Badge v-if="tx.tag" variant="secondary"
|
||||||
<div
|
class="absolute -top-2.75 left-8 sm:left-11 text-xs font-medium z-10 bg-blue-100 text-blue-800 border-blue-200 pointer-events-none">
|
||||||
v-for="tx in txs"
|
{{ tx.tag }}
|
||||||
:key="tx.id"
|
</Badge>
|
||||||
class="relative p-2.5 sm:p-3 rounded-lg border hover:bg-accent/50 transition-colors"
|
<!-- Mobile Layout: Stack vertically -->
|
||||||
>
|
<div class="block sm:hidden">
|
||||||
<!-- Tag badge in top-left corner -->
|
<!-- Header Row: Icon, Amount, Type -->
|
||||||
<Badge v-if="tx.tag" variant="secondary" class="absolute -top-2.75 left-8 sm:left-11 text-xs font-medium z-10 bg-blue-100 text-blue-800 border-blue-200 pointer-events-none">
|
<div class="flex items-center justify-between mb-1.5">
|
||||||
{{ tx.tag }}
|
<div class="flex items-center gap-2">
|
||||||
</Badge>
|
<div class="p-1 rounded-full bg-background border"
|
||||||
<!-- Mobile Layout: Stack vertically -->
|
:class="getTransactionColor(tx.type, tx.status)">
|
||||||
<div class="block sm:hidden">
|
<component :is="getTransactionIcon(tx.type, tx.status)" class="h-3.5 w-3.5" />
|
||||||
<!-- Header Row: Icon, Amount, Type -->
|
</div>
|
||||||
<div class="flex items-center justify-between mb-1.5">
|
<span class="text-xs text-muted-foreground">{{
|
||||||
<div class="flex items-center gap-2">
|
(() => {
|
||||||
<div
|
try {
|
||||||
class="p-1 rounded-full bg-background border"
|
if (tx.timestamp instanceof Date && !isNaN(tx.timestamp.getTime())) {
|
||||||
:class="getTransactionColor(tx.type, tx.status)"
|
return format(tx.timestamp, 'HH:mm')
|
||||||
>
|
} else if (tx.timestamp) {
|
||||||
<component
|
// Try to parse as string or number
|
||||||
:is="getTransactionIcon(tx.type, tx.status)"
|
const date = new Date(tx.timestamp)
|
||||||
class="h-3.5 w-3.5"
|
if (!isNaN(date.getTime())) {
|
||||||
/>
|
return format(date, 'HH:mm')
|
||||||
</div>
|
|
||||||
<span class="text-xs text-muted-foreground">{{
|
|
||||||
(() => {
|
|
||||||
try {
|
|
||||||
if (tx.timestamp instanceof Date && !isNaN(tx.timestamp.getTime())) {
|
|
||||||
return format(tx.timestamp, 'HH:mm')
|
|
||||||
} else if (tx.timestamp) {
|
|
||||||
// Try to parse as string or number
|
|
||||||
const date = new Date(tx.timestamp)
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
return format(date, 'HH:mm')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '--:--'
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to format timestamp:', tx.timestamp, error)
|
|
||||||
return '--:--'
|
|
||||||
}
|
}
|
||||||
})()
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-right">
|
|
||||||
<p
|
|
||||||
class="font-semibold text-sm"
|
|
||||||
:class="getTransactionColor(tx.type, tx.status)"
|
|
||||||
>
|
|
||||||
{{ tx.type === 'received' ? '+' : '-' }}{{ tx.amount.toLocaleString() }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-muted-foreground">sats</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description Row -->
|
|
||||||
<div class="mb-1">
|
|
||||||
<p class="font-medium text-xs leading-tight break-words pr-1">{{ tx.description }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Badges Row -->
|
|
||||||
<div class="flex items-center gap-1.5 flex-wrap">
|
|
||||||
<span v-if="tx.fee" class="text-xs text-muted-foreground">
|
|
||||||
Fee: {{ tx.fee }} sats
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop Layout: Better space management -->
|
|
||||||
<div class="hidden sm:flex items-start gap-4">
|
|
||||||
<div class="flex items-center gap-3 flex-shrink-0">
|
|
||||||
<div
|
|
||||||
class="p-2 rounded-full bg-background border"
|
|
||||||
:class="getTransactionColor(tx.type, tx.status)"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="getTransactionIcon(tx.type, tx.status)"
|
|
||||||
class="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm text-muted-foreground">{{
|
|
||||||
(() => {
|
|
||||||
try {
|
|
||||||
if (tx.timestamp instanceof Date && !isNaN(tx.timestamp.getTime())) {
|
|
||||||
return format(tx.timestamp, 'HH:mm')
|
|
||||||
} else if (tx.timestamp) {
|
|
||||||
// Try to parse as string or number
|
|
||||||
const date = new Date(tx.timestamp)
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
return format(date, 'HH:mm')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '--:--'
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to format timestamp:', tx.timestamp, error)
|
|
||||||
return '--:--'
|
|
||||||
}
|
}
|
||||||
})()
|
return '--:--'
|
||||||
}}</span>
|
} catch (error) {
|
||||||
</div>
|
console.warn('Failed to format timestamp:', tx.timestamp, error)
|
||||||
|
return '--:--'
|
||||||
<!-- Description with flexible width -->
|
}
|
||||||
<div class="min-w-0 flex-1">
|
})()
|
||||||
<p class="font-medium break-words leading-tight pr-2">{{ tx.description }}</p>
|
}}</span>
|
||||||
<div v-if="tx.fee" class="text-xs text-muted-foreground mt-1">
|
|
||||||
Fee: {{ tx.fee }} sats
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Amount - fixed width, always visible -->
|
|
||||||
<div class="text-right flex-shrink-0 min-w-[100px]">
|
|
||||||
<p
|
|
||||||
class="font-semibold whitespace-nowrap"
|
|
||||||
:class="getTransactionColor(tx.type, tx.status)"
|
|
||||||
>
|
|
||||||
{{ tx.type === 'received' ? '+' : '-' }}{{ tx.amount.toLocaleString() }} sats
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<!-- Addresses Tab -->
|
|
||||||
<TabsContent value="addresses" class="space-y-3 sm:space-y-4 mt-3 sm:mt-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader class="pb-3 sm:pb-6">
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-0">
|
|
||||||
<div>
|
|
||||||
<CardTitle class="text-lg sm:text-xl">Receive Addresses</CardTitle>
|
|
||||||
<CardDescription class="text-sm">Your LNURL and Lightning addresses for receiving payments</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
@click="showReceiveDialog = true"
|
|
||||||
class="w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
<QrCode class="h-4 w-4 mr-2" />
|
|
||||||
New Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="pt-0">
|
|
||||||
<div v-if="walletService?.payLinks?.value?.length === 0" class="text-center py-8 sm:py-12 text-muted-foreground">
|
|
||||||
<QrCode class="h-8 w-8 sm:h-12 sm:w-12 mx-auto mb-3 sm:mb-4 opacity-50" />
|
|
||||||
<p class="text-sm sm:text-base">No receive addresses created</p>
|
|
||||||
<p class="text-xs sm:text-sm mt-1 sm:mt-2">Create an address to start receiving payments</p>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
class="mt-3 sm:mt-4"
|
|
||||||
@click="showReceiveDialog = true"
|
|
||||||
>
|
|
||||||
Create Your First Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="space-y-3 sm:space-y-4">
|
|
||||||
<div
|
|
||||||
v-for="link in walletService?.payLinks?.value"
|
|
||||||
:key="link.id"
|
|
||||||
class="p-3 sm:p-4 border rounded-lg hover:bg-accent/50 transition-colors"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-3 sm:gap-0">
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="font-medium text-sm sm:text-base">{{ link.description }}</p>
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mt-2 text-sm text-muted-foreground">
|
|
||||||
<span class="text-xs sm:text-sm">{{ link.min }}-{{ link.max }} sats</span>
|
|
||||||
<Badge v-if="link.username" variant="secondary" class="w-fit text-xs">
|
|
||||||
{{ link.username }}@{{ baseDomain }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 sm:mt-3 space-y-1.5 sm:space-y-2">
|
<div class="text-right">
|
||||||
<div v-if="link.lnaddress" class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
|
<p class="font-semibold text-sm" :class="getTransactionColor(tx.type, tx.status)">
|
||||||
<span class="text-xs text-muted-foreground">Lightning Address:</span>
|
{{ tx.type === 'received' ? '+' : '-' }}{{ tx.amount.toLocaleString() }}
|
||||||
<code class="text-xs bg-muted px-2 py-1 rounded break-all">{{ link.lnaddress }}</code>
|
</p>
|
||||||
</div>
|
<p class="text-xs text-muted-foreground">sats</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<!-- Description Row -->
|
||||||
variant="ghost"
|
<div class="mb-1">
|
||||||
size="sm"
|
<p class="font-medium text-xs leading-tight break-words pr-1">{{ tx.description }}</p>
|
||||||
@click="showReceiveDialog = true"
|
</div>
|
||||||
>
|
|
||||||
<QrCode class="h-4 w-4" />
|
<!-- Badges Row -->
|
||||||
</Button>
|
<div class="flex items-center gap-1.5 flex-wrap">
|
||||||
|
<span v-if="tx.fee" class="text-xs text-muted-foreground">
|
||||||
|
Fee: {{ tx.fee }} sats
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Layout: Better space management -->
|
||||||
|
<div class="hidden sm:flex items-start gap-4">
|
||||||
|
<div class="flex items-center gap-3 flex-shrink-0">
|
||||||
|
<div class="p-2 rounded-full bg-background border" :class="getTransactionColor(tx.type, tx.status)">
|
||||||
|
<component :is="getTransactionIcon(tx.type, tx.status)" class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-muted-foreground">{{
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
if (tx.timestamp instanceof Date && !isNaN(tx.timestamp.getTime())) {
|
||||||
|
return format(tx.timestamp, 'HH:mm')
|
||||||
|
} else if (tx.timestamp) {
|
||||||
|
// Try to parse as string or number
|
||||||
|
const date = new Date(tx.timestamp)
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
return format(date, 'HH:mm')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '--:--'
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to format timestamp:', tx.timestamp, error)
|
||||||
|
return '--:--'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description with flexible width -->
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="font-medium break-words leading-tight pr-2">{{ tx.description }}</p>
|
||||||
|
<div v-if="tx.fee" class="text-xs text-muted-foreground mt-1">
|
||||||
|
Fee: {{ tx.fee }} sats
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Amount - fixed width, always visible -->
|
||||||
|
<div class="text-right flex-shrink-0 min-w-[100px]">
|
||||||
|
<p class="font-semibold whitespace-nowrap" :class="getTransactionColor(tx.type, tx.status)">
|
||||||
|
{{ tx.type === 'received' ? '+' : '-' }}{{ tx.amount.toLocaleString() }} sats
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</ScrollArea>
|
||||||
</TabsContent>
|
</CardContent>
|
||||||
</Tabs>
|
</Card>
|
||||||
|
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
<ReceiveDialog
|
<ReceiveDialog :open="showReceiveDialog" @update:open="showReceiveDialog = $event" />
|
||||||
:open="showReceiveDialog"
|
|
||||||
@update:open="showReceiveDialog = $event"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SendDialog
|
<SendDialog v-if="showSendDialog" :open="showSendDialog" :initial-destination="scannedDestination" @update:open="(open) => {
|
||||||
v-if="showSendDialog"
|
showSendDialog = open
|
||||||
:open="showSendDialog"
|
if (!open) scannedDestination = ''
|
||||||
:initial-destination="scannedDestination"
|
}" />
|
||||||
@update:open="(open) => {
|
|
||||||
showSendDialog = open
|
|
||||||
if (!open) scannedDestination = ''
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- QR Scanner Dialog -->
|
<!-- QR Scanner Dialog -->
|
||||||
<Dialog :open="showQRScanner" @update:open="showQRScanner = $event">
|
<Dialog :open="showQRScanner" @update:open="showQRScanner = $event">
|
||||||
|
|
@ -570,10 +432,7 @@ onMounted(async () => {
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<QRScanner
|
<QRScanner @result="handleQRScanResult" @close="closeQRScanner" />
|
||||||
@result="handleQRScanResult"
|
|
||||||
@close="closeQRScanner"
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue