Skip to content
Merged
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
266 changes: 266 additions & 0 deletions components/dashboard/export-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
'use client'

import { useState, useMemo } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import { Download, FileText, FileSpreadsheet, FileJson } from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import { exportToCSV, exportToJSON, exportToPDF, exportToExcel } from '@/lib/export-utils'

interface ExportModalProps {
open: boolean
onOpenChange: (open: boolean) => void
transactions?: any[]
}

type TimePeriod = 'last30' | 'last90' | 'custom'
type ExportFormat = 'csv' | 'pdf' | 'excel' | 'json'

export function ExportModal({ open, onOpenChange, transactions = [] }: ExportModalProps) {
const [timePeriod, setTimePeriod] = useState<TimePeriod>('last30')
const [format, setFormat] = useState<ExportFormat>('csv')
const [emailReport, setEmailReport] = useState(false)
const [exporting, setExporting] = useState(false)

const now = new Date()
const maxDaysAgo = 365
const [customDaysAgo, setCustomDaysAgo] = useState([30])

const { startDate, endDate, recordCount } = useMemo(() => {
let daysAgo: number

if (timePeriod === 'last30') {
daysAgo = 30
} else if (timePeriod === 'last90') {
daysAgo = 90
} else {
daysAgo = customDaysAgo[0]
}

const end = new Date()
const start = new Date()
start.setDate(start.getDate() - daysAgo)

const count = transactions.filter(tx => {
const txDate = new Date(tx.timestamp)
return txDate >= start && txDate <= end
}).length

return {
startDate: start,
endDate: end,
recordCount: count
}
}, [timePeriod, customDaysAgo, transactions])

const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}

const handleExport = async () => {
setExporting(true)

try {
const filteredTransactions = transactions.filter(tx => {
const txDate = new Date(tx.timestamp)
return txDate >= startDate && txDate <= endDate
})

const filename = `aframp-transactions-${formatDate(startDate).replace(/\s/g, '-')}-to-${formatDate(endDate).replace(/\s/g, '-')}`

switch (format) {
case 'csv':
exportToCSV(filteredTransactions, `${filename}.csv`)
break
case 'json':
exportToJSON(filteredTransactions, `${filename}.json`)
break
case 'pdf':
exportToPDF(filteredTransactions, `${filename}.pdf`)
break
case 'excel':
exportToExcel(filteredTransactions, `${filename}.xlsx`)
break
}

await new Promise(resolve => setTimeout(resolve, 800))

toast.success('Export successful!', {
description: emailReport
? `${recordCount} records exported and sent to your email`
: `${recordCount} records exported as ${format.toUpperCase()}`
})

onOpenChange(false)
} catch (error) {
toast.error('Export failed', {
description: 'Please try again or contact support'
})
} finally {
setExporting(false)
}
}

const formats: { value: ExportFormat; label: string; icon: any }[] = [
{ value: 'csv', label: 'CSV', icon: FileSpreadsheet },
{ value: 'pdf', label: 'PDF', icon: FileText },
{ value: 'excel', label: 'Excel', icon: FileSpreadsheet },
{ value: 'json', label: 'JSON', icon: FileJson },
]

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Download className="w-5 h-5" />
Export Transaction Data
</DialogTitle>
<DialogDescription>
Select time period and format for your transaction export
</DialogDescription>
</DialogHeader>

<div className="space-y-6 py-4">
{/* Time Period */}
<div className="space-y-3">
<Label>Time Period</Label>
<div className="grid grid-cols-3 gap-2">
<Button
type="button"
variant={timePeriod === 'last30' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('last30')}
>
Last 30 days
</Button>
<Button
type="button"
variant={timePeriod === 'last90' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('last90')}
>
Last 3 months
</Button>
<Button
type="button"
variant={timePeriod === 'custom' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('custom')}
>
Custom
</Button>
</div>
</div>

