final-touches

This commit is contained in:
kirukib 2025-12-21 23:29:48 +03:00
parent 264f1e6159
commit 3d66182b95
47 changed files with 4793 additions and 733 deletions

View File

@ -4,6 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
<title>Amba Checkout</title> <title>Amba Checkout</title>
</head> </head>
<body> <body>

2602
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

1
public/404.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

View File

BIN
public/AmbaLogo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

View File

6
public/Logo.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86.42 80.35">
<g id="Layer_1-2" data-name="Layer 1">
<path d="m86.42,70.54h-8.94l-4.29-7.02c2.34-1.06,4.47-2.49,6.34-4.25l6.9,11.27Zm-3.16-28.6c0-10.99-8.94-19.92-19.92-19.92h-18.76l-25.06,40.9h-5.91L43.21,14.59l2.02,3.29h8.94L43.22,0,0,70.54h20.49c2.05,0,3.96-1.07,5.02-2.81l20.9-34.12,2.43-3.97h14.49c6.78,0,12.3,5.52,12.3,12.3,0,2.68-.86,5.16-2.33,7.18-1.51,2.1-3.67,3.7-6.18,4.51-1.2.39-2.47.6-3.8.6h-16.88c-3.48,0-6.77,1.85-8.59,4.81l-13.05,21.3h5.64c2.04,0,3.95-1.07,5.02-2.81l8.89-14.51c.44-.72,1.24-1.17,2.09-1.17h16.88c2.78,0,5.44-.57,7.85-1.61,2.37-1.02,4.51-2.49,6.31-4.31,3.57-3.6,5.77-8.56,5.77-14.01Zm-17.3,8.07c1.69-.55,3.21-1.65,4.25-3.11.34-.46.62-.95.84-1.46l-7.25-11.83h-8.94l10.19,16.64c.31-.05.61-.14.91-.24Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 877 B

View File

1
public/Maintenance.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

1
public/NoInternet.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

View File

View File

@ -1,32 +1,38 @@
import React from 'react'; import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { CheckoutProvider } from './contexts/CheckoutContext'; import { ChatButton } from './components/ui/ChatButton';
import { Layout } from './components/layout/Layout'; import { AccountSelectionPage } from './pages/AccountSelectionPage';
import { CheckoutPage } from './pages/CheckoutPage'; import { CheckoutPageRoute } from './pages/CheckoutPageRoute';
import { AdminPage } from './pages/AdminPage'; import { SuccessPageRoute } from './pages/SuccessPageRoute';
import { TransactionsPage } from './pages/TransactionsPage'; import { Error404 } from './components/errors/Error404';
import { SuspiciousPage } from './pages/SuspiciousPage'; import { NoInternet } from './components/errors/NoInternet';
import { AccountsPage } from './pages/AccountsPage'; import { useNetworkStatus } from './hooks/useNetworkStatus';
const AppRoutes: React.FC = () => {
const isOnline = useNetworkStatus();
const location = useLocation();
// Don't show NoInternet on the no-internet page itself to avoid infinite loop
if (!isOnline && location.pathname !== '/no-internet') {
return <NoInternet />;
}
return (
<Routes>
<Route path="/" element={<AccountSelectionPage />} />
<Route path="/checkout" element={<CheckoutPageRoute />} />
<Route path="/success" element={<SuccessPageRoute />} />
<Route path="/no-internet" element={<NoInternet />} />
<Route path="*" element={<Error404 />} />
</Routes>
);
};
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Layout> <AppRoutes />
<Routes> <ChatButton />
<Route
path="/"
element={
<CheckoutProvider>
<CheckoutPage />
</CheckoutProvider>
}
/>
<Route path="/admin" element={<AdminPage />} />
<Route path="/admin/transactions" element={<TransactionsPage />} />
<Route path="/admin/suspicious" element={<SuspiciousPage />} />
<Route path="/admin/accounts" element={<AccountsPage />} />
</Routes>
</Layout>
</BrowserRouter> </BrowserRouter>
); );
} }

View File

