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:
padreug 2025-11-14 16:39:32 +01:00
parent 9c4b14f382
commit 557d7ecacc

View file

@ -8,10 +8,8 @@ import type { Transaction } from '../types'
import type { FuzzySearchOptions } from '@/composables/useFuzzySearch' import type { FuzzySearchOptions } from '@/composables/useFuzzySearch'
import FuzzySearch from '@/components/ui/fuzzy-search/FuzzySearch.vue' 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 { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { import {
CheckCircle2, CheckCircle2,
Clock, Clock,
@ -177,164 +175,158 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="container mx-auto p-4 max-w-4xl"> <div class="flex flex-col">
<!-- Header --> <!-- Compact Header -->
<div class="mb-6"> <div class="flex flex-col gap-3 p-4 md:p-6 border-b md:bg-card/50 md:backdrop-blur-sm">
<h1 class="text-2xl sm:text-3xl font-bold mb-2">My Transactions</h1> <div class="w-full max-w-3xl mx-auto">
<p class="text-muted-foreground">View your recent transaction history</p> <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> </div>
<!-- Search Bar --> <!-- Content Container -->
<div class="mb-4"> <div class="w-full max-w-3xl mx-auto px-0 md:px-4">
<FuzzySearch <!-- Search Bar -->
:data="transactions" <div class="px-4 md:px-0 py-3">
:options="searchOptions" <FuzzySearch
placeholder="Search transactions by description, payee, reference..." :data="transactions"
@results="handleSearchResults" :options="searchOptions"
/> placeholder="Search transactions..."
</div> @results="handleSearchResults"
/>
</div>
<!-- Controls --> <!-- Results Count -->
<Card class="mb-4"> <div class="px-4 md:px-0 py-2 text-xs md:text-sm text-muted-foreground">
<CardContent class="pt-6"> <span v-if="searchResults.length > 0">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> Found {{ transactionsToDisplay.length }} matching transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }}
<!-- Day Filter --> </span>
<div class="space-y-2"> <span v-else>
<div class="text-sm text-muted-foreground flex items-center gap-2"> Showing {{ pagination.offset + 1 }} - {{ Math.min(pagination.offset + pagination.limit, pagination.total) }} of {{ pagination.total }} transactions
<Calendar class="h-4 w-4" /> </span>
<span>Show transactions from:</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>
<div class="flex flex-wrap gap-2">
<Button <!-- Amount -->
v-for="option in dayOptions" <div class="text-right flex-shrink-0">
:key="option.value" <p class="font-semibold text-sm sm:text-base">
:variant="selectedDays === option.value ? 'default' : 'outline'" {{ formatAmount(transaction.amount) }} sats
size="sm" </p>
@click="changeDayFilter(option.value)" <p v-if="transaction.fiat_amount" class="text-xs text-muted-foreground">
:disabled="isLoading" {{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }}
> </p>
{{ option.label }}
</Button>
</div> </div>
</div> </div>
<!-- Refresh Button --> <!-- Transaction Details -->
<Button variant="outline" size="sm" @click="loadTransactions" :disabled="isLoading"> <div class="space-y-1 text-xs sm:text-sm">
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': isLoading }" /> <!-- Payee -->
</Button> <div v-if="transaction.payee" class="text-muted-foreground">
</div> <span class="font-medium">Payee:</span> {{ transaction.payee }}
</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>
</div> </div>
<!-- Transaction Details (Collapsible on mobile) --> <!-- Reference -->
<div class="space-y-1 text-xs sm:text-sm"> <div v-if="transaction.reference" class="text-muted-foreground">
<!-- Payee --> <span class="font-medium">Ref:</span> {{ transaction.reference }}
<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>
</div> </div>
<!-- Separator between items (except last) --> <!-- Username (if available) -->
<Separator v-if="index < transactionsToDisplay.length - 1" class="mt-3" /> <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>
</div> </div>
<!-- Pagination (hide when searching) --> <!-- Pagination (hide when searching) -->
<div <div
v-if="!isLoading && transactions.length > 0 && searchResults.length === 0 && (pagination.has_next || pagination.has_prev)" 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 <Button
variant="outline" variant="outline"
@ -360,7 +352,12 @@ onMounted(() => {
<ChevronRight class="h-4 w-4 ml-1" /> <ChevronRight class="h-4 w-4 ml-1" />
</Button> </Button>
</div> </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> </div>
</template> </template>