import { useState, useEffect, useCallback } from "react";
import { Send, RefreshCw, AlertCircle, Filter } from "lucide-react";
import styled from 'styled-components';
import { motion, AnimatePresence } from 'framer-motion';
import toast from 'react-hot-toast';
import ConfirmModal from '@/utils/ConfirmModal';
// 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 FormContainer = styled(motion.div)`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
padding: ${spacing.xxxl};
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};
}
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: ${spacing.xl};
@media (max-width: 768px) {
grid-template-columns: 1fr;
padding: ${spacing.xxl};
}
`;
const Input = styled.input`
width: 100%;
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};
}
&::placeholder {
color: ${colors.textMuted};
}
`;
const Select = styled.select`
width: 100%;
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 TransferButton = styled(motion.button)`
display: flex;
align-items: center;
justify-content: center;
gap: ${spacing.sm};
padding: ${spacing.lg} ${spacing.xxxl};
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: none;
background: ${gradients.primary};
color: white;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
grid-column: 1 / -1;
max-width: 300px;
justify-self: center;
&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
}
&:active {
transform: translateY(0);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
@media (max-width: 768px) {
max-width: 100%;
}
`;
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' || props.status === 'FAILURE') && `
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 NewTag = styled.span`
background: ${gradients.success};
color: white;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
margin-left: ${spacing.sm};
`;
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) => {
await new Promise(resolve => setTimeout(resolve, 1000));
if (endpoint.includes('/accounts/user/')) {
return {
data: [
{ accountNumber: '123456789', accountType: 'SAVINGS', balance: 5000 },
{ accountNumber: '987654321', accountType: 'CHECKING', balance: 3000 },
{ accountNumber: '555666777', accountType: 'BUSINESS', balance: 10000 }
]
};
}
if (endpoint.includes('/transactions/user/')) {
return {
data: [
{
id: 1,
date: "2023-10-01",
type: "TRANSFER",
amount: 1500,
fromAccount: { accountNumber: "123456789" },
toAccount: { accountNumber: "987654321" },
status: "SUCCESS",
description: "Transfer to Savings",
},
{
id: 2,
date: "2023-10-02",
type: "TRANSFER",
amount: 2000,
fromAccount: { accountNumber: "123456789" },
toAccount: { accountNumber: "987654322" },
status: "PENDING",
description: "Transfer to Checking",
},
{
id: 3,
date: "2023-10-03",
type: "DEPOSIT",
amount: 750,
fromAccount: null,
toAccount: { accountNumber: "123456789" },
status: "SUCCESS",
description: "Salary deposit",
},
{
id: 4,
date: "2023-10-04",
type: "TRANSFER",
amount: 3000,
fromAccount: { accountNumber: "987654321" },
toAccount: { accountNumber: "123456789" },
status: "FAILED",
description: "Failed Transfer",
},
]
};
}
},
post: async (endpoint, data) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (endpoint === '/transactions/transfer') {
return {
data: {
data: {
id: Date.now(),
date: new Date().toISOString().split('T')[0],
type: "TRANSFER",
amount: data.amount,
fromAccount: { accountNumber: data.fromAccountNumber },
toAccount: { accountNumber: data.toAccountNumber },
status: "SUCCESS",
description: data.description,
new: true
}
}
};
}
}
};
// Mock query invalidation
const invalidateQueries = (queryKey) => {
console.log(`Invalidating queries: ${queryKey}`);
};
export default function Transfer() {
const [transactions, setTransactions] = useState([]);
const [userAccounts, setUserAccounts] = useState([]);
const [form, setForm] = useState({
fromAccountNumber: "",
toAccountNumber: "",
amount: "",
description: ""
});
const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [accountsLoading, setAccountsLoading] = useState(true);
const [transferring, setTransferring] = useState(false);
const [error, setError] = useState(null);
const { user } = useAuth();
// Fetch user accounts
const fetchUserAccounts = useCallback(async () => {
if (!user?.id) return;
try {
setAccountsLoading(true);
const response = await api.get(`/accounts/user/${user.id}`);
setUserAccounts(response.data);
} catch (err) {
console.error('Error fetching user accounts:', err);
toast.error('Failed to load user accounts');
} finally {
setAccountsLoading(false);
}
}, [user?.id]);
// Fetch transactions
const fetchTransactions = useCallback(async () => {
if (!user?.id) return;
try {
setLoading(true);
setError(null);
const response = await api.get(`/transactions/user/${user.id}`);
// Filter only transfer transactions
const transferTransactions = response.data.filter(transaction =>
transaction.type === 'TRANSFER'
);
setTransactions(transferTransactions);
} catch (err) {
setError('Failed to load transactions. Please try again.');
console.error('Error fetching transactions:', err);
} finally {
setLoading(false);
}
}, [user?.id]);
// Initial load
useEffect(() => {
fetchUserAccounts();
fetchTransactions();
}, [fetchUserAccounts, fetchTransactions]);
const handleInputChange = (e) => {
setForm(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
const handleTransfer = () => {
// Validation
if (!form.fromAccountNumber || !form.toAccountNumber || !form.amount || !form.description) {
toast.error('Please fill in all fields');
return;
}
if (parseFloat(form.amount) <= 0) {
toast.error('Amount must be greater than 0');
return;
}
if (form.fromAccountNumber === form.toAccountNumber) {
toast.error('From and To accounts cannot be the same');
return;
}
setModalOpen(true);
};
const confirmTransfer = async () => {
try {
setTransferring(true);
const transferData = {
fromAccountNumber: form.fromAccountNumber,
toAccountNumber: form.toAccountNumber,
amount: parseFloat(form.amount),
description: form.description
};
const response = await api.post('/transactions/transfer', transferData);
// Update transactions list with new transaction
setTransactions(prev => [response.data.data, ...prev]);
// Invalidate accounts query to refresh balances
invalidateQueries(['accounts']);
toast.success('Money transferred successfully');
// Reset form
setForm({
fromAccountNumber: "",
toAccountNumber: "",
amount: "",
description: ""
});
setModalOpen(false);
} catch (err) {
console.error('Transfer error:', err);
toast.error('Transfer failed. Please try again.');
} finally {
setTransferring(false);
}
};
const cancelTransfer = () => {
toast.info('Transfer cancelled');
setModalOpen(false);
};
// 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'
});
};
return (
<Container
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2>Money Transfer</h2>
<FormContainer
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1, duration: 0.5 }}
>
<Select
name="fromAccountNumber"
value={form.fromAccountNumber}
onChange={handleInputChange}
disabled={accountsLoading}
>
<option value="">Select From Account</option>
{userAccounts.map(account => (
<option key={account.accountNumber} value={account.accountNumber}>
{account.accountNumber} ({account.accountType})
</option>
))}
</Select>
<Input
name="toAccountNumber"
value={form.toAccountNumber}
placeholder="To Account Number"
onChange={handleInputChange}
/>
<Input
name="amount"
value={form.amount}
type="number"
placeholder="Amount"
onChange={handleInputChange}
min="0"
step="0.01"
/>
<Input
name="description"
value={form.description}
placeholder="Description"
onChange={handleInputChange}
/>
<TransferButton
onClick={handleTransfer}
disabled={transferring || accountsLoading}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Send size={18} />
{transferring ? 'Processing...' : 'Transfer'}
</TransferButton>
</FormContainer>
<TableContainer>
{loading ? (
<LoadingSpinner
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<RefreshCw size={32} />
<p style={{ marginLeft: spacing.lg, fontSize: '18px', fontWeight: '500' }}>
Loading transfers...
</p>
</LoadingSpinner>
) : error ? (
<ErrorMessage
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<AlertCircle size={48} />
<h3>Something went wrong</h3>
<p>{error}</p>
<TransferButton
onClick={fetchTransactions}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw size={16} />
Try Again
</TransferButton>
</ErrorMessage>
) : transactions.length === 0 ? (
<EmptyState
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<Filter size={48} />
<h3>No transfers found</h3>
<p>Start your first transfer using the form above</p>
</EmptyState>
) : (
<Table>
<thead>
<tr>
<th>#</th>
<th>ID</th>
<th>From</th>
<th>To</th>
<th>Amount</th>
<th>Status</th>
<th>Date</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{transactions.map((transaction, idx) => (
<motion.tr
key={transaction.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: idx * 0.05 }}
>
<td>{idx + 1}</td>
<td>
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
T{String(transaction.id).padStart(3, '0')}
</code>
</td>
<td>
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.fromAccount?.accountNumber || '-'}
</code>
</td>
<td>
<code style={{
background: colors.shadowHover,
padding: '4px 8px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600'
}}>
{transaction.toAccount?.accountNumber || '-'}
</code>
</td>
<td style={{
fontWeight: '600',
color: colors.primary
}}>
{formatCurrency(transaction.amount)}
</td>
<td>
<StatusBadge status={transaction.status}>
{transaction.status}
</StatusBadge>
{transaction.new && <NewTag>new</NewTag>}
</td>
<td>{formatDate(transaction.date)}</td>
<td>{transaction.description}</td>
</motion.tr>
))}
</tbody>
</Table>
)}
</TableContainer>
<ConfirmModal
isOpen={modalOpen}
onConfirm={confirmTransfer}
onCancel={cancelTransfer}
title="Confirm Transfer"
message={`Transfer ${formatCurrency(parseFloat(form.amount) || 0)} from ${form.fromAccountNumber} to ${form.toAccountNumber}?`}
loading={transferring}
/>
</Container>
);
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)