@ -4,6 +4,7 @@ import type { Account } from '../../types';
import { Card } from '../ui/Card'; import { Card } from '../ui/Card';
import { Button } from '../ui/Button'; import { Button } from '../ui/Button';
import { Input } from '../ui/Input'; import { Input } from '../ui/Input';
import { Loading } from '../ui/Loading';
import { FiFlag, FiX, FiSearch, FiUser, FiBriefcase } from 'react-icons/fi'; import { FiFlag, FiX, FiSearch, FiUser, FiBriefcase } from 'react-icons/fi';
export const AccountFlagging: React.FC = () => { export const AccountFlagging: React.FC = () => {
@ -75,11 +76,7 @@ export const AccountFlagging: React.FC = () => {
}; };
if (loading) { if (loading) {
return ( return <Loading fullScreen message="Loading accounts..." />;
<div className="flex items-center justify-center min-h-screen">
<div className="text-gray-600">Loading...</div>
</div>
);
} }
return ( return (

View File

@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { api } from '../../utils/api'; import { api } from '../../utils/api';
import type { Transaction, Account } from '../../types'; import type { Transaction, Account } from '../../types';
import { Card } from '../ui/Card'; import { Card } from '../ui/Card';
import { Loading } from '../ui/Loading';
import { FiDollarSign, FiAlertTriangle, FiCheckCircle, FiClock, FiUsers } from 'react-icons/fi'; import { FiDollarSign, FiAlertTriangle, FiCheckCircle, FiClock, FiUsers } from 'react-icons/fi';
export const AdminDashboard: React.FC = () => { export const AdminDashboard: React.FC = () => {
@ -47,11 +48,7 @@ export const AdminDashboard: React.FC = () => {
}; };
if (loading) { if (loading) {
return ( return <Loading fullScreen message="Loading dashboard..." />;
<div className="flex items-center justify-center min-h-screen">
<div className="text-gray-600">Loading...</div>
</div>
);
} }
return ( return (

View File

@ -3,6 +3,7 @@ import { api } from '../../utils/api';
import type { Transaction } from '../../types'; import type { Transaction } from '../../types';
import { Card } from '../ui/Card'; import { Card } from '../ui/Card';
import { Button } from '../ui/Button'; import { Button } from '../ui/Button';
import { Loading } from '../ui/Loading';
import { FiAlertTriangle, FiCheckCircle, FiXCircle } from 'react-icons/fi'; import { FiAlertTriangle, FiCheckCircle, FiXCircle } from 'react-icons/fi';
export const SuspiciousTransactions: React.FC = () => { export const SuspiciousTransactions: React.FC = () => {
@ -71,11 +72,7 @@ export const SuspiciousTransactions: React.FC = () => {
}; };
if (loading) { if (loading) {
return ( return <Loading fullScreen message="Loading suspicious transactions..." />;
<div className="flex items-center justify-center min-h-screen">
<div className="text-gray-600">Loading...</div>
</div>
);
} }
return ( return (

View File

@ -3,6 +3,7 @@ import { api } from '../../utils/api';
import type { Transaction } from '../../types'; import type { Transaction } from '../../types';
import { Card } from '../ui/Card'; import { Card } from '../ui/Card';
import { Button } from '../ui/Button'; import { Button } from '../ui/Button';
import { Loading } from '../ui/Loading';
import { FiSearch, FiFilter, FiEye } from 'react-icons/fi'; import { FiSearch, FiFilter, FiEye } from 'react-icons/fi';
export const TransactionList: React.FC = () => { export const TransactionList: React.FC = () => {
@ -89,11 +90,7 @@ export const TransactionList: React.FC = () => {
}; };
if (loading) { if (loading) {
return ( return <Loading fullScreen message="Loading transactions..." />;
<div className="flex items-center justify-center min-h-screen">
<div className="text-gray-600">Loading...</div>
</div>
);
} }
return ( return (

View File

@ -1,87 +0,0 @@
import React from 'react';
import { Card } from '../ui/Card';
import type { AccountType } from '../../types';
import { FiBriefcase, FiUser } from 'react-icons/fi';
interface AccountSelectionProps {
selectedAccountType?: AccountType;
onSelect: (accountType: AccountType) => void;
onNext?: () => void;
}
export const AccountSelection: React.FC<AccountSelectionProps> = ({
selectedAccountType,
onSelect,
onNext,
}) => {
return (
<div className="w-full">
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<div className="grid grid-cols-2 gap-2">
<div className="w-3 h-3 rounded-full bg-primary-500"></div>
<div className="w-3 h-3 rounded-full bg-primary-500"></div>
<div className="w-3 h-3 rounded-full bg-primary-500"></div>
<div className="w-3 h-3 rounded-full bg-primary-500"></div>
</div>
</div>
<h2 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2">
Choose your account type
</h2>
<p className="text-gray-600 text-sm md:text-base max-w-md mx-auto">
It is a long established fact that a reader will be distracted by the readable content of a page
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto mb-6">
<Card
selected={selectedAccountType === 'business'}
onClick={() => onSelect('business')}
className="p-6"
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-12 h-12 rounded-lg bg-primary-100 flex items-center justify-center">
<FiBriefcase className="w-6 h-6 text-primary-600" />
</div>
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-800 mb-1">Business</h3>
<p className="text-sm text-gray-600">Search or find a home</p>
</div>
</div>
</Card>
<Card
selected={selectedAccountType === 'personal'}
onClick={() => onSelect('personal')}
className="p-6"
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-12 h-12 rounded-lg bg-primary-100 flex items-center justify-center">
<FiUser className="w-6 h-6 text-primary-600" />
</div>
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-800 mb-1">Personal</h3>
<p className="text-sm text-gray-600">Post or share your home</p>
</div>
</div>
</Card>
</div>
{selectedAccountType && onNext && (
<div className="max-w-2xl mx-auto">
<button
onClick={onNext}
className="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium py-3 px-6 rounded-lg transition-colors"
>
Next Step
</button>
</div>
)}
</div>
);
};

View File

@ -1,163 +1,89 @@
import React from 'react'; import React, { useState } from 'react';
import { useCheckout } from '../../contexts/CheckoutContext'; import { useCheckout } from '../../contexts/CheckoutContext';
import { FundRequestForm } from './FundRequestForm'; import { RequesterDetails } from './RequesterDetails';
import { AccountSelection } from './AccountSelection'; import { CheckoutPage } from './CheckoutPage';
import { TransactionConfirmation } from './TransactionConfirmation'; import { Loading } from '../ui/Loading';
import { TransactionStatus } from './TransactionStatus';
import { PromotionalPanel } from './PromotionalPanel';
import { FiCheck } from 'react-icons/fi';
export const CheckoutFlow: React.FC = () => { export const CheckoutFlow: React.FC = () => {
const { const {
currentStep, requester,
selectedRecipientAccountId,
selectedRecipientAccount,
fundRequest, fundRequest,
selectedAccountType, transactionFee,
transaction, setSelectedRecipientAccountId,
isLoading, submitPayment,
error,
setFundRequest,
setAccountType,
submitTransaction,
reset,
goToStep,
} = useCheckout(); } = useCheckout();
const handleFundRequestSubmit = (data: any) => { const [showCheckout, setShowCheckout] = useState(false);
setFundRequest(data);
goToStep(2); const handleAccountSelect = (accountId: string) => {
setSelectedRecipientAccountId(accountId);
}; };
const handleAccountSelect = (accountType: 'business' | 'personal') => { const handleNext = () => {
setAccountType(accountType); // Ensure an account is selected before proceeding
if (!selectedRecipientAccountId && requester?.bankAccounts && requester.bankAccounts.length > 0) {
const defaultAccount = requester.bankAccounts.find(acc => acc.isDefault) || requester.bankAccounts[0];
if (defaultAccount) {
setSelectedRecipientAccountId(defaultAccount.id);
}
}
// Proceed to checkout page
setShowCheckout(true);
}; };
const handleConfirm = async () => { const handlePaymentComplete = async () => {
await submitTransaction(); await submitPayment();
}; };
const handleCancel = () => { if (!requester) {
goToStep(1); return <Loading fullScreen message="Loading payment request..." />;
}; }
const steps = [ // Show checkout page if user clicked next
{ number: 1, label: 'Request', title: 'Fund Request' }, if (showCheckout && requester && fundRequest) {
{ number: 2, label: 'Account', title: 'Account Type' }, // Get the selected account from the requester's bank accounts
{ number: 3, label: 'Review', title: 'Confirmation' }, const account = selectedRecipientAccount ||
{ number: 4, label: 'Status', title: 'Status' }, (selectedRecipientAccountId && requester.bankAccounts?.find(acc => acc.id === selectedRecipientAccountId)) ||
]; requester.bankAccounts?.[0];
if (!account) {
return <Loading fullScreen message="Loading account details..." />;
}
return (
<CheckoutPage
requester={{
name: requester.name,
requestAmount: requester.requestAmount,
description: requester.description,
}}
selectedAccount={{
bankName: account.bankName,
accountNumber: account.accountNumber,
}}
transactionFee={transactionFee}
onComplete={handlePaymentComplete}
/>
);
}
// Show requester details page (initial page)
return ( return (
<div className="min-h-screen bg-gray-50 py-4 md:py-8 px-4"> <RequesterDetails
<div className="max-w-7xl mx-auto"> requester={requester}
{/* Progress Indicator - Hidden on account selection step to match design */} recipientAccounts={requester.bankAccounts || []}
{currentStep !== 2 && ( selectedAccountId={selectedRecipientAccountId || undefined}
<div className="mb-8"> onSelectAccount={handleAccountSelect}
<div className="flex items-center justify-between mb-4"> onNext={handleNext}
{steps.map((step, index) => ( onFlag={(reason) => {
<React.Fragment key={step.number}> // Handle flagging - in real app, this would call an API
<div className="flex flex-col items-center flex-1"> console.log('Flagged user:', requester.id, 'Reason:', reason);
<div alert(`User has been reported. Reason: ${reason === 'spam' ? 'Spam or suspicious activity' : reason === 'unknown' ? "I don't know this person" : 'Other'}`);
className={` }}
w-10 h-10 rounded-full flex items-center justify-center />
transition-colors
${
currentStep > step.number
? 'bg-primary-500 text-white'
: currentStep === step.number
? 'bg-primary-500 text-white ring-4 ring-primary-200'
: 'bg-gray-200 text-gray-600'
}
`}
>
{currentStep > step.number ? (
<FiCheck className="w-5 h-5" />
) : (
<span className="text-sm font-medium">{step.number}</span>
)}
</div>
<span
className={`
mt-2 text-xs font-medium hidden sm:block
${
currentStep >= step.number
? 'text-primary-600'
: 'text-gray-500'
}
`}
>
{step.label}
</span>
</div>
{index < steps.length - 1 && (
<div
className={`
flex-1 h-1 mx-2 transition-colors
${
currentStep > step.number
? 'bg-primary-500'
: 'bg-gray-200'
}
`}
/>
)}
</React.Fragment>
))}
</div>
</div>
)}
{/* Error Display */}
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
{/* Step Content */}
{currentStep === 2 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-[600px] rounded-lg overflow-hidden shadow-lg">
<div className="hidden lg:block">
<PromotionalPanel />
</div>
<div className="bg-white p-6 md:p-8 lg:p-12 flex items-center">
<AccountSelection
selectedAccountType={selectedAccountType || undefined}
onSelect={handleAccountSelect}
onNext={() => goToStep(3)}
/>
</div>
</div>
) : (
<div className="bg-white rounded-lg shadow-sm p-6 md:p-8">
{currentStep === 1 && (
<FundRequestForm
onSubmit={handleFundRequestSubmit}
initialData={fundRequest || undefined}
/>
)}
{currentStep === 3 && fundRequest && selectedAccountType && (
<TransactionConfirmation
transactionData={{
...fundRequest,
accountType: selectedAccountType,
}}
onConfirm={handleConfirm}
onCancel={handleCancel}
isLoading={isLoading}
/>
)}
{currentStep === 4 && transaction && (
<TransactionStatus
transaction={transaction}
onNewTransaction={reset}
/>
)}
</div>
)}
</div>
</div>
); );
}; };

View File

@ -0,0 +1,633 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { Input } from '../ui/Input';
import { Card } from '../ui/Card';
import { Logo } from '../ui/Logo';
import { DonationSection } from './DonationSection';
import { FiLock, FiArrowLeft, FiEye, FiEyeOff, FiInfo, FiShield, FiCheck, FiCreditCard } from 'react-icons/fi';
interface CheckoutFormData {
email: string;
cardNumber: string;
expiryDate: string;
cvv: string;
cardName: string;
address: string;
agreeToTerms: boolean;
saveInfo: boolean;
}
interface CheckoutPageProps {
requester: {
name: string;
requestAmount: number;
description?: string;
};
selectedAccount: {
bankName: string;
accountNumber: string;
};
transactionFee: number;
onComplete: () => void;
}
export const CheckoutPage: React.FC<CheckoutPageProps> = ({
requester,
selectedAccount,
transactionFee,
onComplete,
}) => {
const [cardNumber, setCardNumber] = useState('');
const [showCardNumber, setShowCardNumber] = useState(false);
const [donationAmount, setDonationAmount] = useState(0);
const [referralCode, setReferralCode] = useState('');
const [paymentMethod, setPaymentMethod] = useState<'card' | 'googlepay' | 'applepay'>('card');
const navigate = useNavigate();
const [isProcessing, setIsProcessing] = useState(false);
const {
register,
handleSubmit,
watch,
formState: { errors, isSubmitting },
} = useForm<CheckoutFormData>({
defaultValues: {
email: '',
cardNumber: '',
expiryDate: '',
cvv: '',
cardName: '',
address: '',
agreeToTerms: false,
},
});
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
};
const subtotal = requester.requestAmount;
const total = subtotal + transactionFee + donationAmount;
const onSubmit = async (data: CheckoutFormData) => {
// Simulate payment processing
await new Promise(resolve => setTimeout(resolve, 1500));
// Navigate to success page with email and transaction details
const transactionId = `TXN-${Date.now()}`;
const totalAmount = subtotal + transactionFee + donationAmount;
navigate(`/success?email=${encodeURIComponent(data.email)}&amount=${totalAmount}&transactionId=${transactionId}`);
onComplete();
};
const emailValue = watch('email');
const handleGooglePay = async () => {
if (!emailValue) {
return;
}
setIsProcessing(true);
try {
// Simulate Google Pay processing
await new Promise(resolve => setTimeout(resolve, 1500));
const transactionId = `TXN-${Date.now()}`;
const totalAmount = subtotal + transactionFee + donationAmount;
// In real app, get email from Google Pay response
navigate(`/success?email=${encodeURIComponent(emailValue)}&amount=${totalAmount}&transactionId=${transactionId}`);
onComplete();
} finally {
setIsProcessing(false);
}
};
const handleApplePay = async () => {
if (!emailValue) {
return;
}
setIsProcessing(true);
try {
// Simulate Apple Pay processing
await new Promise(resolve => setTimeout(resolve, 1500));
const transactionId = `TXN-${Date.now()}`;
const totalAmount = subtotal + transactionFee + donationAmount;
// In real app, get email from Apple Pay response
navigate(`/success?email=${encodeURIComponent(emailValue)}&amount=${totalAmount}&transactionId=${transactionId}`);
onComplete();
} finally {
setIsProcessing(false);
}
};
const formatCardNumber = (value: string) => {
const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
const matches = v.match(/\d{4,16}/g);
const match = (matches && matches[0]) || '';
const parts = [];
for (let i = 0, len = match.length; i < len; i += 4) {
parts.push(match.substring(i, i + 4));
}
if (parts.length) {
return parts.join(' ');
} else {
return v;
}
};
const getCardBrand = (cardNumber: string) => {
const number = cardNumber.replace(/\s/g, '');
if (/^4/.test(number)) return 'visa';
if (/^5[1-5]/.test(number)) return 'mastercard';
if (/^3[47]/.test(number)) return 'amex';
return null;
};
return (
<div className="min-h-screen bg-gray-50">
<div className="grid grid-cols-1 lg:grid-cols-[1.2fr_1fr] h-screen">
{/* Left Column - Order Summary (Dark Green) - Takes more space */}
<div className="bg-primary-500 overflow-y-auto relative">
<div
className="absolute inset-0 opacity-10"
style={{
backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,255,255,.05) 10px, rgba(255,255,255,.05) 20px)',
}}
/>
<div className="relative w-full px-4 sm:px-6 lg:px-8 py-8 lg:py-12">
{/* Back Button & Logo */}
<div className="flex items-center gap-4 mb-8">
<button
onClick={() => navigate('/')}
className="text-white hover:text-gray-200 transition-colors"
aria-label="Go back to account selection"
>
<FiArrowLeft className="w-5 h-5" />
</button>
<Logo dark={true} />
</div>
{/* Transaction Title */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-white mb-2">
Payment to {requester.name}
</h1>
<p className="text-primary-100">
Complete your payment to proceed
</p>
</div>
{/* Main Price Display */}
<div className="mb-8">
<div className="flex items-baseline gap-1 mb-2">
<span className="text-5xl lg:text-6xl font-bold text-white">
${Math.floor(total)}
</span>
<span className="text-4xl lg:text-5xl font-bold text-primary-100">
.{((total % 1) * 100).toFixed(0).padStart(2, '0')}
</span>
</div>
<p className="text-primary-100 text-sm">Total amount</p>
</div>
{/* Selected Plan/Transaction Box */}
<div className="mb-8">
<Card className="p-4 bg-white/10 border-white/20 backdrop-blur-sm">
<div className="flex justify-between items-center">
<div>
<p className="font-medium text-white">
{requester.name} - Payment Request
</p>
<p className="text-sm text-primary-100">
{selectedAccount.bankName} {selectedAccount.accountNumber}
</p>
</div>
<p className="text-white font-semibold">
{formatCurrency(subtotal)}
</p>
</div>
</Card>
</div>
{/* Order Summary */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-white mb-4">Order Summary</h3>
<div className="space-y-3 mb-4">
<div className="flex justify-between text-sm">
<span className="text-primary-100">Subtotal</span>
<span className="text-white">{formatCurrency(subtotal)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-primary-100">Transaction Fee</span>
<span className="text-white">{formatCurrency(transactionFee)}</span>
</div>
{donationAmount > 0 && (
<div className="flex justify-between text-sm">
<span className="text-primary-100">Donation</span>
<span className="text-white">{formatCurrency(donationAmount)}</span>
</div>
)}
</div>
{/* Referral Code Input */}
<div className="mb-4">
<label className="block text-sm text-primary-100 mb-2">
Referral Code
</label>
<input
type="text"
placeholder="Enter referral code"
value={referralCode}
onChange={(e) => setReferralCode(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-primary-200 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-white/40"
/>
</div>
{/* Total Due */}
<div className="flex justify-between items-center pt-2">
<span className="text-lg font-bold text-white">Total due today</span>
<span className="text-2xl font-bold text-white">
{formatCurrency(total)}
</span>
</div>
</div>
{/* Footer */}
<div className="mt-auto pt-8 border-t border-white/20">
<p className="text-xs text-primary-100 mb-2">
©2023 All rights reserved
</p>
<div className="flex gap-4 text-xs">
<a href="#" className="text-primary-100 hover:text-white underline">
Terms
</a>
<a href="#" className="text-primary-100 hover:text-white underline">
Privacy
</a>
</div>
</div>
</div>
</div>
{/* Right Column - Payment Form (Light Gray) */}
<div className="bg-gray-50 overflow-y-auto">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Payment Method</h2>
{/* Payment Method Selector */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-6">
{/* Card Payment */}
<button
type="button"
onClick={() => setPaymentMethod('card')}
className={`p-4 border-2 rounded-lg transition-all ${
paymentMethod === 'card'
? 'border-primary-500 bg-primary-50'
: 'border-gray-200 hover:border-primary-300 bg-white'
}`}
>
<div className="flex flex-col items-center gap-2">
<FiCreditCard className={`w-6 h-6 ${paymentMethod === 'card' ? 'text-primary-500' : 'text-gray-600'}`} />
<span className={`text-sm font-medium ${paymentMethod === 'card' ? 'text-primary-600' : 'text-gray-700'}`}>
Card
</span>
</div>
</button>
{/* Google Pay */}
<button
type="button"
onClick={() => setPaymentMethod('googlepay')}
className={`p-4 border-2 rounded-lg transition-all ${
paymentMethod === 'googlepay'
? 'border-primary-500 bg-primary-50'
: 'border-gray-200 hover:border-primary-300 bg-white'
}`}
>
<div className="flex flex-col items-center gap-2">
<svg className={`w-6 h-6 ${paymentMethod === 'googlepay' ? 'text-primary-500' : 'text-gray-600'}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
<span className={`text-sm font-medium ${paymentMethod === 'googlepay' ? 'text-primary-600' : 'text-gray-700'}`}>
Google Pay
</span>
</div>
</button>
{/* Apple Pay */}
<button
type="button"
onClick={() => setPaymentMethod('applepay')}
className={`p-4 border-2 rounded-lg transition-all ${
paymentMethod === 'applepay'
? 'border-primary-500 bg-primary-50'
: 'border-gray-200 hover:border-primary-300 bg-white'
}`}
>
<div className="flex flex-col items-center gap-2">
<svg className={`w-6 h-6 ${paymentMethod === 'applepay' ? 'text-primary-500' : 'text-gray-600'}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
</svg>
<span className={`text-sm font-medium ${paymentMethod === 'applepay' ? 'text-primary-600' : 'text-gray-700'}`}>
Apple Pay
</span>
</div>
</button>
</div>
{/* Payment Method Content */}
{paymentMethod === 'card' && (
<>
{/* Email */}
<div>
<Input
label="Email"
type="email"
placeholder="Email"
error={errors.email?.message}
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
</div>
{/* Card Details */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Card details
</label>
<div className="space-y-4">
{/* Card Number */}
<div className="relative">
<input
type={showCardNumber ? 'text' : 'password'}
placeholder="1234 1234 1234 1234"
maxLength={19}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent pr-20 ${
errors.cardNumber ? 'border-red-500' : 'border-gray-300'
}`}
{...register('cardNumber', {
required: 'Card number is required',
onChange: (e) => {
const formatted = formatCardNumber(e.target.value);
e.target.value = formatted;
setCardNumber(formatted);
},
})}
/>
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center gap-2">
{getCardBrand(cardNumber) === 'visa' && (
<span className="text-xs font-semibold text-blue-600">VISA</span>
)}
{getCardBrand(cardNumber) === 'mastercard' && (
<span className="text-xs font-semibold text-red-600">MC</span>
)}
{getCardBrand(cardNumber) === 'amex' && (
<span className="text-xs font-semibold text-blue-600">AMEX</span>
)}
{!getCardBrand(cardNumber) && (
<div className="flex gap-1">
<div className="w-6 h-4 bg-blue-600 rounded"></div>
<div className="w-6 h-4 bg-yellow-500 rounded"></div>
<div className="w-6 h-4 bg-blue-500 rounded"></div>
</div>
)}
<button
type="button"
onClick={() => setShowCardNumber(!showCardNumber)}
className="text-gray-400 hover:text-gray-600"
>
{showCardNumber ? <FiEyeOff className="w-5 h-5" /> : <FiEye className="w-5 h-5" />}
</button>
</div>
</div>
{errors.cardNumber && (
<p className="text-sm text-red-600">{errors.cardNumber.message}</p>
)}
{/* Expiry and CVC */}
<div className="grid grid-cols-2 gap-4">
<div>
<input
type="text"
placeholder="MM/YY"
maxLength={5}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent ${
errors.expiryDate ? 'border-red-500' : 'border-gray-300'
}`}
{...register('expiryDate', {
required: 'Expiry date is required',
})}
/>
{errors.expiryDate && (
<p className="text-sm text-red-600 mt-1">{errors.expiryDate.message}</p>
)}
</div>
<div className="relative">
<input
type="text"
placeholder="CVC"
maxLength={4}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent pr-10 ${
errors.cvv ? 'border-red-500' : 'border-gray-300'
}`}
{...register('cvv', {
required: 'CVC is required',
})}
/>
<FiLock className="absolute right-3 top-1/2 transform -translate-y-1/2 text-secondary-500" />
{errors.cvv && (
<p className="text-sm text-red-600 mt-1">{errors.cvv.message}</p>
)}
</div>
</div>
</div>
</div>
{/* Name on card */}
<div>
<Input
label="Name on card"
type="text"
placeholder="Name on card"
error={errors.cardName?.message}
{...register('cardName', {
required: 'Name on card is required',
})}
/>
</div>
{/* Divider */}
<div className="border-t border-gray-200 my-6"></div>
{/* Billing Address */}
<div>
<Input
label="Billing address"
type="text"
placeholder="Address"
error={errors.address?.message}
{...register('address', {
required: 'Address is required',
})}
/>
</div>
{/* Donation Section */}
<DonationSection
onDonationChange={(amount) => setDonationAmount(amount)}
/>
{/* Terms Checkbox */}
<div className="flex items-start gap-3">
<input
type="checkbox"
id="agreeToTerms"
{...register('agreeToTerms', {
required: 'You must agree to the terms and conditions',
})}
className="mt-1 w-4 h-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
/>
<label htmlFor="agreeToTerms" className="text-sm text-gray-700">
By confirming your payment, you allow us to charge your card for this and future payments in accordance with terms. You can always cancel your subscription.
</label>
</div>
{errors.agreeToTerms && (
<p className="text-sm text-red-600 -mt-2">{errors.agreeToTerms.message}</p>
)}
{/* Subscribe/Pay Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium py-4 px-6 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Processing...' : 'Pay'}
</button>
</>
)}
{/* Google Pay Button */}
{paymentMethod === 'googlepay' && (
<>
<div>
<Input
label="Email"
type="email"
placeholder="Email"
error={errors.email?.message}
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
</div>
<button
type="button"
onClick={handleGooglePay}
disabled={isProcessing || !emailValue}
className="w-full bg-gray-900 hover:bg-gray-800 text-white font-medium py-4 px-6 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-3"
>
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
<span>{isProcessing ? 'Processing...' : 'Pay with Google Pay'}</span>
</button>
</>
)}
{/* Apple Pay Button */}
{paymentMethod === 'applepay' && (
<>
<div>
<Input
label="Email"
type="email"
placeholder="Email"
error={errors.email?.message}
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
</div>
<button
type="button"
onClick={handleApplePay}
disabled={isProcessing || !emailValue}
className="w-full bg-black hover:bg-gray-900 text-white font-medium py-4 px-6 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-3"
>
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
</svg>
<span>{isProcessing ? 'Processing...' : 'Pay with Apple Pay'}</span>
</button>
</>
)}
{/* Common sections for all payment methods */}
{(paymentMethod === 'googlepay' || paymentMethod === 'applepay') && (
<>
{/* Donation Section */}
<DonationSection
onDonationChange={(amount) => setDonationAmount(amount)}
/>
{/* Terms Checkbox */}
<div className="flex items-start gap-3">
<input
type="checkbox"
id="agreeToTermsAlt"
{...register('agreeToTerms', {
required: 'You must agree to the terms and conditions',
})}
className="mt-1 w-4 h-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
/>
<label htmlFor="agreeToTermsAlt" className="text-sm text-gray-700">
By confirming your payment, you allow us to charge your card for this and future payments in accordance with terms. You can always cancel your subscription.
</label>
</div>
{errors.agreeToTerms && (
<p className="text-sm text-red-600 -mt-2">{errors.agreeToTerms.message}</p>
)}
</>
)}
{/* Verified Checkout Badge */}
<div className="flex items-center justify-center gap-2 pt-4 border-t border-gray-200">
<div className="flex items-center gap-2 text-sm text-gray-600">
<FiShield className="w-5 h-5 text-secondary-500" />
<span className="font-medium">Verified checkout page by</span>
<span className="font-bold text-primary-500">AmbaPay</span>
<FiCheck className="w-4 h-4 text-secondary-500" />
</div>
</div>
</form>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,216 @@
import React, { useState, useRef, useEffect } from "react";
import { FiHeart, FiX, FiChevronLeft, FiChevronRight } from "react-icons/fi";
interface DonationCause {
id: string;
name: string;
description: string;
image?: string;
category: string;
}
interface DonationSectionProps {
onDonationChange: (amount: number, causeId?: string) => void;
}
const mockCauses: DonationCause[] = [
{
id: "1",
name: "Education for All",
description: "Supporting children's education in underserved communities",
category: "Education",
},
{
id: "2",
name: "Clean Water Initiative",
description: "Providing access to clean drinking water",
category: "Health",
},
{
id: "3",
name: "Wildlife Conservation",
description: "Protecting endangered species and their habitats",
category: "Environment",
},
{
id: "4",
name: "Medical Relief",
description: "Emergency medical assistance for those in need",
category: "Health",
},
];
export const DonationSection: React.FC<DonationSectionProps> = ({
onDonationChange,
}) => {
const [donationAmount, setDonationAmount] = useState("");
const [selectedCause, setSelectedCause] = useState<DonationCause | null>(
null
);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftButton, setShowLeftButton] = useState(false);
const [showRightButton, setShowRightButton] = useState(true);
const handleAmountChange = (value: string) => {
setDonationAmount(value);
const amount = parseFloat(value) || 0;
onDonationChange(amount, selectedCause?.id);
};
const handleCauseSelect = (cause: DonationCause | null) => {
setSelectedCause(cause);
const amount = parseFloat(donationAmount) || 0;
onDonationChange(amount, cause?.id);
};
const checkScrollButtons = () => {
if (scrollContainerRef.current) {
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
setShowLeftButton(scrollLeft > 0);
setShowRightButton(scrollLeft < scrollWidth - clientWidth - 10);
}
};
useEffect(() => {
checkScrollButtons();
const container = scrollContainerRef.current;
if (container) {
container.addEventListener("scroll", checkScrollButtons);
window.addEventListener("resize", checkScrollButtons);
return () => {
container.removeEventListener("scroll", checkScrollButtons);
window.removeEventListener("resize", checkScrollButtons);
};
}
}, []);
const scrollLeft = () => {
if (scrollContainerRef.current) {
const cardWidth =
scrollContainerRef.current.querySelector('div[style*="width"]')
?.clientWidth || 250;
scrollContainerRef.current.scrollBy({
left: -(cardWidth + 12),
behavior: "smooth",
});
}
};
const scrollRight = () => {
if (scrollContainerRef.current) {
const cardWidth =
scrollContainerRef.current.querySelector('div[style*="width"]')
?.clientWidth || 250;
scrollContainerRef.current.scrollBy({
left: cardWidth + 12,
behavior: "smooth",
});
}
};
// Add "None" option to causes
const allCauses = [
{ id: "none", name: "None", description: "No donation", category: "None" },
...mockCauses,
];
return (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Add donation to institution
</label>
<input
type="number"
min="0"
step="0.01"
placeholder="0.00"
value={donationAmount}
onChange={(e) => handleAmountChange(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 mb-4"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Cause
</label>
<div className="relative">
{/* Left Scroll Button */}
{showLeftButton && (
<button
onClick={scrollLeft}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 w-10 h-10 bg-white border border-gray-300 rounded-full shadow-lg flex items-center justify-center hover:bg-gray-50 transition-colors"
aria-label="Scroll left"
>
<FiChevronLeft className="w-5 h-5 text-gray-700" />
</button>
)}
{/* Scrollable Container */}
<div
ref={scrollContainerRef}
className="overflow-x-auto scrollbar-hide -mx-4 px-4"
style={{
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
onScroll={checkScrollButtons}
>
<div className="flex gap-3 pb-2" style={{ width: "max-content" }}>
{allCauses.map((cause) => (
<div
key={cause.id}
onClick={() =>
handleCauseSelect(cause.id === "none" ? null : cause)
}
className={`p-3 cursor-pointer transition-colors flex-shrink-0 border rounded-lg ${
selectedCause?.id === cause.id ||
(cause.id === "none" && selectedCause === null)
? "border-primary-500 border-2 bg-primary-50"
: "border-gray-200 hover:border-primary-300 bg-white"
}`}
style={{
width: "calc(50vw - 2rem)",
minWidth: "200px",
maxWidth: "250px",
}}
>
<div className="flex items-start gap-2">
<div className="w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center flex-shrink-0">
{cause.id === "none" ? (
<FiX className="w-4 h-4 text-gray-600" />
) : (
<FiHeart className="w-4 h-4 text-primary-600" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{cause.name}
</p>
<p className="text-xs text-gray-600 line-clamp-2">
{cause.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
{/* Right Scroll Button */}
{showRightButton && (
<button
onClick={scrollRight}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 w-10 h-10 bg-white border border-gray-300 rounded-full shadow-lg flex items-center justify-center hover:bg-gray-50 transition-colors"
aria-label="Scroll right"
>
<FiChevronRight className="w-5 h-5 text-gray-700" />
</button>
)}
</div>
</div>
</div>
);
};

View File

@ -1,111 +0,0 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { Input } from '../ui/Input';
import { Button } from '../ui/Button';
import type { FundRequest } from '../../types';
interface FundRequestFormProps {
onSubmit: (data: FundRequest) => void;
initialData?: Partial<FundRequest>;
}
export const FundRequestForm: React.FC<FundRequestFormProps> = ({
onSubmit,
initialData,
}) => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FundRequest>({
defaultValues: initialData,
});
return (
<div className="w-full max-w-md mx-auto">
<div className="text-center mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2">
Request Funds Transfer
</h2>
<p className="text-gray-600 text-sm md:text-base">
Enter the details for the funds you want to transfer
</p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<Input
label="Amount"
type="number"
step="0.01"
min="0.01"
placeholder="0.00"
error={errors.amount?.message}
{...register('amount', {
required: 'Amount is required',
min: { value: 0.01, message: 'Amount must be greater than 0' },
valueAsNumber: true,
})}
/>
<Input
label="Recipient Name"
type="text"
placeholder="John Doe"
error={errors.recipient?.message}
{...register('recipient', {
required: 'Recipient name is required',
minLength: { value: 2, message: 'Name must be at least 2 characters' },
})}
/>
<Input
label="Recipient Email"
type="email"
placeholder="recipient@example.com"
error={errors.recipientEmail?.message}
{...register('recipientEmail', {
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
<div>
<label
htmlFor="description"
className="block text-sm font-medium text-gray-700 mb-1"
>
Description (Optional)
</label>
<textarea
id="description"
rows={4}
placeholder="Add a note about this transaction..."
className={`
w-full px-4 py-2 border rounded-lg
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent
transition-colors
${errors.description ? 'border-red-500' : 'border-gray-300'}
`}
{...register('description')}
/>
{errors.description && (
<p className="mt-1 text-sm text-red-600">{errors.description.message}</p>
)}
</div>
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
isLoading={isSubmitting}
>
Continue
</Button>
</form>
</div>
);
};

View File

@ -1,28 +1,93 @@
import React from 'react'; import React from 'react';
import { Logo } from '../ui/Logo';
import { FiFacebook, FiTwitter, FiInstagram, FiLinkedin, FiSmartphone } from 'react-icons/fi';
export const PromotionalPanel: React.FC = () => { export const PromotionalPanel: React.FC = () => {
return ( return (
<div className="bg-primary-500 h-full flex flex-col justify-between p-8 md:p-12 text-white"> <div className="bg-primary-500 h-full flex flex-col justify-between p-8 md:p-12 text-white relative">
<div> <div
<div className="mb-8"> className="absolute inset-0 opacity-10"
<div className="flex items-center gap-2"> style={{
<div className="w-4 h-4 bg-white rounded"></div> backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,255,255,.05) 10px, rgba(255,255,255,.05) 20px)',
<span className="text-xl font-bold">sanswap</span> }}
</div> />
<div className="relative flex flex-col h-full">
<div className="mb-8 flex justify-center">
<Logo dark={true} className="h-16 md:h-20 lg:h-24" />
</div> </div>
<div className="flex-1 flex flex-col justify-center"> <div className="flex-1 flex flex-col justify-center items-center text-center">
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4"> <h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold mb-6 max-w-2xl">
Start searching for homes or posting ads now. Start searching for homes or posting ads now.
</h1> </h1>
<p className="text-primary-100 text-sm md:text-base max-w-md"> <p className="text-primary-100 text-base md:text-lg max-w-xl">
Interfaces are well designed for all ages and target audiences are extremely simple and work with social media integrations Interfaces are well designed for all ages and target audiences are extremely simple and work with social media integrations
</p> </p>
</div> </div>
</div> <div className="mt-auto">
<div className="flex gap-2 justify-center"> {/* Download App Buttons */}
<div className="w-2 h-2 bg-white rounded-full"></div> <div className="mb-6">
<div className="w-2 h-2 bg-white rounded-full"></div> <p className="text-sm text-primary-100 text-center mb-3">Download the Amba App</p>
<div className="w-2 h-2 bg-white rounded-full"></div> <div className="flex gap-3 justify-center">
<button
onClick={() => window.open('https://apps.apple.com/app/amba', '_blank')}
className="flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors border border-white/20"
>
<FiSmartphone className="w-4 h-4" />
<span className="text-sm font-medium">iOS</span>
</button>
<button
onClick={() => window.open('https://play.google.com/store/apps/details?id=com.amba', '_blank')}
className="flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors border border-white/20"
>
<FiSmartphone className="w-4 h-4" />
<span className="text-sm font-medium">Android</span>
</button>
</div>
</div>
{/* Social Links */}
<div className="flex gap-4 justify-center mb-6">
<a
href="#"
className="w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
aria-label="Facebook"
>
<FiFacebook className="w-5 h-5" />
</a>
<a
href="#"
className="w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
aria-label="Twitter"
>
<FiTwitter className="w-5 h-5" />
</a>
<a
href="#"
className="w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
aria-label="Instagram"
>
<FiInstagram className="w-5 h-5" />
</a>
<a
href="#"
className="w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
aria-label="LinkedIn"
>
<FiLinkedin className="w-5 h-5" />
</a>
</div>
{/* Footer Links */}
<div className="flex gap-4 justify-center text-sm text-primary-100 pb-4">
<a href="#" className="hover:text-white underline">
Terms
</a>
<span className="text-primary-200"></span>
<a href="#" className="hover:text-white underline">
Privacy Policy
</a>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,396 @@
import React, { useEffect, useState } from "react";
import { Card } from "../ui/Card";
import { PromotionalPanel } from "./PromotionalPanel";
import type { RecipientBankAccount } from "../../types";
import {
FiBriefcase,
FiUser,
FiCreditCard,
FiFlag,
FiGlobe,
FiChevronDown,
} from "react-icons/fi";
interface RequesterDetailsProps {
requester: {
id: string;
name: string;
email: string;
phone?: string;
requestAmount: number;
description?: string;
};
recipientAccounts: RecipientBankAccount[];
selectedAccountId?: string;
onSelectAccount: (accountId: string) => void;
onNext: () => void;
onFlag?: (reason: string) => void;
}
export const RequesterDetails: React.FC<RequesterDetailsProps> = ({
requester,
recipientAccounts,
selectedAccountId,
onSelectAccount,
onNext,
onFlag,
}) => {
const [showFlagModal, setShowFlagModal] = useState(false);
const [flagReason, setFlagReason] = useState("");
const [showLanguageMenu, setShowLanguageMenu] = useState(false);
const [selectedLanguage, setSelectedLanguage] = useState("English");
const languages = [
{ code: "en", name: "English" },
{ code: "fr", name: "French" },
{ code: "am", name: "Amharic" },
{ code: "om", name: "Oromo" },
{ code: "ti", name: "Tigrinya" },
];
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
// Auto-select first account if none selected and accounts exist
useEffect(() => {
if (!selectedAccountId && recipientAccounts.length > 0) {
const defaultAccount =
recipientAccounts.find((acc) => acc.isDefault) || recipientAccounts[0];
if (defaultAccount) {
onSelectAccount(defaultAccount.id);
}
}
}, [selectedAccountId, recipientAccounts]);
return (
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-2">
<div className="hidden lg:block">
<PromotionalPanel />
</div>
<div className="bg-white p-6 md:p-8 lg:p-12 overflow-y-auto">
{/* Requester Info */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
<h2 className="text-lg sm:text-xl font-bold text-gray-800 break-words">
Payment Request
</h2>
<div className="flex items-center gap-2 flex-shrink-0">
{/* Language Selector */}
<div className="relative">
<button
onClick={() => setShowLanguageMenu(!showLanguageMenu)}
className="flex items-center gap-2 px-4 py-2 bg-gray-50 hover:bg-gray-100 text-gray-700 rounded-lg transition-colors border border-gray-200"
>
<FiGlobe className="w-4 h-4 text-secondary-500" />
<span className="font-medium text-sm">
{selectedLanguage}
</span>
<FiChevronDown
className={`w-4 h-4 transition-transform ${
showLanguageMenu ? "rotate-180" : ""
}`}
/>
</button>
{showLanguageMenu && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setShowLanguageMenu(false)}
/>
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-20 py-1">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => {
setSelectedLanguage(lang.name);
setShowLanguageMenu(false);
// Here you would typically update the app language
// For now, we'll just update the selected language
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-50 transition-colors ${
selectedLanguage === lang.name
? "bg-primary-50 text-primary-600 font-medium"
: "text-gray-700"
}`}
>
{lang.name}
</button>
))}
</div>
</>
)}
</div>
{/* Report Button */}
{onFlag && (
<button
onClick={() => setShowFlagModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg transition-colors border border-red-200"
>
<FiFlag className="w-4 h-4" />
<span className="font-medium">Report</span>
</button>
)}
</div>
</div>
<Card className="p-4 mb-4">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
<span className="text-primary-600 font-bold text-lg">
{requester.name.charAt(0).toUpperCase()}
</span>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-900 text-lg mb-1 truncate">
{requester.name}
</h3>
<p className="text-sm text-gray-600 mb-1 truncate">
{requester.email}
</p>
{requester.phone && (
<p className="text-sm text-gray-600 truncate">
{requester.phone}
</p>
)}
</div>
<div className="text-right flex-shrink-0">
<p className="text-sm text-gray-600 mb-1 whitespace-nowrap">
Requesting
</p>
<p className="text-xl sm:text-2xl font-bold text-primary-600 whitespace-nowrap">
{formatCurrency(requester.requestAmount)}
</p>
</div>
</div>
{requester.description && (
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">{requester.description}</p>
</div>
)}
</Card>
</div>
{/* Line Divider */}
<div className="mb-6">
<div className="h-px bg-gray-200"></div>
</div>
{/* Recipient Account Selection */}
{recipientAccounts.length > 1 ? (
<div>
<div className="text-left mb-6">
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-800 mb-2 break-words">
Choose recipient account
</h2>
<p className="text-gray-600 text-sm md:text-base break-words">
Select which account to send the money to
</p>
</div>
<div className="grid grid-cols-1 gap-4 mb-6">
{recipientAccounts.map((account) => (
<Card
key={account.id}
selected={selectedAccountId === account.id}
onClick={() => onSelectAccount(account.id)}
className="p-6 cursor-pointer"
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-12 h-12 rounded-lg bg-primary-100 flex items-center justify-center">
{account.accountType === "business" ? (
<FiBriefcase className="w-6 h-6 text-secondary-500" />
) : (
<FiUser className="w-6 h-6 text-secondary-500" />
)}
</div>
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-bold text-gray-800">
{account.accountHolderName}
</h3>
<span className="text-sm font-medium text-primary-600">
{account.bankName}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-1">
<FiCreditCard className="w-4 h-4 text-secondary-500" />
<span>{account.accountNumber}</span>
</div>
<span className="capitalize">
{account.accountType}
</span>
</div>
</div>
</div>
</Card>
))}
</div>
</div>
) : recipientAccounts.length === 1 ? (
<div>
<div className="text-left mb-6">
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-800 mb-2 break-words">
Recipient Account
</h2>
<p className="text-gray-600 text-sm md:text-base break-words">
Money will be sent to this account
</p>
</div>
<Card className="p-6 mb-6 border-primary-500 border-2">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-12 h-12 rounded-lg bg-primary-100 flex items-center justify-center">
{recipientAccounts[0].accountType === "business" ? (
<FiBriefcase className="w-6 h-6 text-secondary-500" />
) : (
<FiUser className="w-6 h-6 text-secondary-500" />
)}
</div>
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-bold text-gray-800">
{recipientAccounts[0].accountHolderName}
</h3>
<span className="text-sm font-medium text-primary-600">
{recipientAccounts[0].bankName}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-1">
<FiCreditCard className="w-4 h-4 text-secondary-500" />
<span>{recipientAccounts[0].accountNumber}</span>
</div>
<span className="capitalize">
{recipientAccounts[0].accountType}
</span>
</div>
</div>
</div>
</Card>
</div>
) : null}
{/* Next Button */}
{recipientAccounts.length > 0 && (
<button
onClick={() => {
// Auto-select if only one account exists and none is selected
if (recipientAccounts.length === 1 && !selectedAccountId) {
onSelectAccount(recipientAccounts[0].id);
}
// Call onNext directly - it will handle account selection if needed
onNext();
}}
className="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium py-3 px-6 rounded-lg transition-colors"
>
Next Step
</button>
)}
{/* Flag/Report Modal */}
{showFlagModal && onFlag && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<Card className="max-w-md w-full">
<div className="p-6">
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
Report User
</h2>
<button
onClick={() => {
setShowFlagModal(false);
setFlagReason("");
}}
className="text-gray-400 hover:text-gray-600"
>
×
</button>
</div>
<div className="mb-4">
<p className="text-sm text-gray-600 mb-4">
Why are you reporting{" "}
<span className="font-medium">{requester.name}</span>?
</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="flagReason"
value="spam"
checked={flagReason === "spam"}
onChange={(e) => setFlagReason(e.target.value)}
className="w-4 h-4 text-primary-600"
/>
<span className="text-sm text-gray-900">
Spam or suspicious activity
</span>
</label>
<label className="flex items-center gap-3 p-3 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="flagReason"
value="unknown"
checked={flagReason === "unknown"}
onChange={(e) => setFlagReason(e.target.value)}
className="w-4 h-4 text-primary-600"
/>
<span className="text-sm text-gray-900">
I don't know this person
</span>
</label>
<label className="flex items-center gap-3 p-3 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="flagReason"
value="other"
checked={flagReason === "other"}
onChange={(e) => setFlagReason(e.target.value)}
className="w-4 h-4 text-primary-600"
/>
<span className="text-sm text-gray-900">Other</span>
</label>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => {
if (flagReason) {
onFlag(flagReason);
setShowFlagModal(false);
setFlagReason("");
}
}}
disabled={!flagReason}
className="flex-1 bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Report
</button>
<button
onClick={() => {
setShowFlagModal(false);
setFlagReason("");
}}
className="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition-colors"
>
Cancel
</button>
</div>
</div>
</Card>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,144 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from '../ui/Card';
import { Logo } from '../ui/Logo';
import { FiCheckCircle, FiMail, FiSmartphone, FiDownload } from 'react-icons/fi';
interface SuccessPageProps {
transactionId: string;
amount: number;
email: string;
}
export const SuccessPage: React.FC<SuccessPageProps> = ({
transactionId,
amount,
email,
}) => {
const [emailSent, setEmailSent] = useState(false);
const navigate = useNavigate();
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
};
const handleSendEmail = async () => {
// Simulate sending email
await new Promise(resolve => setTimeout(resolve, 1000));
setEmailSent(true);
// In real app, this would call an API to send the download link via email
console.log('Sending download link to:', email);
};
const handleDownloadIOS = () => {
// In real app, this would redirect to App Store
window.open('https://apps.apple.com/app/amba', '_blank');
};
const handleDownloadAndroid = () => {
// In real app, this would redirect to Google Play Store
window.open('https://play.google.com/store/apps/details?id=com.amba', '_blank');
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="max-w-2xl w-full">
<Card className="p-8 md:p-12">
{/* Success Icon */}
<div className="text-center mb-8">
<div className="w-20 h-20 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
<FiCheckCircle className="w-12 h-12 text-primary-500" />
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Payment Successful!
</h1>
<p className="text-gray-600 mb-1">
Your payment of {formatCurrency(amount)} has been processed successfully.
</p>
<p className="text-sm text-gray-500">Transaction ID: {transactionId}</p>
</div>
{/* Download App Section */}
<div className="mb-8">
<h2 className="text-xl font-bold text-gray-900 mb-4 text-center">
Download the Amba App
</h2>
<p className="text-sm text-gray-600 text-center mb-6">
Get the Amba app to manage your payments and transactions on the go.
</p>
{/* Download Buttons */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
<button
onClick={handleDownloadIOS}
className="flex items-center justify-center gap-3 px-6 py-4 bg-gray-900 hover:bg-gray-800 text-white rounded-lg transition-colors"
>
<FiSmartphone className="w-6 h-6" />
<div className="text-left">
<p className="text-xs">Download on the</p>
<p className="font-semibold">App Store</p>
</div>
</button>
<button
onClick={handleDownloadAndroid}
className="flex items-center justify-center gap-3 px-6 py-4 bg-gray-900 hover:bg-gray-800 text-white rounded-lg transition-colors"
>
<FiSmartphone className="w-6 h-6" />
<div className="text-left">
<p className="text-xs">Get it on</p>
<p className="font-semibold">Google Play</p>
</div>
</button>
</div>
{/* Email Link Section */}
<div className="border-t border-gray-200 pt-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<FiMail className="w-5 h-5 text-primary-500" />
<div>
<p className="font-medium text-gray-900">Send download link via email</p>
<p className="text-sm text-gray-600">{email}</p>
</div>
</div>
<button
onClick={handleSendEmail}
disabled={emailSent}
className="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
>
{emailSent ? 'Sent!' : 'Send Link'}
</button>
</div>
{emailSent && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 text-sm text-green-800">
Download link has been sent to your email!
</div>
)}
</div>
</div>
{/* Footer Actions */}
<div className="flex gap-4">
<button
onClick={() => navigate('/')}
className="flex-1 px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors font-medium"
>
Back to Home
</button>
<button
onClick={() => window.print()}
className="px-6 py-3 bg-primary-500 hover:bg-primary-600 text-white rounded-lg transition-colors font-medium flex items-center gap-2"
>
<FiDownload className="w-4 h-4" />
Download Receipt
</button>
</div>
</Card>
</div>
</div>
);
};

View File

@ -1,114 +0,0 @@
import React from 'react';
import { Button } from '../ui/Button';
import { Card } from '../ui/Card';
import type { FundRequest } from '../../types';
import { FiCheckCircle, FiXCircle, FiAlertTriangle } from 'react-icons/fi';
interface TransactionConfirmationProps {
transactionData: FundRequest & { accountType: 'business' | 'personal' };
onConfirm: () => void;
onCancel: () => void;
isLoading?: boolean;
}
export const TransactionConfirmation: React.FC<TransactionConfirmationProps> = ({
transactionData,
onConfirm,
onCancel,
isLoading = false,
}) => {
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
};
return (
<div className="w-full max-w-2xl mx-auto">
<div className="text-center mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2">
Review Transaction
</h2>
<p className="text-gray-600 text-sm md:text-base">
Please review the details before confirming
</p>
</div>
<Card className="p-6 md:p-8 mb-6">
<div className="space-y-4">
<div className="flex justify-between items-center pb-4 border-b border-gray-200">
<span className="text-gray-600">Amount</span>
<span className="text-2xl font-bold text-gray-800">
{formatCurrency(transactionData.amount)}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
<div>
<p className="text-sm text-gray-600 mb-1">Recipient</p>
<p className="font-medium text-gray-800">{transactionData.recipient}</p>
</div>
{transactionData.recipientEmail && (
<div>
<p className="text-sm text-gray-600 mb-1">Email</p>
<p className="font-medium text-gray-800">{transactionData.recipientEmail}</p>
</div>
)}
<div>
<p className="text-sm text-gray-600 mb-1">Account Type</p>
<p className="font-medium text-gray-800 capitalize">
{transactionData.accountType}
</p>
</div>
{transactionData.description && (
<div className="md:col-span-2">
<p className="text-sm text-gray-600 mb-1">Description</p>
<p className="font-medium text-gray-800">{transactionData.description}</p>
</div>
)}
</div>
</div>
</Card>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex items-start gap-3">
<FiAlertTriangle className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-yellow-800 mb-1">
Transaction Review
</p>
<p className="text-sm text-yellow-700">
This transaction will be reviewed for suspicious activity. You will be notified once the review is complete.
</p>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<Button
variant="outline"
size="lg"
className="flex-1"
onClick={onCancel}
disabled={isLoading}
>
Cancel
</Button>
<Button
variant="primary"
size="lg"
className="flex-1"
onClick={onConfirm}
isLoading={isLoading}
>
Confirm Transaction
</Button>
</div>
</div>
);
};

View File

@ -1,156 +0,0 @@
import React from 'react';
import { Button } from '../ui/Button';
import { Card } from '../ui/Card';
import type { Transaction } from '../../types';
import { FiCheckCircle, FiXCircle, FiClock, FiAlertCircle } from 'react-icons/fi';
interface TransactionStatusProps {
transaction: Transaction;
onNewTransaction?: () => void;
}
export const TransactionStatus: React.FC<TransactionStatusProps> = ({
transaction,
onNewTransaction,
}) => {
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
};
const getStatusConfig = () => {
switch (transaction.status) {
case 'approved':
return {
icon: FiCheckCircle,
title: 'Transaction Approved',
message: 'Your transaction has been successfully processed and approved.',
color: 'text-green-600',
bgColor: 'bg-green-50',
borderColor: 'border-green-200',
iconColor: 'text-green-600',
};
case 'rejected':
return {
icon: FiXCircle,
title: 'Transaction Rejected',
message: transaction.rejectionReason || 'Your transaction has been flagged as suspicious and rejected.',
color: 'text-red-600',
bgColor: 'bg-red-50',
borderColor: 'border-red-200',
iconColor: 'text-red-600',
};
case 'suspicious':
return {
icon: FiAlertCircle,
title: 'Under Review',
message: 'Your transaction is being reviewed for suspicious activity. You will be notified once the review is complete.',
color: 'text-yellow-600',
bgColor: 'bg-yellow-50',
borderColor: 'border-yellow-200',
iconColor: 'text-yellow-600',
};
default:
return {
icon: FiClock,
title: 'Transaction Pending',
message: 'Your transaction is being processed. Please wait for confirmation.',
color: 'text-blue-600',
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
iconColor: 'text-blue-600',
};
}
};
const statusConfig = getStatusConfig();
const Icon = statusConfig.icon;
return (
<div className="w-full max-w-2xl mx-auto">
<Card className={`p-8 ${statusConfig.bgColor} ${statusConfig.borderColor}`}>
<div className="text-center mb-6">
<div className="flex justify-center mb-4">
<div className={`w-16 h-16 rounded-full ${statusConfig.bgColor} border-2 ${statusConfig.borderColor} flex items-center justify-center`}>
<Icon className={`w-8 h-8 ${statusConfig.iconColor}`} />
</div>
</div>
<h2 className={`text-2xl md:text-3xl font-bold ${statusConfig.color} mb-2`}>
{statusConfig.title}
</h2>
<p className="text-gray-600 text-sm md:text-base">
{statusConfig.message}
</p>
</div>
<div className="bg-white rounded-lg p-6 mb-6">
<div className="space-y-4">
<div className="flex justify-between items-center pb-4 border-b border-gray-200">
<span className="text-gray-600">Transaction ID</span>
<span className="font-mono text-sm font-medium text-gray-800">
{transaction.id}
</span>
</div>
<div className="flex justify-between items-center pb-4 border-b border-gray-200">
<span className="text-gray-600">Amount</span>
<span className="text-xl font-bold text-gray-800">
{formatCurrency(transaction.amount)}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
<div>
<p className="text-sm text-gray-600 mb-1">Recipient</p>
<p className="font-medium text-gray-800">{transaction.recipient}</p>
</div>
{transaction.recipientEmail && (
<div>
<p className="text-sm text-gray-600 mb-1">Email</p>
<p className="font-medium text-gray-800">{transaction.recipientEmail}</p>
</div>
)}
<div>
<p className="text-sm text-gray-600 mb-1">Account Type</p>
<p className="font-medium text-gray-800 capitalize">
{transaction.accountType}
</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Status</p>
<p className="font-medium text-gray-800 capitalize">
{transaction.status}
</p>
</div>
{transaction.description && (
<div className="md:col-span-2">
<p className="text-sm text-gray-600 mb-1">Description</p>
<p className="font-medium text-gray-800">{transaction.description}</p>
</div>
)}
</div>
</div>
</div>
{onNewTransaction && (
<div className="text-center">
<Button
variant="primary"
size="lg"
onClick={onNewTransaction}
>
Create New Transaction
</Button>
</div>
)}
</Card>
</div>
);
};

View File

@ -0,0 +1,30 @@
import React from 'react';
import { Button } from '../ui/Button';
import { useNavigate } from 'react-router-dom';
export const Error404: React.FC = () => {
const navigate = useNavigate();
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="max-w-md w-full text-center">
<img
src="/404.svg"
alt="404 Error"
className="w-full max-w-sm mx-auto mb-8"
/>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Page Not Found</h1>
<p className="text-gray-600 mb-8">
The page you're looking for doesn't exist or has been moved.
</p>
<Button
variant="primary"
size="lg"
onClick={() => navigate('/')}
>
Go Back Home
</Button>
</div>
</div>
);
};

View File

@ -0,0 +1,23 @@
import React from 'react';
export const Maintenance: React.FC = () => {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="max-w-md w-full text-center">
<img
src="/Maintenance.svg"
alt="Maintenance"
className="w-full max-w-sm mx-auto mb-8"
/>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Under Maintenance</h1>
<p className="text-gray-600 mb-4">
We're currently performing scheduled maintenance to improve your experience.
</p>
<p className="text-sm text-gray-500">
Please check back soon. We apologize for any inconvenience.
</p>
</div>
</div>
);
};

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Button } from '../ui/Button';
export const NoInternet: React.FC = () => {
const handleRetry = () => {
window.location.reload();
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="max-w-md w-full text-center">
<img
src="/NoInternet.svg"
alt="No Internet"
className="w-full max-w-sm mx-auto mb-8"
/>
<h1 className="text-3xl font-bold text-gray-900 mb-2">No Internet Connection</h1>
<p className="text-gray-600 mb-8">
Please check your internet connection and try again.
</p>
<Button
variant="primary"
size="lg"
onClick={handleRetry}
>
Retry
</Button>
</div>
</div>
);
};

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Logo } from '../ui/Logo';
import { FiHome, FiShield, FiUsers, FiAlertTriangle } from 'react-icons/fi'; import { FiHome, FiShield, FiUsers, FiAlertTriangle } from 'react-icons/fi';
interface LayoutProps { interface LayoutProps {
@ -18,7 +19,9 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
<div className="flex justify-between h-16"> <div className="flex justify-between h-16">
<div className="flex"> <div className="flex">
<div className="flex-shrink-0 flex items-center"> <div className="flex-shrink-0 flex items-center">
<span className="text-xl font-bold text-primary-600">Amba Checkout</span> <Link to="/" className="flex items-center gap-2">
<Logo />
</Link>
</div> </div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8"> <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<Link <Link

View File

@ -0,0 +1,79 @@
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { FiMessageCircle, FiX } from 'react-icons/fi';
export const ChatButton: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const location = useLocation();
// Don't show chat on error pages or admin pages
const hideChat = location.pathname.includes('/admin') ||
location.pathname === '/maintenance' ||
location.pathname === '/no-internet' ||
location.pathname === '/404';
if (hideChat) return null;
return (
<>
{/* Chat Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="fixed bottom-6 right-6 z-50 w-14 h-14 bg-primary-500 hover:bg-primary-600 text-white rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110"
aria-label="Open support chat"
>
{isOpen ? (
<FiX className="w-6 h-6" />
) : (
<FiMessageCircle className="w-6 h-6" />
)}
</button>
{/* Chat Window */}
{isOpen && (
<div className="fixed bottom-24 right-6 z-50 w-80 h-96 bg-white rounded-lg shadow-2xl flex flex-col border border-gray-200">
{/* Chat Header */}
<div className="bg-primary-500 text-white p-4 rounded-t-lg flex items-center justify-between">
<div>
<h3 className="font-semibold">Support Chat</h3>
<p className="text-xs text-primary-100">We typically reply in minutes</p>
</div>
</div>
{/* Chat Messages */}
<div className="flex-1 p-4 overflow-y-auto bg-gray-50">
<div className="space-y-4">
<div className="flex items-start gap-2">
<div className="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
<span className="text-primary-600 text-xs font-bold">A</span>
</div>
<div className="flex-1">
<div className="bg-white rounded-lg p-3 shadow-sm">
<p className="text-sm text-gray-800">
Hello! How can we help you today?
</p>
</div>
</div>
</div>
</div>
</div>
{/* Chat Input */}
<div className="p-4 border-t border-gray-200 bg-white rounded-b-lg">
<div className="flex gap-2">
<input
type="text"
placeholder="Type your message..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-sm"
/>
<button className="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-lg transition-colors text-sm font-medium">
Send
</button>
</div>
</div>
</div>
)}
</>
);
};

View File

@ -0,0 +1,31 @@
import React from 'react';
interface LoadingProps {
message?: string;
fullScreen?: boolean;
}
export const Loading: React.FC<LoadingProps> = ({
message = 'Loading...',
fullScreen = false
}) => {
const containerClass = fullScreen
? 'min-h-screen bg-gray-50 flex items-center justify-center'
: 'flex items-center justify-center py-12';
return (
<div className={containerClass}>
<div className="text-center">
<img
src="/AmbaLogo.gif"
alt="Loading"
className="w-32 h-32 mx-auto mb-4"
/>
{message && (
<p className="text-gray-600">{message}</p>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
interface LogoProps {
className?: string;
dark?: boolean; // If true, use dark version for light backgrounds
color?: string; // Custom color override (e.g., '#105D38')
}
export const Logo: React.FC<LogoProps> = ({ className = '', dark = false, color }) => {
// For dark backgrounds, use white filter, for light backgrounds use default
// If custom color is provided, use CSS filter to convert to that color
let filterStyle: React.CSSProperties = {};
if (color === '#105D38') {
// CSS filter to convert to #105D38 green
// This filter converts black to the specified green color
filterStyle = {
filter: 'brightness(0) saturate(100%) invert(13%) sepia(94%) saturate(1352%) hue-rotate(118deg) brightness(95%) contrast(87%)',
};
} else if (dark) {
// For dark backgrounds, invert to white
filterStyle = {
filter: 'brightness(0) invert(1)',
};
}
return (
<img
src="/Logo.svg"
alt="Amba Logo"
className={`h-8 w-auto ${className}`}
style={filterStyle}
/>
);
};

View File

@ -1,17 +1,26 @@
import React, { createContext, useContext, useState, ReactNode } from 'react'; import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
import type { FundRequest, AccountType, Transaction } from '../types'; import type { FundRequest, AccountType, Transaction, Requester, RecipientBankAccount, TransactionFeedback } from '../types';
import { api } from '../utils/api'; import { api } from '../utils/api';
interface CheckoutContextType { interface CheckoutContextType {
currentStep: number; currentStep: number;
requester: Requester | null;
selectedRecipientAccountId: string | null;
selectedRecipientAccount: RecipientBankAccount | null;
fundRequest: Partial<FundRequest> | null; fundRequest: Partial<FundRequest> | null;
selectedAccountType: AccountType | null; selectedAccountType: AccountType | null;
transaction: Transaction | null; transaction: Transaction | null;
showSuccessModal: boolean;
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | null;
transactionFee: number;
setRequester: (requester: Requester) => void;
setSelectedRecipientAccountId: (accountId: string) => void;
setFundRequest: (data: Partial<FundRequest>) => void; setFundRequest: (data: Partial<FundRequest>) => void;
setAccountType: (type: AccountType) => void; setAccountType: (type: AccountType) => void;
submitTransaction: () => Promise<void>; submitPayment: () => Promise<void>;
submitFeedback: (feedback: TransactionFeedback) => void;
closeSuccessModal: () => void;
reset: () => void; reset: () => void;
goToStep: (step: number) => void; goToStep: (step: number) => void;
} }
@ -20,11 +29,80 @@ const CheckoutContext = createContext<CheckoutContextType | undefined>(undefined
export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => { export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
const [requester, setRequesterState] = useState<Requester | null>(null);
const [selectedRecipientAccountId, setSelectedRecipientAccountIdState] = useState<string | null>(null);
const [selectedRecipientAccount, setSelectedRecipientAccountState] = useState<RecipientBankAccount | null>(null);
const [fundRequest, setFundRequestState] = useState<Partial<FundRequest> | null>(null); const [fundRequest, setFundRequestState] = useState<Partial<FundRequest> | null>(null);
const [selectedAccountType, setSelectedAccountType] = useState<AccountType | null>(null); const [selectedAccountType, setSelectedAccountType] = useState<AccountType | null>(null);
const [transaction, setTransaction] = useState<Transaction | null>(null); const [transaction, setTransaction] = useState<Transaction | null>(null);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [transactionFee] = useState(2.50); // Fixed transaction fee for now
// Initialize with mock data for development
useEffect(() => {
// Mock requester data with bank accounts (in real app, this would come from URL params or API)
const mockRequester: Requester = {
id: 'req-1',
name: 'John Doe',
email: 'john.doe@example.com',
phone: '+251 911 234 567',
requestAmount: 1250.00,
description: 'Payment for services rendered',
requestId: 'req-12345',
bankAccounts: [
{
id: 'bank-1',
bankName: 'Awash Bank',
accountNumber: '1000123456789',
accountHolderName: 'John Doe',
accountType: 'personal',
isDefault: true,
},
{
id: 'bank-2',
bankName: 'Commercial Bank of Ethiopia',
accountNumber: '2000987654321',
accountHolderName: 'John Doe',
accountType: 'business',
},
],
};
setRequesterState(mockRequester);
// Set initial fund request from requester
setFundRequestState({
amount: mockRequester.requestAmount,
recipient: mockRequester.name,
recipientEmail: mockRequester.email,
description: mockRequester.description,
});
}, []);
const setRequester = (req: Requester) => {
setRequesterState(req);
setFundRequestState({
amount: req.requestAmount,
recipient: req.name,
recipientEmail: req.email,
description: req.description,
});
setError(null);
};
const setSelectedRecipientAccountId = (accountId: string) => {
setSelectedRecipientAccountIdState(accountId);
if (requester?.bankAccounts) {
const account = requester.bankAccounts.find(acc => acc.id === accountId);
if (account) {
setSelectedRecipientAccountState(account);
setSelectedAccountType(account.accountType);
}
}
setError(null);
};
const setFundRequest = (data: Partial<FundRequest>) => { const setFundRequest = (data: Partial<FundRequest>) => {
setFundRequestState(data); setFundRequestState(data);
@ -36,8 +114,8 @@ export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }
setError(null); setError(null);
}; };
const submitTransaction = async () => { const submitPayment = async () => {
if (!fundRequest || !selectedAccountType) { if (!fundRequest || !selectedAccountType || !selectedRecipientAccountId) {
setError('Please complete all required fields'); setError('Please complete all required fields');
return; return;
} }
@ -53,19 +131,34 @@ export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }
const newTransaction = await api.createTransaction(transactionData); const newTransaction = await api.createTransaction(transactionData);
setTransaction(newTransaction); setTransaction(newTransaction);
setCurrentStep(4); setShowSuccessModal(true);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create transaction'); setError(err instanceof Error ? err.message : 'Failed to process payment');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const submitFeedback = (feedback: TransactionFeedback) => {
// In real app, this would send feedback to backend
console.log('Feedback submitted:', feedback);
setShowSuccessModal(false);
setCurrentStep(3);
};
const closeSuccessModal = () => {
setShowSuccessModal(false);
setCurrentStep(3);
};
const reset = () => { const reset = () => {
setCurrentStep(1); setCurrentStep(1);
setSelectedRecipientAccountIdState(null);
setSelectedRecipientAccountState(null);
setFundRequestState(null); setFundRequestState(null);
setSelectedAccountType(null); setSelectedAccountType(null);
setTransaction(null); setTransaction(null);
setShowSuccessModal(false);
setError(null); setError(null);
setIsLoading(false); setIsLoading(false);
}; };
@ -80,14 +173,23 @@ export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }
<CheckoutContext.Provider <CheckoutContext.Provider
value={{ value={{
currentStep, currentStep,
requester,
selectedRecipientAccountId,
selectedRecipientAccount,
fundRequest, fundRequest,
selectedAccountType, selectedAccountType,
transaction, transaction,
showSuccessModal,
isLoading, isLoading,
error, error,
transactionFee,
setRequester,
setSelectedRecipientAccountId,
setFundRequest, setFundRequest,
setAccountType, setAccountType,
submitTransaction, submitPayment,
submitFeedback,
closeSuccessModal,
reset, reset,
goToStep, goToStep,
}} }}

View File

@ -0,0 +1,21 @@
import { useState, useEffect } from 'react';
export const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
};

View File

@ -4,9 +4,7 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: 'DM Sans', sans-serif;
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
@ -15,3 +13,14 @@ body {
box-sizing: border-box; box-sizing: border-box;
} }
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}

View File

@ -0,0 +1,60 @@
import React from 'react';
import { CheckoutProvider } from '../contexts/CheckoutContext';
import { RequesterDetails } from '../components/checkout/RequesterDetails';
import { useCheckout } from '../contexts/CheckoutContext';
import { useNavigate } from 'react-router-dom';
import { Loading } from '../components/ui/Loading';
const AccountSelectionContent: React.FC = () => {
const {
requester,
selectedRecipientAccountId,
setSelectedRecipientAccountId,
} = useCheckout();
const navigate = useNavigate();
const handleAccountSelect = (accountId: string) => {
setSelectedRecipientAccountId(accountId);
};
const handleNext = () => {
// Ensure an account is selected before proceeding
if (!selectedRecipientAccountId && requester?.bankAccounts && requester.bankAccounts.length > 0) {
const defaultAccount = requester.bankAccounts.find(acc => acc.isDefault) || requester.bankAccounts[0];
if (defaultAccount) {
setSelectedRecipientAccountId(defaultAccount.id);
}
}
// Navigate to checkout page
navigate('/checkout');
};
if (!requester) {
return <Loading fullScreen message="Loading payment request..." />;
}
return (
<RequesterDetails
requester={requester}
recipientAccounts={requester.bankAccounts || []}
selectedAccountId={selectedRecipientAccountId || undefined}
onSelectAccount={handleAccountSelect}
onNext={handleNext}
onFlag={(reason) => {
// Handle flagging - in real app, this would call an API
console.log('Flagged user:', requester.id, 'Reason:', reason);
alert(`User has been reported. Reason: ${reason === 'spam' ? 'Spam or suspicious activity' : reason === 'unknown' ? "I don't know this person" : 'Other'}`);
}}
/>
);
};
export const AccountSelectionPage: React.FC = () => {
return (
<CheckoutProvider>
<AccountSelectionContent />
</CheckoutProvider>
);
};

View File

@ -1,7 +0,0 @@
import React from 'react';
import { AccountFlagging } from '../components/admin/AccountFlagging';
export const AccountsPage: React.FC = () => {
return <AccountFlagging />;
};

View File

@ -1,7 +0,0 @@
import React from 'react';
import { AdminDashboard } from '../components/admin/AdminDashboard';
export const AdminPage: React.FC = () => {
return <AdminDashboard />;
};

View File

@ -1,11 +0,0 @@
import React from 'react';
import { CheckoutFlow } from '../components/checkout/CheckoutFlow';
export const CheckoutPage: React.FC = () => {
return (
<div className="min-h-screen bg-gray-50">
<CheckoutFlow />
</div>
);
};

View File

@ -0,0 +1,61 @@
import React from 'react';
import { CheckoutProvider } from '../contexts/CheckoutContext';
import { CheckoutPage } from '../components/checkout/CheckoutPage';
import { useCheckout } from '../contexts/CheckoutContext';
import { useNavigate } from 'react-router-dom';
import { Loading } from '../components/ui/Loading';
const CheckoutContent: React.FC = () => {
const {
requester,
selectedRecipientAccountId,
selectedRecipientAccount,
fundRequest,
transactionFee,
submitPayment,
} = useCheckout();
const navigate = useNavigate();
const handlePaymentComplete = async () => {
await submitPayment();
// After payment, you might want to navigate somewhere or show a success message
};
if (!requester || !fundRequest) {
return <Loading fullScreen message="Loading checkout..." />;
}
// Get the selected account from the requester's bank accounts
const account = selectedRecipientAccount ||
(selectedRecipientAccountId && requester.bankAccounts?.find(acc => acc.id === selectedRecipientAccountId)) ||
requester.bankAccounts?.[0];
if (!account) {
return <Loading fullScreen message="Loading account details..." />;
}
return (
<CheckoutPage
requester={{
name: requester.name,
requestAmount: requester.requestAmount,
description: requester.description,
}}
selectedAccount={{
bankName: account.bankName,
accountNumber: account.accountNumber,
}}
transactionFee={transactionFee}
onComplete={handlePaymentComplete}
/>
);
};
export const CheckoutPageRoute: React.FC = () => {
return (
<CheckoutProvider>
<CheckoutContent />
</CheckoutProvider>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import { useSearchParams } from 'react-router-dom';
import { SuccessPage } from '../components/checkout/SuccessPage';
import { Loading } from '../components/ui/Loading';
export const SuccessPageRoute: React.FC = () => {
const [searchParams] = useSearchParams();
const email = searchParams.get('email') || '';
const amount = parseFloat(searchParams.get('amount') || '0');
const transactionId = searchParams.get('transactionId') || `TXN-${Date.now()}`;
if (!email) {
return <Loading fullScreen message="Loading transaction details..." />;
}
return (
<SuccessPage
transactionId={transactionId}
amount={amount}
email={email}
/>
);
};

View File

@ -1,7 +0,0 @@
import React from 'react';
import { SuspiciousTransactions } from '../components/admin/SuspiciousTransactions';
export const SuspiciousPage: React.FC = () => {
return <SuspiciousTransactions />;
};

View File

@ -1,7 +0,0 @@
import React from 'react';
import { TransactionList } from '../components/admin/TransactionList';
export const TransactionsPage: React.FC = () => {
return <TransactionList />;
};

View File

@ -26,6 +26,8 @@ export interface Account {
flaggedAt?: string; flaggedAt?: string;
flaggedBy?: string; flaggedBy?: string;
createdAt: string; createdAt: string;
balance?: number;
accountNumber?: string;
} }
export interface FundRequest { export interface FundRequest {
@ -36,3 +38,53 @@ export interface FundRequest {
accountType?: AccountType; accountType?: AccountType;
} }
export interface Requester {
id: string;
name: string;
email: string;
phone?: string;
avatar?: string;
requestAmount: number;
description?: string;
requestId: string;
bankAccounts?: RecipientBankAccount[];
}
export interface RecipientBankAccount {
id: string;
bankName: string;
accountNumber: string;
accountHolderName: string;
accountType: AccountType;
isDefault?: boolean;
}
export interface UserAccount {
id: string;
accountType: AccountType;
accountName: string;
accountNumber: string;
balance: number;
bankName?: string;
isDefault?: boolean;
}
export interface PaymentMethod {
type: 'card' | 'mobile' | 'bank';
name: string;
icon?: string;
}
export interface TransactionFeedback {
rating: number;
purpose: string;
otherReason?: string;
}
export interface DonationCause {
id: string;
name: string;
description: string;
image?: string;
category: string;
}

View File

@ -6,19 +6,35 @@ export default {
], ],
theme: { theme: {
extend: { extend: {
fontFamily: {
sans: ['DM Sans', 'sans-serif'],
},
colors: { colors: {
primary: { primary: {
DEFAULT: '#10B981', DEFAULT: '#105D38',
50: '#ECFDF5', 50: '#E6F5ED',
100: '#D1FAE5', 100: '#CCEBDC',
200: '#A7F3D0', 200: '#99D7B9',
300: '#6EE7B7', 300: '#66C396',
400: '#34D399', 400: '#33AF73',
500: '#10B981', 500: '#105D38',
600: '#059669', 600: '#0D4A2D',
700: '#047857', 700: '#0A3822',
800: '#065F46', 800: '#072517',
900: '#064E3B', 900: '#04130B',
},
secondary: {
DEFAULT: '#FFB668',
50: '#FFF5E6',
100: '#FFEBD1',
200: '#FFD7A3',
300: '#FFC375',
400: '#FFAF47',
500: '#FFB668',
600: '#CC9253',
700: '#996D3E',
800: '#66492A',
900: '#332415',
}, },
}, },
}, },