{/* Custom Date Range Slider */}
{timePeriod === 'custom' && (
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Days ago</span>
<span className="font-medium">{customDaysAgo[0]} days</span>
</div>
<Slider
value={customDaysAgo}
onValueChange={setCustomDaysAgo}
min={1}
max={maxDaysAgo}
step={1}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Today</span>
<span>1 year ago</span>
</div>
</div>
)}

{/* Date Range Display */}
<div className="rounded-lg bg-muted/50 p-3 space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Date range:</span>
<span className="font-medium">
{formatDate(startDate)} – {formatDate(endDate)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Records:</span>
<span className="font-semibold text-primary">~{recordCount}</span>
</div>
</div>

{/* Export Format */}
<div className="space-y-3">
<Label>Export Format</Label>
<div className="grid grid-cols-2 gap-2">
{formats.map(({ value, label, icon: Icon }) => (
<button
key={value}
type="button"
onClick={() => setFormat(value)}
className={cn(
'flex items-center gap-2 p-3 rounded-lg border-2 transition-all',
format === value
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
)}
>
<Icon className="w-4 h-4" />
<span className="font-medium text-sm">{label}</span>
</button>
))}
</div>
</div>

{/* Email Report Toggle */}
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label htmlFor="email-report" className="cursor-pointer">
Email report
</Label>
<p className="text-xs text-muted-foreground">
Send export to your registered email
</p>
</div>
<Switch
id="email-report"
checked={emailReport}
onCheckedChange={setEmailReport}
/>
</div>

{/* Export Button */}
<Button
onClick={handleExport}
disabled={exporting || recordCount === 0}
className="w-full"
size="lg"
>
{exporting ? (
'Exporting...'
) : (
<>
<Download className="w-4 h-4 mr-2" />
Export {recordCount} Records
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
)
}
37 changes: 29 additions & 8 deletions components/dashboard/transaction-history.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
'use client'

import { useState } from 'react'
import { motion } from 'framer-motion'
import { ArrowUp, ArrowDown, ArrowLeftRight, Clock, CheckCircle2, XCircle } from 'lucide-react'
import { ArrowUp, ArrowDown, ArrowLeftRight, Clock, CheckCircle2, XCircle, Download } from 'lucide-react'
import { cn } from '@/lib/utils'
import { ExportModal } from './export-modal'

interface Transaction {
id: string
Expand Down Expand Up @@ -54,6 +56,8 @@ const mockTransactions: Transaction[] = [
]

export function TransactionHistory() {
const [exportModalOpen, setExportModalOpen] = useState(false)

const getIcon = (type: Transaction['type']) => {
switch (type) {
case 'send':
Expand All @@ -77,13 +81,23 @@ export function TransactionHistory() {
}

return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-card rounded-2xl p-6 border border-border shadow-sm"
>
<h3 className="text-lg font-semibold text-foreground mb-4">Recent Transactions</h3>
<div className="space-y-3">
<>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-card rounded-2xl p-6 border border-border shadow-sm"
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">Recent Transactions</h3>
<button
onClick={() => setExportModalOpen(true)}
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-primary hover:text-primary/80 hover:bg-primary/10 rounded-lg transition-colors"
>
<Download className="w-4 h-4" />
Export
</button>
</div>
<div className="space-y-3">
{mockTransactions.map((tx, index) => (
<motion.div
key={tx.id}
Expand Down Expand Up @@ -132,5 +146,12 @@ export function TransactionHistory() {
View All Transactions
</button>
</motion.div>

<ExportModal
open={exportModalOpen}
onOpenChange={setExportModalOpen}
transactions={mockTransactions}
/>
</>
)
}
27 changes: 27 additions & 0 deletions components/ui/slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'

import * as React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'
import { cn } from '@/lib/utils'

const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full touch-none select-none items-center',
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName

export { Slider }
28 changes: 28 additions & 0 deletions components/ui/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import * as React from 'react'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import { cn } from '@/lib/utils'

const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName

export { Switch }
Loading
Loading