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" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<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>
</head>
<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 { BrowserRouter, Routes, Route } from 'react-router-dom';
import { CheckoutProvider } from './contexts/CheckoutContext';
import { Layout } from './components/layout/Layout';
import { CheckoutPage } from './pages/CheckoutPage';
import { AdminPage } from './pages/AdminPage';
import { TransactionsPage } from './pages/TransactionsPage';
import { SuspiciousPage } from './pages/SuspiciousPage';
import { AccountsPage } from './pages/AccountsPage';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { ChatButton } from './components/ui/ChatButton';
import { AccountSelectionPage } from './pages/AccountSelectionPage';
import { CheckoutPageRoute } from './pages/CheckoutPageRoute';
import { SuccessPageRoute } from './pages/SuccessPageRoute';
import { Error404 } from './components/errors/Error404';
import { NoInternet } from './components/errors/NoInternet';
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() {
return (
<BrowserRouter>
<Layout>
<Routes>
<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>
<AppRoutes />
<ChatButton />
</BrowserRouter>
);
}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { api } from '../../utils/api';
import type { Transaction } from '../../types';
import { Card } from '../ui/Card';
import { Button } from '../ui/Button';
import { Loading } from '../ui/Loading';
import { FiSearch, FiFilter, FiEye } from 'react-icons/fi';
export const TransactionList: React.FC = () => {
@ -89,11 +90,7 @@ export const TransactionList: React.FC = () => {
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-gray-600">Loading...</div>
</div>
);
return <Loading fullScreen message="Loading transactions..." />;
}
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 { FundRequestForm } from './FundRequestForm';
import { AccountSelection } from './AccountSelection';
import { TransactionConfirmation } from './TransactionConfirmation';
import { TransactionStatus } from './TransactionStatus';
import { PromotionalPanel } from './PromotionalPanel';
import { FiCheck } from 'react-icons/fi';
import { RequesterDetails } from './RequesterDetails';
import { CheckoutPage } from './CheckoutPage';
import { Loading } from '../ui/Loading';
export const CheckoutFlow: React.FC = () => {
const {
currentStep,
requester,
selectedRecipientAccountId,
selectedRecipientAccount,
fundRequest,
selectedAccountType,
transaction,
isLoading,
error,
setFundRequest,
setAccountType,
submitTransaction,
reset,
goToStep,
transactionFee,
setSelectedRecipientAccountId,
submitPayment,
} = useCheckout();
const handleFundRequestSubmit = (data: any) => {
setFundRequest(data);
goToStep(2);
const [showCheckout, setShowCheckout] = useState(false);
const handleAccountSelect = (accountId: string) => {
setSelectedRecipientAccountId(accountId);
};
const handleAccountSelect = (accountType: 'business' | 'personal') => {
setAccountType(accountType);
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);
}
}
// Proceed to checkout page
setShowCheckout(true);
};
const handleConfirm = async () => {
await submitTransaction();
const handlePaymentComplete = async () => {
await submitPayment();
};
const handleCancel = () => {
goToStep(1);
};
if (!requester) {
return <Loading fullScreen message="Loading payment request..." />;
}
const steps = [
{ number: 1, label: 'Request', title: 'Fund Request' },
{ number: 2, label: 'Account', title: 'Account Type' },
{ number: 3, label: 'Review', title: 'Confirmation' },
{ number: 4, label: 'Status', title: 'Status' },
];
// Show checkout page if user clicked next
if (showCheckout && requester && fundRequest) {
// 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}
/>
);
}
// Show requester details page (initial page)
return (
<div className="min-h-screen bg-gray-50 py-4 md:py-8 px-4">
<div className="max-w-7xl mx-auto">
{/* Progress Indicator - Hidden on account selection step to match design */}
{currentStep !== 2 && (
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
{steps.map((step, index) => (
<React.Fragment key={step.number}>
<div className="flex flex-col items-center flex-1">
<div
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>
<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'}`);
}}
/>
);
};

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 { Logo } from '../ui/Logo';
import { FiFacebook, FiTwitter, FiInstagram, FiLinkedin, FiSmartphone } from 'react-icons/fi';
export const PromotionalPanel: React.FC = () => {
return (
<div className="bg-primary-500 h-full flex flex-col justify-between p-8 md:p-12 text-white">
<div>
<div className="mb-8">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-white rounded"></div>
<span className="text-xl font-bold">sanswap</span>
</div>
<div className="bg-primary-500 h-full flex flex-col justify-between p-8 md:p-12 text-white 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 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 className="flex-1 flex flex-col justify-center">
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4">
<div className="flex-1 flex flex-col justify-center items-center text-center">
<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.
</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
</p>
</div>
</div>
<div className="flex gap-2 justify-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
<div className="w-2 h-2 bg-white rounded-full"></div>
<div className="w-2 h-2 bg-white rounded-full"></div>
<div className="mt-auto">
{/* Download App Buttons */}
<div className="mb-6">
<p className="text-sm text-primary-100 text-center mb-3">Download the Amba App</p>
<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>
);

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 { Link, useLocation } from 'react-router-dom';
import { Logo } from '../ui/Logo';
import { FiHome, FiShield, FiUsers, FiAlertTriangle } from 'react-icons/fi';
interface LayoutProps {
@ -18,7 +19,9 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
<div className="flex justify-between h-16">
<div className="flex">
<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 className="hidden sm:ml-6 sm:flex sm:space-x-8">
<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 type { FundRequest, AccountType, Transaction } from '../types';
import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
import type { FundRequest, AccountType, Transaction, Requester, RecipientBankAccount, TransactionFeedback } from '../types';
import { api } from '../utils/api';
interface CheckoutContextType {
currentStep: number;
requester: Requester | null;
selectedRecipientAccountId: string | null;
selectedRecipientAccount: RecipientBankAccount | null;
fundRequest: Partial<FundRequest> | null;
selectedAccountType: AccountType | null;
transaction: Transaction | null;
showSuccessModal: boolean;
isLoading: boolean;
error: string | null;
transactionFee: number;
setRequester: (requester: Requester) => void;
setSelectedRecipientAccountId: (accountId: string) => void;
setFundRequest: (data: Partial<FundRequest>) => void;
setAccountType: (type: AccountType) => void;
submitTransaction: () => Promise<void>;
submitPayment: () => Promise<void>;
submitFeedback: (feedback: TransactionFeedback) => void;
closeSuccessModal: () => void;
reset: () => void;
goToStep: (step: number) => void;
}
@ -20,11 +29,80 @@ const CheckoutContext = createContext<CheckoutContextType | undefined>(undefined
export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
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 [selectedAccountType, setSelectedAccountType] = useState<AccountType | null>(null);
const [transaction, setTransaction] = useState<Transaction | null>(null);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
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>) => {
setFundRequestState(data);
@ -36,8 +114,8 @@ export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }
setError(null);
};
const submitTransaction = async () => {
if (!fundRequest || !selectedAccountType) {
const submitPayment = async () => {
if (!fundRequest || !selectedAccountType || !selectedRecipientAccountId) {
setError('Please complete all required fields');
return;
}
@ -53,19 +131,34 @@ export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }
const newTransaction = await api.createTransaction(transactionData);
setTransaction(newTransaction);
setCurrentStep(4);
setShowSuccessModal(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create transaction');
setError(err instanceof Error ? err.message : 'Failed to process payment');
} finally {
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 = () => {
setCurrentStep(1);
setSelectedRecipientAccountIdState(null);
setSelectedRecipientAccountState(null);
setFundRequestState(null);
setSelectedAccountType(null);
setTransaction(null);
setShowSuccessModal(false);
setError(null);
setIsLoading(false);
};
@ -80,14 +173,23 @@ export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }
<CheckoutContext.Provider
value={{
currentStep,
requester,
selectedRecipientAccountId,
selectedRecipientAccount,
fundRequest,
selectedAccountType,
transaction,
showSuccessModal,
isLoading,
error,
transactionFee,
setRequester,
setSelectedRecipientAccountId,
setFundRequest,
setAccountType,
submitTransaction,
submitPayment,
submitFeedback,
closeSuccessModal,
reset,
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 {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
font-family: 'DM Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@ -15,3 +13,14 @@ body {
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;
flaggedBy?: string;
createdAt: string;
balance?: number;
accountNumber?: string;
}
export interface FundRequest {
@ -36,3 +38,53 @@ export interface FundRequest {
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: {
extend: {
fontFamily: {
sans: ['DM Sans', 'sans-serif'],
},
colors: {
primary: {
DEFAULT: '#10B981',
50: '#ECFDF5',
100: '#D1FAE5',
200: '#A7F3D0',
300: '#6EE7B7',
400: '#34D399',
500: '#10B981',
600: '#059669',
700: '#047857',
800: '#065F46',
900: '#064E3B',
DEFAULT: '#105D38',
50: '#E6F5ED',
100: '#CCEBDC',
200: '#99D7B9',
300: '#66C396',
400: '#33AF73',
500: '#105D38',
600: '#0D4A2D',
700: '#0A3822',
800: '#072517',
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',
},
},
},