Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ build
requirements.md
.vercel
.env*.local
temp_auto_push.bat
temp_interactive_push.bat
19 changes: 19 additions & 0 deletions app/dashboard/transactions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Suspense } from 'react'
import { TransactionsPageClient } from '@/components/dashboard/transactions-page-client'

export default function DashboardTransactionsPage() {
return (
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4" />
<p className="text-muted-foreground">Loading transactions...</p>
</div>
</div>
}
>
<TransactionsPageClient />
</Suspense>
)
}
10 changes: 9 additions & 1 deletion components/bills/bills-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CategoryGrid } from '@/components/bills/category-grid'
import { RecentBillers } from '@/components/bills/recent-billers'
import { ScheduledPayments } from '@/components/bills/scheduled-payments'
import { TransactionStats } from '@/components/bills/transaction-stats'
import { RecentPayments } from '@/components/bills/recent-payments'
import { useBillsData } from '@/hooks/use-bills-data'

export function BillsPageClient() {
Expand Down Expand Up @@ -139,6 +140,8 @@ export function BillsPageClient() {
<TransactionStats transactions={transactions} loading={loading} />
</div>

<RecentPayments transactions={transactions} loading={loading} />

<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
{/* Category Grid */}
Expand All @@ -149,7 +152,12 @@ export function BillsPageClient() {
/>

{/* Recent Billers */}
<RecentBillers billers={recentBillers} searchQuery={debouncedSearch} loading={loading} />
<RecentBillers
billers={recentBillers}
searchQuery={debouncedSearch}
loading={loading}
onClearSearch={() => setSearchQuery('')}
/>
</div>

<div className="space-y-8">
Expand Down
22 changes: 11 additions & 11 deletions components/bills/category-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
import { CategoryIcon } from '@/components/bills/biller-icons'
import { EmptyState } from '@/components/ui/empty-state'
import { ArrowRight } from 'lucide-react'

interface BillCategory {
Expand Down Expand Up @@ -40,17 +41,16 @@ export function CategoryGrid({ categories, searchQuery, selectedCountry }: Categ

if (filteredCategories.length === 0 && searchQuery) {
return (
<div className="text-center py-16">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-white/5 mb-4">
<svg className="w-8 h-8 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div className="text-lg font-medium text-foreground">No categories found</div>
<div className="text-muted-foreground mt-1">
We couldn&apos;t find any categories matching &quot;{searchQuery}&quot;
</div>
</div>
<section className="space-y-6">
<h2 className="text-2xl font-bold font-cal-sans tracking-tight">Categories</h2>
<EmptyState
variant="search"
title="No categories found"
description={`Nothing matched "${searchQuery}". Try a different search term.`}
bordered={false}
className="py-8"
/>
</section>
)
}

Expand Down
36 changes: 19 additions & 17 deletions components/bills/category-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { useState, useMemo } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import Link from 'next/link'
import { ArrowLeft, Search, Filter, AlertCircle } from 'lucide-react'
import { ArrowLeft, Search, Filter } from 'lucide-react'
import { EmptyState } from '@/components/ui/empty-state'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
Expand Down Expand Up @@ -121,22 +122,23 @@ export function CategoryPageClient({ categorySlug }: CategoryPageClientProps) {
))}
</motion.div>
) : (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center justify-center py-24 border-2 border-dashed border-border rounded-3xl bg-muted/20"
>
<div className="h-16 w-16 bg-muted rounded-full flex items-center justify-center mb-4">
<AlertCircle className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-xl font-bold">No billers found</h3>
<p className="text-muted-foreground mb-6 text-center max-w-xs">
We couldn&apos;t find any {category?.name.toLowerCase()} providers matching &quot;
{searchQuery}&quot; in this country.
</p>
<Button variant="secondary" onClick={() => setSearchQuery('')}>
Clear Search Query
</Button>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<EmptyState
variant={searchQuery ? 'search' : 'bills'}
title="No billers found"
description={
searchQuery
? `We couldn't find any ${category?.name.toLowerCase() ?? 'category'} providers matching "${searchQuery}".`
: `No ${category?.name.toLowerCase() ?? 'category'} providers are available in this country yet.`
}
action={
searchQuery
? { label: 'Clear search', onClick: () => setSearchQuery('') }
: { label: 'Back to bills', href: '/bills', variant: 'outline' }
}
bordered={false}
className="py-16 border-2 border-dashed border-border rounded-3xl bg-muted/20"
/>
</motion.div>
)}
</AnimatePresence>
Expand Down
37 changes: 35 additions & 2 deletions components/bills/recent-billers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import Link from 'next/link'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Clock, Star, ArrowRight } from 'lucide-react'

Check failure on line 8 in components/bills/recent-billers.tsx

View workflow job for this annotation

GitHub Actions / Code Quality

'Clock' is defined but never used
import { BillerIcon } from '@/components/bills/biller-icons'
import { EmptyState } from '@/components/ui/empty-state'
import { cn } from '@/lib/utils'

Check failure on line 11 in components/bills/recent-billers.tsx

View workflow job for this annotation

GitHub Actions / Code Quality

'cn' is defined but never used

interface Biller {
id: string
Expand All @@ -22,9 +23,10 @@
billers: Biller[]
searchQuery: string
loading: boolean
onClearSearch?: () => void
}

