import { useState, useEffect, useCallback } from "react";
import { Search, Filter, X, RefreshCw, AlertCircle } from "lucide-react";
import styled from 'styled-components';
import { motion, AnimatePresence } from 'framer-motion';
// Design tokens
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);
h2 {
font-size: 32px;
font-weight: 800;
color: ${colors.text};
margin-bottom: ${spacing.xxxl};
background: ${gradients.primary};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
}
@media (max-width: 768px) {
padding: ${spacing.xl};
h2 {
font-size: 28px;
margin-bottom: ${spacing.xxl};
}
}
`;
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};
}
`;
const SearchContainer = styled.div`
position: relative;
margin-bottom: ${spacing.xl};
svg {
position: absolute;
left: ${spacing.lg};
top: 50%;
transform: translateY(-50%);
color: ${colors.textMuted};
z-index: 1;
}
`;
const SearchInput = styled.input`
width: 100%;
padding: ${spacing.lg} ${spacing.lg} ${spacing.lg} 48px;
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};
}
&::placeholder {
color: ${colors.textMuted};
}
`;
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};
}
option {
background: white;
color: ${colors.text};
padding: ${spacing.md};
}
`;
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 TableContainer = styled.div`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 8px 32px ${colors.shadow};
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: ${gradients.primary};
}
`;
const Table = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
th {
background: ${gradients.primary};
color: white;
padding: ${spacing.xl} ${spacing.lg};
text-align: left;
font-weight: 600;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
border: none;
white-space: nowrap;
}
td {
padding: ${spacing.xl} ${spacing.lg};
color: ${colors.textSecondary};
font-weight: 500;
border-bottom: 1px solid rgba(226, 232, 240, 0.5);
vertical-align: middle;
}
tr:hover {
background: ${colors.shadowHover};
}
tr:nth-child(even) {
background: rgba(248, 250, 252, 0.5);
}
@media (max-width: 1024px) {
font-size: 14px;
th, td {
padding: ${spacing.md} ${spacing.sm};
}
}
@media (max-width: 768px) {
display: block;
overflow-x: auto;
white-space: nowrap;
}
`;
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);
`}
`;
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); }
}
`;
const ErrorMessage = styled(motion.div)`
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: ${spacing.lg};
padding: 64px ${spacing.xxxl};
text-align: center;
color: ${colors.error};
h3 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
p {
margin: 0;
color: ${colors.textSecondary};
line-height: 1.5;
}
`;
const EmptyState = styled(motion.div)`
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: ${spacing.lg};
padding: 64px ${spacing.xxxl};
text-align: center;
color: ${colors.textMuted};
h3 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
p {
margin: 0;
line-height: 1.5;
}
`;
// Mock useAuth hook for demonstration
const useAuth = () => ({
user: { id: 'user123' }
});
// Mock API function
const api = {
get: async (endpoint) => {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Mock data
return {
data: [
{
srNo: 1,
transactionId: "TX123456",
date: "2023-10-01",
type: "DEPOSIT",
amount: 1000,
fromAccount: "87452147890",
toAccount: "87452147891",
status: "SUCCESS",
description: "Deposit to savings account",
},
{
srNo: 2,
transactionId: "TX123457",
date: "2023-10-02",
type: "WITHDRAW",
amount: 500,
fromAccount: "87452147891",
toAccount: "",
status: "SUCCESS",
description: "ATM withdrawal",
},
{
srNo: 3,
transactionId: "TX123458",
date: "2023-10-03",
type: "TRANSFER",
amount: 750,
fromAccount: "87452147890",
toAccount: "87452147892",
status: "PENDING",
description: "Transfer to checking account",
},
{
srNo: 4,
transactionId: "TX123459",
date: "2023-10-04",
type: "LOAN",
amount: 5000,
fromAccount: "",
toAccount: "87452147890",
status: "FAILED",
description: "Personal loan disbursement",
},
]
};
}
};
function TransactionsPage() {
const [transactions, setTransactions] = useState([]);
const [filteredTransactions, setFilteredTransactions] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [accountFilter, setAccountFilter] = useState("");
const [statusFilter, setStatusFilter] = useState("");
const [typeFilter, setTypeFilter] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { user } = useAuth();
// Fetch transactions
const fetchTransactions = useCallback(async () => {
if (!user?.id) return;
try {
setLoading(true);
setError(null);
const response = await api.get(`/transactions/user/${user.id}`);
setTransactions(response.data);
setFilteredTransactions(response.data);
} catch (err) {
setError('Failed to load transactions. Please try again.');
console.error('Error fetching transactions:', err);
} finally {
setLoading(false);
}
}, [user?.id]);
// Initial load
useEffect(() => {
fetchTransactions();
}, [fetchTransactions]);
// Handle search
const handleSearch = useCallback((e) => {
const value = e.target.value;
setSearchTerm(value);
if (!value.trim()) {
applyFilters(transactions, accountFilter, statusFilter, typeFilter);
return;
}
const filtered = transactions.filter((transaction) => {
const searchLower = value.toLowerCase();
return (
transaction.transactionId?.toLowerCase().includes(searchLower) ||
transaction.fromAccount?.toLowerCase().includes(searchLower) ||
transaction.toAccount?.toLowerCase().includes(searchLower) ||
transaction.description?.toLowerCase().includes(searchLower)
);
});
applyFilters(filtered, accountFilter, statusFilter, typeFilter);
}, [transactions, accountFilter, statusFilter, typeFilter]);
// Apply filters
const applyFilters = useCallback((data, account, status, type) => {
let filtered = [...data];
if (account) {
filtered = filtered.filter(
(transaction) =>
transaction.fromAccount?.includes(account) ||
transaction.toAccount?.includes(account)
);
}
if (status) {
filtered = filtered.filter(
(transaction) => transaction.status === status
);
}
if (type) {
filtered = filtered.filter(
(transaction) => transaction.type === type
);
}
setFilteredTransactions(filtered);
}, []);
// Handle filter changes
useEffect(() => {
const baseData = searchTerm.trim()
? transactions.filter((transaction) => {
const searchLower = searchTerm.toLowerCase();
return (
transaction.transactionId?.toLowerCase().includes(searchLower) ||
transaction.fromAccount?.toLowerCase().includes(searchLower) ||
transaction.toAccount?.toLowerCase().includes(searchLower) ||
transaction.description?.toLowerCase().includes(searchLower)
);
})
: transactions;
applyFilters(baseData, accountFilter, statusFilter, typeFilter);
}, [accountFilter, statusFilter, typeFilter, transactions, searchTerm, applyFilters]);
// Clear all filters
const clearFilters = () => {
setSearchTerm("");
setAccountFilter("");
setStatusFilter("");
setTypeFilter("");
setFilteredTransactions(transactions);
};
// Format currency
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
// Format date
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
// Get unique account numbers for filter
const uniqueAccounts = [...new Set([
...transactions.map(t => t.fromAccount).filter(Boolean),
...transactions.map(t => t.toAccount).filter(Boolean)
])];
if (loading) {
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2>All Transactions</h2>
<TableContainer>
<LoadingSpinner
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<RefreshCw size={32} />
<p style={{ marginLeft: spacing.lg, fontSize: '18px', fontWeight: '500' }}>
Loading transactions...
</p>
</LoadingSpinner>
</TableContainer>
</Container>
);
}
if (error) {
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2>All Transactions</h2>
<TableContainer>
<ErrorMessage
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<AlertCircle size={48} />
<h3>Something went wrong</h3>
<p>{error}</p>
<Button
variant="primary"
onClick={fetchTransactions}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw size={16} />
Try Again
</Button>
</ErrorMessage>
</TableContainer>
</Container>
);
}
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2>All Transactions</h2>
<FilterSection
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1, duration: 0.5 }}
>
<SearchContainer>
<Search size={20} />
<SearchInput
type="text"
placeholder="Search transactions by ID, account, or description..."
value={searchTerm}
onChange={handleSearch}
/>
</SearchContainer>
<FiltersGrid>
<FilterSelect
value={accountFilter}
onChange={(e) => setAccountFilter(e.target.value)}
>
<option value="">Filter by account number</option>
{uniqueAccounts.map(account => (
<option key={account} value={account}>{account}</option>
))}
</FilterSelect>
<FilterSelect
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="">Filter by status</option>
<option value="SUCCESS">SUCCESS</option>
<option value="PENDING">PENDING</option>
<option value="FAILED">FAILED</option>
</FilterSelect>
<FilterSelect
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option value="">Filter by transaction type</option>
<option value="DEPOSIT">DEPOSIT</option>
<option value="WITHDRAW">WITHDRAW</option>
<option value="TRANSFER">TRANSFER</option>
<option value="LOAN">LOAN</option>
</FilterSelect>
</FiltersGrid>
<ButtonGroup>
<Button
variant="secondary"
onClick={clearFilters}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<X size={16} />
Clear Filters
</Button>
<Button
variant="primary"
onClick={fetchTransactions}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw size={16} />
Refresh
</Button>
</ButtonGroup>
</FilterSection>
<TableContainer>
{filteredTransactions.length === 0 ? (
<EmptyState
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<Filter size={48} />
<h3>No transactions found</h3>
<p>
{searchTerm || accountFilter || statusFilter || typeFilter
? "Try adjusting your search or filter criteria"
: "No transactions available for your account"}
</p>
</EmptyState>
) : (
<Table>
<thead>
<tr>
<th>Sr No</th>
<th>Transaction ID</th>
<th>Date</th>
<th>Type</th>
<th>Amount</th>
<th>From Account</th>
<th>To Account</th>
<th>Status</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{filteredTransactions.map((transaction) => (
<motion.tr
key={transaction.srNo}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<td>{transaction.srNo}</td>
<td>
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.transactionId}
</code>
</td>
<td>{formatDate(transaction.date)}</td>
<td>
<TypeBadge type={transaction.type}>
{transaction.type}
</TypeBadge>
</td>
<td style={{
fontWeight: '600',
color: transaction.type === 'WITHDRAW' ? colors.error : colors.success
}}>
{formatCurrency(transaction.amount)}
</td>
<td>
{transaction.fromAccount ? (
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.fromAccount}
</code>
) : '-'}
</td>
<td>
{transaction.toAccount ? (
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.toAccount}
</code>
) : '-'}
</td>
<td>
<StatusBadge status={transaction.status}>
{transaction.status}
</StatusBadge>
</td>
<td>{transaction.description}</td>
</motion.tr>
))}
</tbody>
</Table>
)}
</TableContainer>
</Container>
);
}
export default TransactionsPage;
------------------- TRANSACTIONS PAGE ----------------
/* eslint-disable no-unused-vars */
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { motion } from 'framer-motion';
import { AlertCircle, Filter, RefreshCw, Search, X } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import api from '@/services/api';
import { useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import { useFeatureFlag } from '@/contexts/FeatureFlagContext';
function TransactionsPage() {
const [transactions, setTransactions] = useState([]);
const [filteredTransactions, setFilteredTransactions] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [accountFilter, setAccountFilter] = useState("");
const [statusFilter, setStatusFilter] = useState("");
const [typeFilter, setTypeFilter] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [userAccounts, setUser Accounts] = useState([]);
const [accountsLoading, setAccountsLoading] = useState(true);
const { user } = useAuth();
const { getFlag } = useFeatureFlag();
const useFilters = getFlag('useFilters');
// Fetch transactions
const fetchTransactions = useCallback(async () => {
if (!user?.id) return;
try {
setLoading(true);
setError(null);
const response = await api.get(`/transactions/user/${user.id}`);
console.log("TRANSACTIONS", response.data.data);
setTransactions(response.data.data);
setFilteredTransactions(response.data.data);
} catch (err) {
setError('Failed to load transactions. Please try again.');
console.error('Error fetching transactions:', err);
} finally {
setLoading(false);
}
}, [user?.id]);
// Fetch user accounts
const fetchUser Accounts = useCallback(async () => {
if (!user?.id) return;
try {
setAccountsLoading(true);
const response = await api.get(`/accounts/user/${user.id}`);
setUser Accounts(response.data.data);
} catch (err) {
console.error("Error fetching user accounts:", err);
toast.error("Failed to load user accounts");
} finally {
setAccountsLoading(false);
}
}, [user?.id]);
// Initial load
useEffect(() => {
fetchTransactions();
fetchUser Accounts();
}, [fetchTransactions, fetchUser Accounts]);
// Apply filters
const applyFilters = useCallback((data, account, status, type) => {
let filtered = [...data];
if (account) {
filtered = filtered.filter(
(transaction) =>
transaction.fromAccount?.accountNumber.includes(account) ||
transaction.toAccount?.accountNumber.includes(account)
);
}
if (status) {
filtered = filtered.filter(
(transaction) => transaction.status === status
);
}
if (type) {
filtered = filtered.filter(
(transaction) => transaction.type === type
);
}
setFilteredTransactions(filtered);
}, []);
// Handle search
const handleSearch = useCallback((e) => {
const value = e.target.value;
setSearchTerm(value);
if (!value.trim()) {
applyFilters(transactions, accountFilter, statusFilter, typeFilter);
return;
}
const filtered = transactions.filter((transaction) => {
const searchLower = value.toLowerCase();
return (
transaction.id.toString().includes(searchLower) ||
transaction.fromAccount?.accountNumber.toLowerCase().includes(searchLower) ||
transaction.toAccount?.accountNumber.toLowerCase().includes(searchLower) ||
transaction.description?.toLowerCase().includes(searchLower)
);
});
applyFilters(filtered, accountFilter, statusFilter, typeFilter);
}, [transactions, applyFilters, accountFilter, statusFilter, typeFilter]);
// Handle filter changes
useEffect(() => {
const baseData = searchTerm.trim()
? transactions.filter((transaction) => {
const searchLower = searchTerm.toLowerCase();
return (
transaction.id?.toString().toLowerCase().includes(searchLower) ||
transaction.fromAccount?.accountNumber.toLowerCase().includes(searchLower) ||
transaction.toAccount?.accountNumber.toLowerCase().includes(searchLower) ||
transaction.description?.toLowerCase().includes(searchLower)
);
})
: transactions;
applyFilters(baseData, accountFilter, statusFilter, typeFilter);
}, [accountFilter, statusFilter, typeFilter, transactions, searchTerm, applyFilters]);
// Clear all filters
const clearFilters = () => {
setSearchTerm("");
setAccountFilter("");
setStatusFilter("");
setTypeFilter("");
setFilteredTransactions(transactions);
};
// Format currency
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'INR'
}).format(amount);
};
// Format date
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
console.log("Transactions - ", transactions);
if (loading) {
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2>All Transactions</h2>
<TableContainer>
<LoadingSpinner
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<RefreshCw size={32} />
<p style={{ marginLeft: spacing.lg, fontSize: '18px', fontWeight: '500' }}>
Loading transactions...
</p>
</LoadingSpinner>
</TableContainer>
</Container>
);
}
if (error) {
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2>All Transactions</h2>
<TableContainer>
<ErrorMessage
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<AlertCircle size={48} />
<h3>Something went wrong</h3>
<p>{error}</p>
<Button
variant="primary"
onClick={fetchTransactions}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw size={16} />
Try Again
</Button>
</ErrorMessage>
</TableContainer>
</Container>
);
}
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{useFilters ? (
<FilterSection
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1, duration: 0.5 }}
>
<SearchContainer>
<Search size={20} />
<SearchInput
type="text"
placeholder="Search transactions by ID, account, or description..."
value={searchTerm}
onChange={handleSearch}
/>
</SearchContainer>
<FiltersGrid>
<FilterSelect
value={accountFilter}
onChange={(e) => setAccountFilter(e.target.value)}
>
<option value="">Filter by account number</option>
{(Array.isArray(userAccounts) ? userAccounts : []).map((account) => (
<option key={account.accountNumber} value={account.accountNumber}>
{account.accountNumber} ({account.accountType})
</option>
))}
</FilterSelect>
<FilterSelect
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="">Filter by status</option>
<option value="SUCCESS">SUCCESS</option>
<option value="PENDING">PENDING</option>
<option value="FAILED">FAILED</option>
</FilterSelect>
<FilterSelect
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option value="">Filter by transaction type</option>
<option value="DEPOSIT">DEPOSIT</option>
<option value="WITHDRAWAL">WITHDRAW</option>
<option value="TRANSFER">TRANSFER</option>
<option value="LOAN">LOAN</option>
</FilterSelect>
</FiltersGrid>
<ButtonGroup>
<Button
variant="secondary"
onClick={clearFilters}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<X size={16} />
Clear Filters
</Button>
<Button
variant="primary"
onClick={fetchTransactions}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw size={16} />
Refresh
</Button>
</ButtonGroup>
</FilterSection>
) : (
<Message
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1, duration: 0.5 }}
>
Filtering feature has been taken down temporarily
</Message>
)}
<h2>All Transactions</h2>
<TableContainer>
{filteredTransactions.length === 0 ? (
<EmptyState
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<Filter size={48} />
<h3>No transactions found</h3>
<p>
{searchTerm || accountFilter || statusFilter || typeFilter
? "Try adjusting your search or filter criteria"
: "No transactions available for your account"}
</p>
</EmptyState>
) : (
<Table>
<thead>
<tr>
<th>Sr No</th>
<th>Transaction ID</th>
<th>Date</th>
<th>Type</th>
<th>Amount</th>
<th>From Account</th>
<th>To Account</th>
<th>Status</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{filteredTransactions.map((transaction, idx) => (
<motion.tr
key={transaction.srNo}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<td>{idx + 1}</td>
<td>
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.id}
</code>
</td>
<td>{formatDate(transaction.timestamp)}</td>
<td>
<TypeBadge type={transaction.type}>
{transaction.type}
</TypeBadge>
</td>
<td style={{
fontWeight: '600',
color: transaction.type === 'WITHDRAW' ? colors.error : colors.success
}}>
{formatCurrency(transaction.amount)}
</td>
<td>
{transaction.fromAccount ? (
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.fromAccount.accountNumber}
</code>
) : '-'}
</td>
<td>
{transaction.toAccount ? (
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.toAccount.accountNumber}
</code>
) : '-'}
</td>
<td>
<StatusBadge status={transaction.status}>
{transaction.status}
</StatusBadge>
</td>
<td>{transaction.description}</td>
</motion.tr>
))}
</tbody>
</Table>
)}
</TableContainer>
</Container>
);
}
export default TransactionsPage;
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)`,
warning: `linear-gradient(135deg, ${colors.warning}, #ff8c00)`,
error: `linear-gradient(135deg, ${colors.error}, #ee5a52)`,
}
const spacing = {
xs: "4px",
sm: "8px",
md: "12px",
lg: "16px",
xl: "20px",
xxl: "2px",
xxxl: "32px",
}
-------------------TRANSACTIONS CARD LAYOUT---------------
// Add this styled component with your other styled components
const CardsContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: ${spacing.xl};
padding: ${spacing.lg};
`;
const TransactionCard = styled(motion.div)`
background: ${colors.glass};
border: 1px solid ${colors.border};
border-radius: 16px;
padding: ${spacing.xl};
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px ${colors.shadow};
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.15);
border-color: ${colors.borderHover};
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: ${gradients.primary};
}
`;
const CardHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: ${spacing.lg};
`;
const CardTitle = styled.div`
display: flex;
flex-direction: column;
gap: ${spacing.xs};
`;
const TransactionId = styled.code`
background: ${colors.shadowHover};
padding: 6px 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
color: ${colors.text};
font-family: 'Monaco', 'Menlo', monospace;
`;
const Amount = styled.div`
font-size: 24px;
font-weight: 700;
color: ${props => props.type === 'WITHDRAW' ? colors.error : colors.success};
text-align: right;
`;
const CardBody = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
gap: ${spacing.lg};
margin-bottom: ${spacing.lg};
`;
const InfoGroup = styled.div`
display: flex;
flex-direction: column;
gap: ${spacing.sm};
`;
const InfoLabel = styled.span`
font-size: 12px;
font-weight: 600;
color: ${colors.textMuted};
text-transform: uppercase;
letter-spacing: 0.5px;
`;
const InfoValue = styled.span`
font-size: 14px;
font-weight: 500;
color: ${colors.text};
word-break: break-all;
`;
const AccountCode = styled.code`
background: ${colors.shadowHover};
padding: 4px 8px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
font-family: 'Monaco', 'Menlo', monospace;
`;
const CardFooter = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding-top: ${spacing.md};
border-top: 1px solid ${colors.border};
`;
const Description = styled.p`
margin: 0;
font-size: 14px;
color: ${colors.textSecondary};
font-style: italic;
flex: 1;
margin-right: ${spacing.md};
`;
const DateTime = styled.span`
font-size: 12px;
color: ${colors.textMuted};
font-weight: 500;
`;
// Replace your table rendering section with this:
{filteredTransactions.length === 0 ? (
<EmptyState
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<Filter size={48} />
<h3>No transactions found</h3>
<p>
{searchTerm || accountFilter || statusFilter || typeFilter
? "Try adjusting your search or filter criteria"
: "No transactions available for your account"}
</p>
</EmptyState>
) : (
<CardsContainer>
{filteredTransactions.map((transaction, idx) => (
<TransactionCard
key={transaction.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: idx * 0.05 }}
whileHover={{ scale: 1.02 }}
>
<CardHeader>
<CardTitle>
<TransactionId>#{transaction.id}</TransactionId>
<TypeBadge type={transaction.type}>
{transaction.type}
</TypeBadge>
</CardTitle>
<Amount type={transaction.type}>
{formatCurrency(transaction.amount)}
</Amount>
</CardHeader>
<CardBody>
<InfoGroup>
<InfoLabel>From Account</InfoLabel>
<InfoValue>
{transaction.fromAccount ? (
<AccountCode>{transaction.fromAccount.accountNumber}</AccountCode>
) : (
<span style={{ color: colors.textMuted }}>-</span>
)}
</InfoValue>
</InfoGroup>
<InfoGroup>
<InfoLabel>To Account</InfoLabel>
<InfoValue>
{transaction.toAccount ? (
<AccountCode>{transaction.toAccount.accountNumber}</AccountCode>
) : (
<span style={{ color: colors.textMuted }}>-</span>
)}
</InfoValue>
</InfoGroup>
<InfoGroup>
<InfoLabel>Status</InfoLabel>
<InfoValue>
<StatusBadge status={transaction.status}>
{transaction.status}
</StatusBadge>
</InfoValue>
</InfoGroup>
<InfoGroup>
<InfoLabel>Serial No.</InfoLabel>
<InfoValue>#{idx + 1}</InfoValue>
</InfoGroup>
</CardBody>
<CardFooter>
<Description>
{transaction.description || "No description provided"}
</Description>
<DateTime>
{formatDate(transaction.timestamp)}
</DateTime>
</CardFooter>
</TransactionCard>
))}
</CardsContainer>
)}
-------------FEATURE TOGGLE COMPONENT------------
import { useFeatureFlag } from '@/contexts/FeatureFlagContext';
import React from 'react';
import styled, { keyframes } from 'styled-components';
const ripple = keyframes`
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(4);
opacity: 0;
}
`;
const ToggleContainer = styled.div`
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 0;
`;
const Label = styled.span`
font-size: 0.9rem;
font-weight: 600;
color: ${({ isEnabled }) => (isEnabled ? '#1e293b' : '#64748b')};
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: -0.025em;
user-select: none;
`;
const ToggleButton = styled.button`
position: relative;
display: inline-flex;
height: 1.875rem;
width: 3.25rem;
align-items: center;
border-radius: 1.875rem;
border: none;
cursor: pointer;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: ${({ isEnabled }) =>
isEnabled
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%)'};
box-shadow: ${({ isEnabled }) =>
isEnabled
? '0 4px 12px rgba(102, 126, 234, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2)'
: '0 2px 8px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8)'};
&:hover {
transform: translateY(-1px);
box-shadow: ${({ isEnabled }) =>
isEnabled
? '0 6px 16px rgba(102, 126, 234, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2)'
: '0 4px 12px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.8)'};
}
&:active {
transform: translateY(0);
transition: transform 0.1s;
}
&:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.3);
border-radius: inherit;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
transition: all 0.3s ease;
}
&:active::before {
animation: ${ripple} 0.6s ease-out;
}
`;
const ToggleTrack = styled.div`
position: absolute;
inset: 2px;
border-radius: inherit;
background: ${({ isEnabled }) =>
isEnabled ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)'};
transition: background 0.3s ease;
`;
const ToggleCircle = styled.span`
position: relative;
display: inline-block;
height: 1.375rem;
width: 1.375rem;
border-radius: 50%;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateX(${({ isEnabled }) => (isEnabled ? '1.375rem' : '0.25rem')});
z-index: 2;
&::before {
content: '';
position: absolute;
inset: 1px;
border-radius: inherit;
background: ${({ isEnabled }) =>
isEnabled
? 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)'
: 'linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%)'};
transition: background 0.3s ease;
}
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0.375rem;
height: 0.375rem;
border-radius: 50%;
background: ${({ isEnabled }) =>
isEnabled
? 'radial-gradient(circle, #667eea 0%, #764ba2 100%)'
: 'radial-gradient(circle, #94a3b8 0%, #64748b 100%)'};
transform: translate(-50%, -50%);
opacity: ${({ isEnabled }) => (isEnabled ? 1 : 0.6)};
transition: all 0.3s ease;
z-index: 1;
}
`;
const StatusIndicator = styled.div`
position: absolute;
top: -0.25rem;
right: ${({ isEnabled }) => (isEnabled ? '-0.25rem' : 'auto')};
left: ${({ isEnabled }) => (isEnabled ? 'auto' : '-0.25rem')};
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: ${({ isEnabled }) => (isEnabled ? '#10b981' : '#ef4444')};
box-shadow: 0 0 0 2px white, 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
opacity: ${({ isEnabled }) => (isEnabled ? 1 : 0.8)};
`;
const FeatureToggle = ({ flagName, label }) => {
const { getFlag, toggleFlag } = useFeatureFlag();
const isEnabled = getFlag(flagName);
return (
<ToggleContainer>
<Label isEnabled={isEnabled}>{label}</Label>
<ToggleButton
onClick={() => toggleFlag(flagName)}
role="switch"
aria-checked={isEnabled}
isEnabled={isEnabled}
>
<ToggleTrack isEnabled={isEnabled} />
<ToggleCircle isEnabled={isEnabled} />
<StatusIndicator isEnabled={isEnabled} />
</ToggleButton>
</ToggleContainer>
);
};
export default FeatureToggle;
------------------GLOBAL FEATURE FLAG IMPLEMENTATION----------------
// 1. FeatureFlagContext.js - Context for managing feature flags
import React, { createContext, useContext, useState } from 'react';
const FeatureFlagContext = createContext();
export const FeatureFlagProvider = ({ children }) => {
const [flags, setFlags] = useState({
useCardLayout: false, // Flag for card vs table layout
// Add more flags here as needed
// darkMode: false,
// newNavigation: false,
});
const toggleFlag = (flagName) => {
setFlags(prevFlags => ({
...prevFlags,
[flagName]: !prevFlags[flagName]
}));
};
const setFlag = (flagName, value) => {
setFlags(prevFlags => ({
...prevFlags,
[flagName]: value
}));
};
const getFlag = (flagName) => {
return flags[flagName] || false;
};
return (
<FeatureFlagContext.Provider value={{
flags,
toggleFlag,
setFlag,
getFlag
}}>
{children}
</FeatureFlagContext.Provider>
);
};
export const useFeatureFlag = () => {
const context = useContext(FeatureFlagContext);
if (!context) {
throw new Error('useFeatureFlag must be used within a FeatureFlagProvider');
}
return context;
};
// 2. FeatureToggle.js - Toggle button component for header
import React from 'react';
import { useFeatureFlag } from './FeatureFlagContext';
const FeatureToggle = ({ flagName, label, className = '' }) => {
const { getFlag, toggleFlag } = useFeatureFlag();
const isEnabled = getFlag(flagName);
return (
<div className={`flex items-center space-x-2 ${className}`}>
<span className="text-sm font-medium text-gray-700">{label}</span>
<button
onClick={() => toggleFlag(flagName)}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
${isEnabled ? 'bg-blue-600' : 'bg-gray-200'}
`}
role="switch"
aria-checked={isEnabled}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${isEnabled ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</div>
);
};
export default FeatureToggle;
// 3. Header.js - Updated header component with feature toggle
import React from 'react';
import FeatureToggle from './FeatureToggle';
const Header = () => {
return (
<header className="bg-white shadow-sm border-b border-gray-200 px-6 py-4">
<div className="flex justify-between items-center">
<div className="flex items-center">
<h1 className="text-xl font-semibold text-gray-900">Banking Dashboard</h1>
</div>
<div className="flex items-center space-x-4">
{/* Other header items like user menu, notifications etc. */}
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">View:</span>
<FeatureToggle
flagName="useCardLayout"
label="Card Layout"
/>
</div>
</div>
</div>
</header>
);
};
export default Header;
// 4. App.js - Root component wrapped with FeatureFlagProvider
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { FeatureFlagProvider } from './FeatureFlagContext';
import Layout from './Layout';
import DataPage from './DataPage';
// Import other pages
function App() {
return (
<FeatureFlagProvider>
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="data" element={<DataPage />} />
{/* Other routes */}
</Route>
</Routes>
</Router>
</FeatureFlagProvider>
);
}
export default App;
// 5. Layout.js - Main layout component
import React from 'react';
import { Outlet } from 'react-router-dom';
import Header from './Header';
import Sidebar from './Sidebar';
const Layout = () => {
return (
<div className="flex h-screen bg-gray-50">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<Header />
<main className="flex-1 overflow-auto p-6">
<Outlet />
</main>
</div>
</div>
);
};
export default Layout;
// 6. DataPage.js - Example page that uses the feature flag
import React from 'react';
import { useFeatureFlag } from './FeatureFlagContext';
const DataPage = () => {
const { getFlag } = useFeatureFlag();
const useCardLayout = getFlag('useCardLayout');
// Sample data
const data = [
{ id: 1, name: 'John Doe', account: '****1234', balance: '$5,000', status: 'Active' },
{ id: 2, name: 'Jane Smith', account: '****5678', balance: '$12,500', status: 'Active' },
{ id: 3, name: 'Bob Johnson', account: '****9012', balance: '$2,750', status: 'Pending' },
{ id: 4, name: 'Alice Brown', account: '****3456', balance: '$8,900', status: 'Active' },
];
// Table View Component
const TableView = () => (
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium text-gray-900">Account Data - Table View</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Account
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Balance
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.map((item) => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{item.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.account}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{item.balance}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
item.status === 'Active'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{item.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
// Card View Component
const CardView = () => (
<div>
<div className="mb-6">
<h2 className="text-lg font-medium text-gray-900">Account Data - Card View</h2>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{data.map((item) => (
<div key={item.id} className="bg-white shadow rounded-lg p-6 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">{item.name}</h3>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
item.status === 'Active'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{item.status}
</span>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Account:</span>
<span className="text-sm font-medium text-gray-900">{item.account}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Balance:</span>
<span className="text-sm font-medium text-gray-900">{item.balance}</span>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-200">
<button className="text-blue-600 hover:text-blue-800 text-sm font-medium">
View Details →
</button>
</div>
</div>
))}
</div>
</div>
);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Customer Accounts</h1>
<div className="text-sm text-gray-500">
Current view: {useCardLayout ? 'Card Layout' : 'Table Layout'}
</div>
</div>
{/* Conditional rendering based on feature flag */}
{useCardLayout ? <CardView /> : <TableView />}
</div>
);
};
export default DataPage;
// 7. Sidebar.js - Example sidebar component
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
const Sidebar = () => {
const location = useLocation();
const menuItems = [
{ path: '/', label: 'Dashboard', icon: '📊' },
{ path: '/data', label: 'Accounts', icon: '👥' },
{ path: '/transactions', label: 'Transactions', icon: '💳' },
{ path: '/settings', label: 'Settings', icon: '⚙️' },
];
return (
<div className="bg-gray-800 w-64 flex flex-col">
<div className="flex items-center justify-center h-16 bg-gray-900">
<span className="text-white text-lg font-semibold">BankApp</span>
</div>
<nav className="flex-1 px-4 py-6 space-y-2">
{menuItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`flex items-center px-4 py-2 text-sm rounded-lg transition-colors ${
location.pathname === item.path
? 'bg-gray-700 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
}`}
>
<span className="mr-3">{item.icon}</span>
{item.label}
</Link>
))}
</nav>
</div>
);
};
export default Sidebar;
------------------------ TRANSACTIONS IN THE PROFILE PAGE----------------
- Additional Imports (at the top with other imports)
import { useState } from 'react';
import { FaTimes } from 'react-icons/fa';
- New Styled Components (add after the existing styled components)
const ModalOverlay = styled(motion.div)`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
`;
const ModalContent = styled(motion.div)`
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 32px;
width: 90%;
max-width: 1000px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.3);
position: relative;
`;
const ModalHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid rgba(102, 126, 234, 0.1);
h3 {
font-size: 24px;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
`;
const CloseButton = styled(motion.button)`
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #667eea;
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
transform: rotate(90deg);
}
`;
const TransactionTable = styled(motion.table)`
width: 100%;
border-collapse: separate;
border-spacing: 0;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
`;
const StatusBadge = styled.span`
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
${props => {
switch (props.status) {
case 'SUCCESS':
return `
background: rgba(34, 197, 94, 0.1);
color: #059669;
border: 1px solid rgba(34, 197, 94, 0.2);
`;
case 'FAILURE':
return `
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
border: 1px solid rgba(239, 68, 68, 0.2);
`;
case 'PENDING':
return `
background: rgba(245, 158, 11, 0.1);
color: #d97706;
border: 1px solid rgba(245, 158, 11, 0.2);
`;
default:
return `
background: rgba(156, 163, 175, 0.1);
color: #6b7280;
border: 1px solid rgba(156, 163, 175, 0.2);
`;
}
}}
`;
const TypeBadge = styled.span`
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
${props => {
switch (props.type) {
case 'DEPOSIT':
return `
background: rgba(34, 197, 94, 0.1);
color: #059669;
border: 1px solid rgba(34, 197, 94, 0.2);
`;
case 'WITHDRAWAL':
return `
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
border: 1px solid rgba(239, 68, 68, 0.2);
`;
case 'TRANSFER':
return `
background: rgba(59, 130, 246, 0.1);
color: #2563eb;
border: 1px solid rgba(59, 130, 246, 0.2);
`;
case 'LOAN':
return `
background: rgba(168, 85, 247, 0.1);
color: #7c3aed;
border: 1px solid rgba(168, 85, 247, 0.2);
`;
default:
return `
background: rgba(156, 163, 175, 0.1);
color: #6b7280;
border: 1px solid rgba(156, 163, 175, 0.2);
`;
}
}}
`;
const EmptyState = styled(motion.div)`
text-align: center;
padding: 60px 20px;
color: #6b7280;
h4 {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
color: #374151;
}
p {
font-size: 16px;
margin: 0;
}
`;
- State Management (add inside the Profile component, after the existing useQuery)
const [selectedAccount, setSelectedAccount] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const { data: transactions, isLoading: isTransactionsLoading } = useQuery({
queryKey: ['transactions', selectedAccount],
queryFn: async () => {
if (!selectedAccount) return [];
try {
const response = await api.get(`/transactions/${selectedAccount}`);
return response.data.data;
} catch (error) {
console.error('Error fetching transactions', error);
toast.error("Error fetching transactions");
return [];
}
},
enabled: !!selectedAccount
});
- Helper Functions (add inside the Profile component)
const openTransactionModal = (accountNumber) => {
setSelectedAccount(accountNumber);
setIsModalOpen(true);
};
const closeTransactionModal = () => {
setIsModalOpen(false);
setSelectedAccount(null);
};
const formatTransactionAmount = (amount, type) => {
const formattedAmount = formatCurrency(amount);
if (type === 'DEPOSIT') return `+${formattedAmount}`;
if (type === 'WITHDRAWAL') return `-${formattedAmount}`;
return formattedAmount;
};
- Update the View Transactions Button (replace the existing ActionLink for transactions)
<ActionLink
variants={actionLinkVariants}
onClick={() => openTransactionModal(account.accountNumber)}
>
<FaEye /> View Transactions
</ActionLink>
- Add the Modal (add this right before the closing tag)
{isModalOpen && (
<ModalOverlay
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={closeTransactionModal}
>
<ModalContent
initial={{ opacity: 0, scale: 0.8, y: 50 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 50 }}
onClick={(e) => e.stopPropagation()}
>
<ModalHeader>
<h3>Transaction History - {selectedAccount}</h3>
<CloseButton
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={closeTransactionModal}
>
<FaTimes />
</CloseButton>
</ModalHeader>
{isTransactionsLoading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
Loading transactions...
</div>
) : transactions?.length === 0 ? (
<EmptyState
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h4>No Transactions Found</h4>
<p>This account has no transaction history yet.</p>
</EmptyState>
) : (
<TransactionTable
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<thead>
<tr>
<TableHeader>Date</TableHeader>
<TableHeader>Type</TableHeader>
<TableHeader>From Account</TableHeader>
<TableHeader>To Account</TableHeader>
<TableHeader>Amount</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader>Status</TableHeader>
</tr>
</thead>
<tbody>
{transactions?.map((transaction) => (
<TableRow key={transaction.id}>
<TableCell>{formatDate(transaction.timestamp)}</TableCell>
<TableCell>
<TypeBadge type={transaction.type}>
{transaction.type}
</TypeBadge>
</TableCell>
<TableCell>
{transaction.fromAccount ? (
<code style={{
background: 'rgba(102,126,234,0.1)',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '12px'
}}>
{transaction.fromAccount.accountNumber}
</code>
) : (
<span style={{ color: '#9ca3af', fontStyle: 'italic' }}>
External
</span>
)}
</TableCell>
<TableCell>
{transaction.toAccount ? (
<code style={{
background: 'rgba(102,126,234,0.1)',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '12px'
}}>
{transaction.toAccount.accountNumber}
</code>
) : (
<span style={{ color: '#9ca3af', fontStyle: 'italic' }}>
External
</span>
)}
</TableCell>
<TableCell>
<span style={{
fontWeight: 'bold',
color: transaction.type === 'DEPOSIT' ? '#059669' :
transaction.type === 'WITHDRAWAL' ? '#dc2626' : '#374151'
}}>
{formatTransactionAmount(transaction.amount, transaction.type)}
</span>
</TableCell>
<TableCell>{transaction.description || 'No description'}</TableCell>
<TableCell>
<StatusBadge status={transaction.status}>
{transaction.status}
</StatusBadge>
</TableCell>
</TableRow>
))}
</tbody>
</TransactionTable>
)}
</ModalContent>
</ModalOverlay>
)}
Top comments (0)