This content originally appeared on DEV Community and was authored by Nebula
import { useState, useEffect, useCallback, useMemo } from "react";
import { Search, Filter, X, RefreshCw, AlertCircle, TrendingUp, TrendingDown, Calendar, DollarSign, PieChart, BarChart3, Download, Eye, EyeOff } from "lucide-react";
import styled from 'styled-components';
import { motion, AnimatePresence } from 'framer-motion';
import { LineChart, Line, AreaChart, Area, BarChart, Bar, PieChart as RechartsPieChart, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
// Design tokens (from your original file)
const colors = {
primary: '#667eea',
primaryDark: '#5a67d8',
secondary: '#764ba2',
success: '#48bb78',
error: '#ff6b6b',
warning: '#f6ad55',
text: '#1a202c',
textSecondary: '#4a5568',
textMuted: '#a0aec0',
background: 'rgba(255, 255, 255, 0.95)',
border: 'rgba(102, 126, 234, 0.2)',
borderHover: '#667eea',
glass: 'rgba(255, 255, 255, 0.95)',
glassLight: 'rgba(255, 255, 255, 0.8)',
shadow: 'rgba(0, 0, 0, 0.1)',
shadowHover: 'rgba(102, 126, 234, 0.1)'
};
const gradients = {
primary: `linear-gradient(135deg, ${colors.primary}, ${colors.secondary})`,
success: `linear-gradient(135deg, ${colors.success}, #38b2ac)`,
error: `linear-gradient(135deg, ${colors.error}, #ee5a52)`,
warning: `linear-gradient(135deg, ${colors.warning}, #ff8c00)`
};
const spacing = {
xs: '4px',
sm: '8px',
md: '12px',
lg: '16px',
xl: '20px',
xxl: '24px',
xxxl: '32px'
};
// Styled Components
const Container = styled(motion.div)`
padding: ${spacing.xxxl};
max-width: 1400px;
margin: 0 auto;
min-height: calc(100vh - 120px);
h1 {
font-size: 36px;
font-weight: 800;
color: ${colors.text};
margin-bottom: ${spacing.sm};
background: ${gradients.primary};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
}
.subtitle {
text-align: center;
color: ${colors.textMuted};
margin-bottom: ${spacing.xxxl};
font-size: 16px;
}
@media (max-width: 768px) {
padding: ${spacing.xl};
h1 {
font-size: 28px;
margin-bottom: ${spacing.xs};
}
}
`;
const StatsGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: ${spacing.xl};
margin-bottom: ${spacing.xxxl};
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: ${spacing.lg};
}
`;
const StatCard = styled(motion.div)`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
padding: ${spacing.xxl};
box-shadow: 0 8px 32px ${colors.shadow};
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: ${props => props.color || gradients.primary};
}
.stat-header {
display: flex;
align-items: center;
gap: ${spacing.md};
margin-bottom: ${spacing.lg};
svg {
color: ${props => props.iconColor || colors.primary};
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: ${colors.textSecondary};
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
.stat-value {
font-size: 32px;
font-weight: 800;
color: ${colors.text};
margin-bottom: ${spacing.sm};
}
.stat-change {
display: flex;
align-items: center;
gap: ${spacing.xs};
font-size: 14px;
font-weight: 600;
&.positive {
color: ${colors.success};
}
&.negative {
color: ${colors.error};
}
}
`;
const FilterSection = styled(motion.div)`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
padding: ${spacing.xxl};
margin-bottom: ${spacing.xxxl};
box-shadow: 0 8px 32px ${colors.shadow};
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: ${gradients.primary};
}
.filter-header {
display: flex;
align-items: center;
gap: ${spacing.md};
margin-bottom: ${spacing.xl};
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: ${colors.text};
}
}
`;
const FiltersGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: ${spacing.lg};
margin-bottom: ${spacing.xl};
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
`;
const FilterSelect = styled.select`
padding: ${spacing.lg} ${spacing.xl};
border: 2px solid ${colors.border};
border-radius: 12px;
font-size: 16px;
font-weight: 500;
color: ${colors.text};
background: ${colors.glassLight};
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
&:focus {
outline: none;
border-color: ${colors.borderHover};
box-shadow: 0 0 0 4px ${colors.shadowHover};
background: ${colors.background};
}
`;
const DateInput = styled.input`
padding: ${spacing.lg} ${spacing.xl};
border: 2px solid ${colors.border};
border-radius: 12px;
font-size: 16px;
font-weight: 500;
color: ${colors.text};
background: ${colors.glassLight};
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:focus {
outline: none;
border-color: ${colors.borderHover};
box-shadow: 0 0 0 4px ${colors.shadowHover};
background: ${colors.background};
}
`;
const AmountRangeContainer = styled.div`
display: flex;
gap: ${spacing.md};
align-items: center;
input {
flex: 1;
padding: ${spacing.lg};
border: 2px solid ${colors.border};
border-radius: 12px;
font-size: 16px;
font-weight: 500;
color: ${colors.text};
background: ${colors.glassLight};
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:focus {
outline: none;
border-color: ${colors.borderHover};
box-shadow: 0 0 0 4px ${colors.shadowHover};
background: ${colors.background};
}
}
span {
color: ${colors.textMuted};
font-weight: 600;
}
`;
const ButtonGroup = styled.div`
display: flex;
gap: ${spacing.md};
justify-content: flex-end;
@media (max-width: 768px) {
justify-content: stretch;
}
`;
const Button = styled(motion.button)`
display: flex;
align-items: center;
gap: ${spacing.sm};
padding: ${spacing.md} ${spacing.xl};
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid transparent;
${props => props.variant === 'primary' ? `
background: ${gradients.primary};
color: white;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
}
` : `
background: transparent;
color: ${colors.textSecondary};
border-color: #e2e8f0;
&:hover:not(:disabled) {
background: #f7fafc;
border-color: #cbd5e0;
}
`}
&:active {
transform: translateY(0);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
@media (max-width: 768px) {
flex: 1;
justify-content: center;
}
`;
const ChartsGrid = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
gap: ${spacing.xl};
margin-bottom: ${spacing.xxxl};
@media (max-width: 1024px) {
grid-template-columns: 1fr;
}
`;
const ChartContainer = styled(motion.div)`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
padding: ${spacing.xxl};
box-shadow: 0 8px 32px ${colors.shadow};
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
grid-column: ${props => props.fullWidth ? 'span 2' : 'span 1'};
@media (max-width: 1024px) {
grid-column: span 1;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: ${gradients.primary};
}
.chart-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: ${spacing.xl};
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: ${colors.text};
}
.chart-controls {
display: flex;
gap: ${spacing.sm};
}
}
.chart-wrapper {
height: 300px;
}
`;
const ToggleButton = styled(motion.button)`
padding: ${spacing.sm} ${spacing.md};
border: 2px solid ${colors.border};
border-radius: 8px;
background: ${props => props.active ? gradients.primary : 'transparent'};
color: ${props => props.active ? 'white' : colors.textSecondary};
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: ${colors.borderHover};
}
`;
const LoadingSpinner = styled(motion.div)`
display: flex;
align-items: center;
justify-content: center;
padding: 64px;
color: ${colors.primary};
svg {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`;
// Mock data generator
const generateMockTransactions = (count = 100) => {
const types = ['DEPOSIT', 'WITHDRAW', 'TRANSFER', 'LOAN'];
const statuses = ['SUCCESS', 'PENDING', 'FAILED'];
const merchants = ['Amazon', 'Starbucks', 'Uber', 'Netflix', 'Spotify', 'Apple', 'Google', 'Microsoft', 'Target', 'Walmart'];
const categories = ['Food & Dining', 'Shopping', 'Transportation', 'Entertainment', 'Bills & Utilities', 'Healthcare', 'Travel', 'Education'];
return Array.from({ length: count }, (_, index) => {
const type = types[Math.floor(Math.random() * types.length)];
const amount = type === 'DEPOSIT'
? Math.floor(Math.random() * 5000) + 100
: -(Math.floor(Math.random() * 1000) + 10);
return {
id: `txn_${index + 1}`,
type,
amount,
status: statuses[Math.floor(Math.random() * statuses.length)],
date: new Date(Date.now() - Math.floor(Math.random() * 90) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
merchant: merchants[Math.floor(Math.random() * merchants.length)],
category: categories[Math.floor(Math.random() * categories.length)],
description: `Transaction with ${merchants[Math.floor(Math.random() * merchants.length)]}`
};
});
};
// Custom tooltip component
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div style={{
background: colors.glass,
backdropFilter: 'blur(20px)',
border: `1px solid ${colors.border}`,
borderRadius: '12px',
padding: spacing.md,
boxShadow: `0 8px 32px ${colors.shadow}`
}}>
<p style={{ margin: 0, color: colors.text, fontWeight: '600' }}>{label}</p>
{payload.map((entry, index) => (
<p key={index} style={{ margin: '4px 0', color: entry.color, fontSize: '14px' }}>
{`${entry.name}: $${Math.abs(entry.value).toLocaleString()}`}
</p>
))}
</div>
);
}
return null;
};
const BankingAnalyticsDashboard = () => {
const [transactions, setTransactions] = useState([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
type: 'ALL',
status: 'ALL',
category: 'ALL',
dateFrom: '',
dateTo: '',
amountMin: '',
amountMax: ''
});
const [chartView, setChartView] = useState({
timeChart: 'line',
showBalance: true
});
// Simulate API call
useEffect(() => {
const fetchTransactions = async () => {
setLoading(true);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
setTransactions(generateMockTransactions(150));
setLoading(false);
};
fetchTransactions();
}, []);
// Filter transactions
const filteredTransactions = useMemo(() => {
return transactions.filter(transaction => {
if (filters.type !== 'ALL' && transaction.type !== filters.type) return false;
if (filters.status !== 'ALL' && transaction.status !== filters.status) return false;
if (filters.category !== 'ALL' && transaction.category !== filters.category) return false;
if (filters.dateFrom && transaction.date < filters.dateFrom) return false;
if (filters.dateTo && transaction.date > filters.dateTo) return false;
if (filters.amountMin && Math.abs(transaction.amount) < parseFloat(filters.amountMin)) return false;
if (filters.amountMax && Math.abs(transaction.amount) > parseFloat(filters.amountMax)) return false;
return true;
});
}, [transactions, filters]);
// Calculate statistics
const stats = useMemo(() => {
const totalIncome = filteredTransactions
.filter(t => t.amount > 0)
.reduce((sum, t) => sum + t.amount, 0);
const totalExpenses = filteredTransactions
.filter(t => t.amount < 0)
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
const netBalance = totalIncome - totalExpenses;
const avgTransaction = filteredTransactions.length > 0
? filteredTransactions.reduce((sum, t) => sum + Math.abs(t.amount), 0) / filteredTransactions.length
: 0;
// Calculate month-over-month changes (mock data)
const incomeChange = Math.random() * 20 - 10; // -10% to +10%
const expenseChange = Math.random() * 20 - 10;
const balanceChange = Math.random() * 30 - 15;
return {
totalIncome,
totalExpenses,
netBalance,
avgTransaction,
transactionCount: filteredTransactions.length,
incomeChange,
expenseChange,
balanceChange
};
}, [filteredTransactions]);
// Prepare chart data
const timeSeriesData = useMemo(() => {
const dataMap = {};
let runningBalance = 0;
filteredTransactions
.sort((a, b) => new Date(a.date) - new Date(b.date))
.forEach(transaction => {
const date = transaction.date;
if (!dataMap[date]) {
dataMap[date] = { date, income: 0, expenses: 0, balance: runningBalance };
}
if (transaction.amount > 0) {
dataMap[date].income += transaction.amount;
} else {
dataMap[date].expenses += Math.abs(transaction.amount);
}
runningBalance += transaction.amount;
dataMap[date].balance = runningBalance;
});
return Object.values(dataMap);
}, [filteredTransactions]);
const categoryData = useMemo(() => {
const categoryMap = {};
filteredTransactions.forEach(transaction => {
if (transaction.amount < 0) { // Only expenses
const category = transaction.category;
categoryMap[category] = (categoryMap[category] || 0) + Math.abs(transaction.amount);
}
});
return Object.entries(categoryMap).map(([name, value]) => ({ name, value }));
}, [filteredTransactions]);
const typeData = useMemo(() => {
const typeMap = {};
filteredTransactions.forEach(transaction => {
const type = transaction.type;
typeMap[type] = (typeMap[type] || 0) + Math.abs(transaction.amount);
});
return Object.entries(typeMap).map(([name, value]) => ({ name, value }));
}, [filteredTransactions]);
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const resetFilters = () => {
setFilters({
type: 'ALL',
status: 'ALL',
category: 'ALL',
dateFrom: '',
dateTo: '',
amountMin: '',
amountMax: ''
});
};
const refreshData = () => {
setTransactions(generateMockTransactions(150));
};
const exportData = () => {
// Mock export functionality
const dataStr = JSON.stringify(filteredTransactions, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'transaction-analytics.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
};
const pieColors = [colors.primary, colors.secondary, colors.success, colors.warning, colors.error, '#9f7aea', '#38b2ac', '#ed8936'];
if (loading) {
return (
<Container>
<LoadingSpinner
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<RefreshCw size={32} />
<span style={{ marginLeft: spacing.md, fontSize: '18px', fontWeight: '600' }}>
Loading Analytics...
</span>
</LoadingSpinner>
</Container>
);
}
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1>Transaction Analytics</h1>
<p className="subtitle">Comprehensive insights into your financial activities</p>
{/* Statistics Cards */}
<StatsGrid>
<StatCard
color={gradients.success}
iconColor={colors.success}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="stat-header">
<TrendingUp size={20} />
<h3>Total Income</h3>
</div>
<div className="stat-value">${stats.totalIncome.toLocaleString()}</div>
<div className={`stat-change ${stats.incomeChange >= 0 ? 'positive' : 'negative'}`}>
{stats.incomeChange >= 0 ? <TrendingUp size={16} /> : <TrendingDown size={16} />}
{Math.abs(stats.incomeChange).toFixed(1)}% from last month
</div>
</StatCard>
<StatCard
color={gradients.error}
iconColor={colors.error}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="stat-header">
<TrendingDown size={20} />
<h3>Total Expenses</h3>
</div>
<div className="stat-value">${stats.totalExpenses.toLocaleString()}</div>
<div className={`stat-change ${stats.expenseChange <= 0 ? 'positive' : 'negative'}`}>
{stats.expenseChange <= 0 ? <TrendingDown size={16} /> : <TrendingUp size={16} />}
{Math.abs(stats.expenseChange).toFixed(1)}% from last month
</div>
</StatCard>
<StatCard
color={stats.netBalance >= 0 ? gradients.success : gradients.error}
iconColor={stats.netBalance >= 0 ? colors.success : colors.error}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="stat-header">
<DollarSign size={20} />
<h3>Net Balance</h3>
</div>
<div className="stat-value">${stats.netBalance.toLocaleString()}</div>
<div className={`stat-change ${stats.balanceChange >= 0 ? 'positive' : 'negative'}`}>
{stats.balanceChange >= 0 ? <TrendingUp size={16} /> : <TrendingDown size={16} />}
{Math.abs(stats.balanceChange).toFixed(1)}% from last month
</div>
</StatCard>
<StatCard
color={gradients.primary}
iconColor={colors.primary}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<div className="stat-header">
<BarChart3 size={20} />
<h3>Avg Transaction</h3>
</div>
<div className="stat-value">${stats.avgTransaction.toLocaleString()}</div>
<div className="stat-change positive">
<Calendar size={16} />
{stats.transactionCount} transactions
</div>
</StatCard>
</StatsGrid>
{/* Filters */}
<FilterSection
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<div className="filter-header">
<Filter size={20} />
<h3>Filters & Controls</h3>
</div>
<FiltersGrid>
<FilterSelect
value={filters.type}
onChange={(e) => handleFilterChange('type', e.target.value)}
>
<option value="ALL">All Types</option>
<option value="DEPOSIT">Deposits</option>
<option value="WITHDRAW">Withdrawals</option>
<option value="TRANSFER">Transfers</option>
<option value="LOAN">Loans</option>
</FilterSelect>
<FilterSelect
value={filters.status}
onChange={(e) => handleFilterChange('status', e.target.value)}
>
<option value="ALL">All Status</option>
<option value="SUCCESS">Success</option>
<option value="PENDING">Pending</option>
<option value="FAILED">Failed</option>
</FilterSelect>
<FilterSelect
value={filters.category}
onChange={(e) => handleFilterChange('category', e.target.value)}
>
<option value="ALL">All Categories</option>
<option value="Food & Dining">Food & Dining</option>
<option value="Shopping">Shopping</option>
<option value="Transportation">Transportation</option>
<option value="Entertainment">Entertainment</option>
<option value="Bills & Utilities">Bills & Utilities</option>
<option value="Healthcare">Healthcare</option>
<option value="Travel">Travel</option>
<option value="Education">Education</option>
</FilterSelect>
<DateInput
type="date"
placeholder="From Date"
value={filters.dateFrom}
onChange={(e) => handleFilterChange('dateFrom', e.target.value)}
/>
<DateInput
type="date"
placeholder="To Date"
value={filters.dateTo}
onChange={(e) => handleFilterChange('dateTo', e.target.value)}
/>
<AmountRangeContainer>
<input
type="number"
placeholder="Min Amount"
value={filters.amountMin}
onChange={(e) => handleFilterChange('amountMin', e.target.value)}
/>
<span>-</span>
<input
type="number"
placeholder="Max Amount"
value={filters.amountMax}
onChange={(e) => handleFilterChange('amountMax', e.target.value)}
/>
</AmountRangeContainer>
</FiltersGrid>
<ButtonGroup>
<Button onClick={resetFilters}>
<X size={16} />
Clear Filters
</Button>
<Button onClick={refreshData}>
<RefreshCw size={16} />
Refresh Data
</Button>
<Button variant="primary" onClick={exportData}>
<Download size={16} />
Export Data
</Button>
</ButtonGroup>
</FilterSection>
{/* Charts */}
<ChartsGrid>
{/* Time Series Chart */}
<ChartContainer
fullWidth
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
>
<div className="chart-header">
<h3>Financial Flow Over Time</h3>
<div className="chart-controls">
<ToggleButton
active={chartView.timeChart === 'line'}
onClick={() => setChartView(prev => ({...prev, timeChart: 'line'}))}
>
Line
</ToggleButton>
<ToggleButton
active={chartView.timeChart === 'area'}
onClick={() => setChartView(prev => ({...prev, timeChart: 'area'}))}
>
Area
</ToggleButton>
<ToggleButton
active={chartView.showBalance}
onClick={() => setChartView(prev => ({...prev, showBalance: !prev.showBalance}))}
>
{chartView.showBalance ? <Eye size={14} /> : <EyeOff size={14} />}
Balance
</ToggleButton>
</div>
</div>
<div className="chart-wrapper">
<ResponsiveContainer width="100%" height="100%">
{chartView.timeChart === 'line' ? (
<LineChart data={timeSeriesData}>
<CartesianGrid strokeDasharray="3 3" stroke={colors.border} />
<XAxis dataKey="date" stroke={colors.textMuted} />
<YAxis stroke={colors.textMuted} />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Line
type="monotone"
dataKey="income"
stroke={colors.success}
strokeWidth={3}
dot={{ fill: colors.success, strokeWidth: 2, r: 4 }}
name="Income"
/>
<Line
type="monotone"
dataKey="expenses"
stroke={colors.error}
strokeWidth={3}
dot={{ fill: colors.error, strokeWidth: 2, r: 4 }}
name="Expenses"
/>
{chartView.showBalance && (
<Line
type="monotone"
dataKey="balance"
stroke={colors.primary}
strokeWidth={3}
dot={{ fill: colors.primary, strokeWidth: 2, r: 4 }}
name="Running Balance"
/>
)}
</LineChart>
) : (
<AreaChart data={timeSeriesData}>
<CartesianGrid strokeDasharray="3 3" stroke={colors.border} />
<XAxis dataKey="date" stroke={colors.textMuted} />
<YAxis stroke={colors.textMuted} />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Area
type="monotone"
dataKey="income"
stackId="1"
stroke={colors.success}
fill={colors.success}
fillOpacity={0.6}
name="Income"
/>
<Area
type="monotone"
dataKey="expenses"
stackId="2"
stroke={colors.error}
fill={colors.error}
fillOpacity={0.6}
name="Expenses"
/>
{chartView.showBalance && (
<Area
type="monotone"
dataKey="balance"
stroke={colors.primary}
fill={colors.primary}
fillOpacity={0.3}
name="Running Balance"
/>
)}
</AreaChart>
)}
</ResponsiveContainer>
</div>
</ChartContainer>
{/* Category Breakdown */}
<ChartContainer
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
>
<div className="chart-header">
<h3>Expenses by Category</h3>
</div>
<div className="chart-wrapper">
<ResponsiveContainer width="100%" height="100%">
<RechartsPieChart>
<Pie
data={categoryData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
dataKey="value"
>
{categoryData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={pieColors[index % pieColors.length]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Legend />
</RechartsPieChart>
</ResponsiveContainer>
</div>
</ChartContainer>
{/* Transaction Types */}
<ChartContainer
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
>
<div className="chart-header">
<h3>Transaction Types</h3>
</div>
<div className="chart-wrapper">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={typeData}>
<CartesianGrid strokeDasharray="3 3" stroke={colors.border} />
<XAxis dataKey="name" stroke={colors.textMuted} />
<YAxis stroke={colors.textMuted} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="value" fill={colors.primary} radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</ChartContainer>
</ChartsGrid>
{/* Recent Transactions Table */}
<ChartContainer
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.9 }}
>
<div className="chart-header">
<h3>Recent Transactions</h3>
<div className="chart-controls">
<span style={{ fontSize: '14px', color: colors.textMuted }}>
Showing {Math.min(10, filteredTransactions.length)} of {filteredTransactions.length} transactions
</span>
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'separate', borderSpacing: 0 }}>
<thead>
<tr style={{ background: gradients.primary }}>
<th style={{ padding: spacing.lg, color: 'white', fontWeight: '600', fontSize: '14px', textAlign: 'left' }}>Date</th>
<th style={{ padding: spacing.lg, color: 'white', fontWeight: '600', fontSize: '14px', textAlign: 'left' }}>Description</th>
<th style={{ padding: spacing.lg, color: 'white', fontWeight: '600', fontSize: '14px', textAlign: 'left' }}>Category</th>
<th style={{ padding: spacing.lg, color: 'white', fontWeight: '600', fontSize: '14px', textAlign: 'left' }}>Type</th>
<th style={{ padding: spacing.lg, color: 'white', fontWeight: '600', fontSize: '14px', textAlign: 'left' }}>Status</th>
<th style={{ padding: spacing.lg, color: 'white', fontWeight: '600', fontSize: '14px', textAlign: 'right' }}>Amount</th>
</tr>
</thead>
<tbody>
{filteredTransactions.slice(0, 10).map((transaction, index) => (
<tr
key={transaction.id}
style={{
background: index % 2 === 0 ? 'rgba(248, 250, 252, 0.5)' : 'transparent',
borderBottom: '1px solid rgba(226, 232, 240, 0.5)'
}}
>
<td style={{ padding: spacing.lg, color: colors.textSecondary, fontWeight: '500' }}>
{new Date(transaction.date).toLocaleDateString()}
</td>
<td style={{ padding: spacing.lg, color: colors.text, fontWeight: '600' }}>
{transaction.description}
</td>
<td style={{ padding: spacing.lg, color: colors.textSecondary }}>
{transaction.category}
</td>
<td style={{ padding: spacing.lg }}>
<TypeBadge type={transaction.type}>
{transaction.type}
</TypeBadge>
</td>
<td style={{ padding: spacing.lg }}>
<StatusBadge status={transaction.status}>
{transaction.status}
</StatusBadge>
</td>
<td style={{
padding: spacing.lg,
textAlign: 'right',
fontWeight: '700',
color: transaction.amount > 0 ? colors.success : colors.error
}}>
{transaction.amount > 0 ? '+' : ''}${transaction.amount.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</ChartContainer>
</Container>
);
};
// Styled badge components
const StatusBadge = styled.span`
padding: 6px ${spacing.md};
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
${props => props.status === 'SUCCESS' && `
background: linear-gradient(135deg, rgba(72, 187, 120, 0.2), rgba(56, 178, 172, 0.2));
color: ${colors.success};
border: 1px solid rgba(72, 187, 120, 0.3);
`}
${props => props.status === 'PENDING' && `
background: linear-gradient(135deg, rgba(255, 193, 7, 0.2), rgba(255, 152, 0, 0.2));
color: ${colors.warning};
border: 1px solid rgba(255, 193, 7, 0.3);
`}
${props => props.status === 'FAILED' && `
background: linear-gradient(135deg, rgba(255, 107, 107, 0.2), rgba(238, 90, 82, 0.2));
color: ${colors.error};
border: 1px solid rgba(255, 107, 107, 0.3);
`}
`;
const TypeBadge = styled.span`
padding: 6px ${spacing.md};
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
${props => props.type === 'DEPOSIT' && `
background: linear-gradient(135deg, rgba(72, 187, 120, 0.2), rgba(56, 178, 172, 0.2));
color: ${colors.success};
border: 1px solid rgba(72, 187, 120, 0.3);
`}
${props => props.type === 'WITHDRAW' && `
background: linear-gradient(135deg, rgba(255, 107, 107, 0.2), rgba(238, 90, 82, 0.2));
color: ${colors.error};
border: 1px solid rgba(255, 107, 107, 0.3);
`}
${props => props.type === 'TRANSFER' && `
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2));
color: ${colors.primary};
border: 1px solid rgba(102, 126, 234, 0.3);
`}
${props => props.type === 'LOAN' && `
background: linear-gradient(135deg, rgba(255, 193, 7, 0.2), rgba(255, 152, 0, 0.2));
color: ${colors.warning};
border: 1px solid rgba(255, 193, 7, 0.3);
`}
`;
export default BankingAnalyticsDashboard
This content originally appeared on DEV Community and was authored by Nebula