export function RecentBillers({ billers, searchQuery, loading }: RecentBillersProps) {
export function RecentBillers({ billers, searchQuery, loading, onClearSearch }: RecentBillersProps) {
const [recentBillers, setRecentBillers] = useState<Biller[]>([])

useEffect(() => {
Expand Down Expand Up @@ -66,7 +68,38 @@
}

if (filteredBillers.length === 0 && searchQuery) {
return null // Handled by category grid
return (
<section className="space-y-6">
<h2 className="text-2xl font-bold font-cal-sans tracking-tight">Recent Billers</h2>
<EmptyState
variant="search"
title="No billers found"
description={`Nothing matched "${searchQuery}". Try another name or category.`}
action={
onClearSearch
? { label: 'Clear search', onClick: onClearSearch, variant: 'outline' }
: undefined
}
bordered={false}
className="py-8"
/>
</section>
)
}

if (billers.length === 0) {
return (
<section className="space-y-6">
<h2 className="text-2xl font-bold font-cal-sans tracking-tight">Recent Billers</h2>
<EmptyState
variant="bills"
title="No billers yet"
description="Pick a category below to pay your first bill — favorites will appear here."
bordered={false}
className="py-8"
/>
</section>
)
}

return (
Expand Down
107 changes: 107 additions & 0 deletions components/bills/recent-payments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use client'

import { motion } from 'framer-motion'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { EmptyState } from '@/components/ui/empty-state'
import { cn } from '@/lib/utils'
import type { BillsTransaction } from '@/hooks/use-bills-data'

interface RecentPaymentsProps {
transactions: BillsTransaction[]
loading: boolean
}

const statusStyles: Record<BillsTransaction['status'], string> = {
completed: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
pending: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
failed: 'bg-rose-500/10 text-rose-600 dark:text-rose-400',
}

export function RecentPayments({ transactions, loading }: RecentPaymentsProps) {
if (loading) {
return (
<section className="space-y-4">
<div className="h-6 w-40 bg-muted rounded animate-pulse" />
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<Card key={i} className="border-border">
<CardContent className="p-4">
<div className="h-4 w-48 bg-muted rounded animate-pulse mb-2" />
<div className="h-3 w-32 bg-muted rounded animate-pulse" />
</CardContent>
</Card>
))}
</div>
</section>
)
}

if (transactions.length === 0) {
return (
<section className="space-y-4">
<h2 className="text-xl font-semibold">Recent Bill Payments</h2>
<EmptyState
variant="bills"
title="No bill payments yet"
description="Pay electricity, airtime, TV, and more — your payments will show up here."
action={{ label: 'Browse bill categories', href: '/bills' }}
secondaryAction={{
label: 'Pay a bill',
href: '/bills/pay',
variant: 'outline',
}}
/>
</section>
)
}

return (
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Recent Bill Payments</h2>
<Badge variant="secondary" className="text-xs">
{transactions.length} payment{transactions.length === 1 ? '' : 's'}
</Badge>
</div>

<div className="space-y-3">
{transactions.map((tx, index) => (
<motion.div
key={tx.id}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Card className="border-border bg-card hover:border-primary/30 transition-colors">
<CardContent className="p-4 flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<p className="font-medium text-foreground truncate">{tx.biller}</p>
<p className="text-sm text-muted-foreground">{tx.accountLabel}</p>
<p className="text-xs text-muted-foreground mt-1">
{new Date(tx.createdAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</p>
</div>
<div className="flex items-center gap-3">
<span className="font-semibold text-foreground">
₦{tx.amount.toLocaleString()}
</span>
<Badge
variant="secondary"
className={cn('text-xs capitalize', statusStyles[tx.status])}
>
{tx.status}
</Badge>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</section>
)
}
9 changes: 6 additions & 3 deletions components/bills/scheduled-payments.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client'

import Link from 'next/link'
import { motion } from 'framer-motion'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Calendar, Pause, Play, MoreHorizontal, ArrowRight } from 'lucide-react'
import { cn } from '@/lib/utils'
import { EmptyStateIllustration } from '@/components/ui/empty-state-illustration'

interface ScheduledPayment {
id: string
Expand Down Expand Up @@ -65,8 +65,11 @@ export function ScheduledPayments({ payments, loading }: ScheduledPaymentsProps)
<p className="text-muted-foreground mb-8 max-w-[250px] mx-auto leading-relaxed">
Set up recurring payments to automate your bills and never miss a due date.
</p>
<Button className="rounded-full bg-primary text-primary-foreground hover:bg-primary/90 font-medium px-8 transition-all hover:shadow-[0_0_20px_-5px_rgba(34,197,94,0.4)]">
Schedule Payment
<Button
className="rounded-full bg-primary text-primary-foreground hover:bg-primary/90 font-medium px-8 transition-all hover:shadow-[0_0_20px_-5px_rgba(34,197,94,0.4)]"
asChild
>
<Link href="/bills/schedule">Schedule Payment</Link>
</Button>
</CardContent>
</Card>
Expand Down
4 changes: 4 additions & 0 deletions components/bills/transaction-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export function TransactionStats({ transactions, loading }: TransactionStatsProp
)
}

if (transactions.length === 0) {
return null
}

const totalSpent = transactions
.filter((t) => t.status === 'completed')
.reduce((sum, t) => sum + t.amount, 0)
Expand Down
Loading
Loading