Optimize TransactionsPage for mobile view
Dramatically reduced wasted space and improved mobile UX by: - **Compact header**: Moved refresh button inline with title, similar to NostrFeed - **Compact controls**: All day filter buttons now on one row with Calendar icon - **Removed nested cards**: Eliminated Card wrapper around transactions list - **Full-width layout**: Transactions now use full screen width on mobile (border-b) and rounded cards on desktop (md:border md:rounded-lg) - **Consistent padding**: Uses px-0 on mobile, px-4 on desktop, matching NostrFeed patterns - **Reduced vertical space**: Compacted header section to about half the original height - **Cleaner imports**: Removed unused Card, CardContent, CardHeader, CardTitle, CardDescription, and Separator components Layout now follows NostrFeed's mobile-optimized patterns with max-w-3xl container and responsive spacing. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9c4b14f382
commit
557d7ecacc
1 changed files with 142 additions and 145 deletions
|
|
@ -8,10 +8,8 @@ import type { Transaction } from '../types'
|
|||
import type { FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
||||
import FuzzySearch from '@/components/ui/fuzzy-search/FuzzySearch.vue'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
|
|
@ -177,164 +175,158 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto p-4 max-w-4xl">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-2">My Transactions</h1>
|
||||
<p class="text-muted-foreground">View your recent transaction history</p>
|
||||
<div class="flex flex-col">
|
||||
<!-- Compact Header -->
|
||||
<div class="flex flex-col gap-3 p-4 md:p-6 border-b md:bg-card/50 md:backdrop-blur-sm">
|
||||
<div class="w-full max-w-3xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h1 class="text-lg md:text-xl font-bold">My Transactions</h1>
|
||||
<p class="text-xs md:text-sm text-muted-foreground">View your recent transaction history</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="loadTransactions"
|
||||
:disabled="isLoading"
|
||||
class="gap-2 md:h-10 md:px-4 hover:bg-accent transition-colors"
|
||||
>
|
||||
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4" />
|
||||
<span class="hidden md:inline">Refresh</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Compact Controls Row -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Calendar class="h-4 w-4 text-muted-foreground" />
|
||||
<Button
|
||||
v-for="option in dayOptions"
|
||||
:key="option.value"
|
||||
:variant="selectedDays === option.value ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-8 px-3 text-xs"
|
||||
@click="changeDayFilter(option.value)"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ option.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="mb-4">
|
||||
<FuzzySearch
|
||||
:data="transactions"
|
||||
:options="searchOptions"
|
||||
placeholder="Search transactions by description, payee, reference..."
|
||||
@results="handleSearchResults"
|
||||
/>
|
||||
</div>
|
||||
<!-- Content Container -->
|
||||
<div class="w-full max-w-3xl mx-auto px-0 md:px-4">
|
||||
<!-- Search Bar -->
|
||||
<div class="px-4 md:px-0 py-3">
|
||||
<FuzzySearch
|
||||
:data="transactions"
|
||||
:options="searchOptions"
|
||||
placeholder="Search transactions..."
|
||||
@results="handleSearchResults"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<Card class="mb-4">
|
||||
<CardContent class="pt-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<!-- Day Filter -->
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<Calendar class="h-4 w-4" />
|
||||
<span>Show transactions from:</span>
|
||||
<!-- Results Count -->
|
||||
<div class="px-4 md:px-0 py-2 text-xs md:text-sm text-muted-foreground">
|
||||
<span v-if="searchResults.length > 0">
|
||||
Found {{ transactionsToDisplay.length }} matching transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }}
|
||||
</span>
|
||||
<span v-else>
|
||||
Showing {{ pagination.offset + 1 }} - {{ Math.min(pagination.offset + pagination.limit, pagination.total) }} of {{ pagination.total }} transactions
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-12">
|
||||
<div class="flex items-center gap-2">
|
||||
<RefreshCw class="h-4 w-4 animate-spin" />
|
||||
<span class="text-muted-foreground">Loading transactions...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="transactionsToDisplay.length === 0" class="text-center py-12 px-4">
|
||||
<p class="text-muted-foreground">No transactions found</p>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
{{ searchResults.length > 0 ? 'Try a different search term' : 'Try selecting a different time period' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Items (Full-width on mobile, no nested cards) -->
|
||||
<div v-else class="md:space-y-3 md:py-4">
|
||||
<div
|
||||
v-for="transaction in transactionsToDisplay"
|
||||
:key="transaction.id"
|
||||
class="border-b md:border md:rounded-lg p-4 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<!-- Transaction Header -->
|
||||
<div class="flex items-start justify-between gap-3 mb-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<!-- Status Icon -->
|
||||
<component
|
||||
v-if="getStatusInfo(transaction.flag)"
|
||||
:is="getStatusInfo(transaction.flag)!.icon"
|
||||
:class="[
|
||||
'h-4 w-4 flex-shrink-0',
|
||||
getStatusInfo(transaction.flag)!.color
|
||||
]"
|
||||
/>
|
||||
<h3 class="font-medium text-sm sm:text-base truncate">
|
||||
{{ transaction.description }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground">
|
||||
{{ formatDate(transaction.date) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
v-for="option in dayOptions"
|
||||
:key="option.value"
|
||||
:variant="selectedDays === option.value ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="changeDayFilter(option.value)"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ option.label }}
|
||||
</Button>
|
||||
|
||||
<!-- Amount -->
|
||||
<div class="text-right flex-shrink-0">
|
||||
<p class="font-semibold text-sm sm:text-base">
|
||||
{{ formatAmount(transaction.amount) }} sats
|
||||
</p>
|
||||
<p v-if="transaction.fiat_amount" class="text-xs text-muted-foreground">
|
||||
{{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<Button variant="outline" size="sm" @click="loadTransactions" :disabled="isLoading">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': isLoading }" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Transactions List -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Transactions</CardTitle>
|
||||
<CardDescription>
|
||||
<span v-if="searchResults.length > 0">
|
||||
Found {{ transactionsToDisplay.length }} matching transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }}
|
||||
</span>
|
||||
<span v-else>
|
||||
Showing {{ pagination.offset + 1 }} -
|
||||
{{ Math.min(pagination.offset + pagination.limit, pagination.total) }} of
|
||||
{{ pagination.total }} transactions
|
||||
</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="text-center py-12">
|
||||
<RefreshCw class="h-8 w-8 animate-spin mx-auto mb-4 text-muted-foreground" />
|
||||
<p class="text-muted-foreground">Loading transactions...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="transactionsToDisplay.length === 0" class="text-center py-12">
|
||||
<p class="text-muted-foreground">No transactions found</p>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
{{ searchResults.length > 0 ? 'Try a different search term' : 'Try selecting a different time period' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Items (Mobile-Optimized) -->
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="(transaction, index) in transactionsToDisplay"
|
||||
:key="transaction.id"
|
||||
class="border rounded-lg p-4 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<!-- Transaction Header -->
|
||||
<div class="flex items-start justify-between gap-3 mb-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<!-- Status Icon -->
|
||||
<component
|
||||
v-if="getStatusInfo(transaction.flag)"
|
||||
:is="getStatusInfo(transaction.flag)!.icon"
|
||||
:class="[
|
||||
'h-4 w-4 flex-shrink-0',
|
||||
getStatusInfo(transaction.flag)!.color
|
||||
]"
|
||||
/>
|
||||
<h3 class="font-medium text-sm sm:text-base truncate">
|
||||
{{ transaction.description }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground">
|
||||
{{ formatDate(transaction.date) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Amount -->
|
||||
<div class="text-right flex-shrink-0">
|
||||
<p class="font-semibold text-sm sm:text-base">
|
||||
{{ formatAmount(transaction.amount) }} sats
|
||||
</p>
|
||||
<p v-if="transaction.fiat_amount" class="text-xs text-muted-foreground">
|
||||
{{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Transaction Details -->
|
||||
<div class="space-y-1 text-xs sm:text-sm">
|
||||
<!-- Payee -->
|
||||
<div v-if="transaction.payee" class="text-muted-foreground">
|
||||
<span class="font-medium">Payee:</span> {{ transaction.payee }}
|
||||
</div>
|
||||
|
||||
<!-- Transaction Details (Collapsible on mobile) -->
|
||||
<div class="space-y-1 text-xs sm:text-sm">
|
||||
<!-- Payee -->
|
||||
<div v-if="transaction.payee" class="text-muted-foreground">
|
||||
<span class="font-medium">Payee:</span> {{ transaction.payee }}
|
||||
</div>
|
||||
|
||||
<!-- Reference -->
|
||||
<div v-if="transaction.reference" class="text-muted-foreground">
|
||||
<span class="font-medium">Ref:</span> {{ transaction.reference }}
|
||||
</div>
|
||||
|
||||
<!-- Username (if available) -->
|
||||
<div v-if="transaction.username" class="text-muted-foreground">
|
||||
<span class="font-medium">User:</span> {{ transaction.username }}
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div v-if="transaction.tags && transaction.tags.length > 0" class="flex flex-wrap gap-1 mt-2">
|
||||
<Badge v-for="tag in transaction.tags" :key="tag" variant="secondary" class="text-xs">
|
||||
{{ tag }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Source -->
|
||||
<div v-if="transaction.meta?.source" class="text-muted-foreground mt-1">
|
||||
<span class="text-xs">Source: {{ transaction.meta.source }}</span>
|
||||
</div>
|
||||
<!-- Reference -->
|
||||
<div v-if="transaction.reference" class="text-muted-foreground">
|
||||
<span class="font-medium">Ref:</span> {{ transaction.reference }}
|
||||
</div>
|
||||
|
||||
<!-- Separator between items (except last) -->
|
||||
<Separator v-if="index < transactionsToDisplay.length - 1" class="mt-3" />
|
||||
<!-- Username (if available) -->
|
||||
<div v-if="transaction.username" class="text-muted-foreground">
|
||||
<span class="font-medium">User:</span> {{ transaction.username }}
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div v-if="transaction.tags && transaction.tags.length > 0" class="flex flex-wrap gap-1 mt-2">
|
||||
<Badge v-for="tag in transaction.tags" :key="tag" variant="secondary" class="text-xs">
|
||||
{{ tag }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Source -->
|
||||
<div v-if="transaction.meta?.source" class="text-muted-foreground mt-1">
|
||||
<span class="text-xs">Source: {{ transaction.meta.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination (hide when searching) -->
|
||||
<div
|
||||
v-if="!isLoading && transactions.length > 0 && searchResults.length === 0 && (pagination.has_next || pagination.has_prev)"
|
||||
class="flex items-center justify-between mt-6 pt-4 border-t"
|
||||
class="flex items-center justify-between px-4 md:px-0 py-6 border-t"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -360,7 +352,12 @@ onMounted(() => {
|
|||
<ChevronRight class="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- End of list indicator -->
|
||||
<div v-if="transactionsToDisplay.length > 0" class="text-center py-6 text-md text-muted-foreground">
|
||||
<p>🐢</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue