DEV Community

Nebula
Nebula

Posted on

transfer page

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)