From 264f1e61596bf6d1f41e5e321a39478318b9d866 Mon Sep 17 00:00:00 2001 From: kirukib Date: Sun, 21 Dec 2025 21:44:04 +0300 Subject: [PATCH] checkout-page --- .eslintrc.cjs | 20 + .gitignore | 25 ++ env.d.ts | 10 + index.html | 14 + package.json | 36 ++ postcss.config.js | 7 + src/App.tsx | 35 ++ src/components/admin/AccountFlagging.tsx | 269 +++++++++++++ src/components/admin/AdminDashboard.tsx | 221 +++++++++++ .../admin/SuspiciousTransactions.tsx | 190 +++++++++ src/components/admin/TransactionList.tsx | 361 ++++++++++++++++++ src/components/checkout/AccountSelection.tsx | 87 +++++ src/components/checkout/CheckoutFlow.tsx | 163 ++++++++ src/components/checkout/FundRequestForm.tsx | 111 ++++++ src/components/checkout/PromotionalPanel.tsx | 30 ++ .../checkout/TransactionConfirmation.tsx | 114 ++++++ src/components/checkout/TransactionStatus.tsx | 156 ++++++++ src/components/layout/Layout.tsx | 86 +++++ src/components/ui/Button.tsx | 54 +++ src/components/ui/Card.tsx | 33 ++ src/components/ui/Input.tsx | 49 +++ src/contexts/CheckoutContext.tsx | 107 ++++++ src/index.css | 17 + src/main.tsx | 11 + src/pages/AccountsPage.tsx | 7 + src/pages/AdminPage.tsx | 7 + src/pages/CheckoutPage.tsx | 11 + src/pages/SuspiciousPage.tsx | 7 + src/pages/TransactionsPage.tsx | 7 + src/types/index.ts | 38 ++ src/utils/api.ts | 156 ++++++++ tailwind.config.js | 28 ++ tsconfig.json | 26 ++ tsconfig.node.json | 11 + vite.config.ts | 8 + 35 files changed, 2512 insertions(+) create mode 100644 .eslintrc.cjs create mode 100644 .gitignore create mode 100644 env.d.ts create mode 100644 index.html create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 src/App.tsx create mode 100644 src/components/admin/AccountFlagging.tsx create mode 100644 src/components/admin/AdminDashboard.tsx create mode 100644 src/components/admin/SuspiciousTransactions.tsx create mode 100644 src/components/admin/TransactionList.tsx create mode 100644 src/components/checkout/AccountSelection.tsx create mode 100644 src/components/checkout/CheckoutFlow.tsx create mode 100644 src/components/checkout/FundRequestForm.tsx create mode 100644 src/components/checkout/PromotionalPanel.tsx create mode 100644 src/components/checkout/TransactionConfirmation.tsx create mode 100644 src/components/checkout/TransactionStatus.tsx create mode 100644 src/components/layout/Layout.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/contexts/CheckoutContext.tsx create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/pages/AccountsPage.tsx create mode 100644 src/pages/AdminPage.tsx create mode 100644 src/pages/CheckoutPage.tsx create mode 100644 src/pages/SuspiciousPage.tsx create mode 100644 src/pages/TransactionsPage.tsx create mode 100644 src/types/index.ts create mode 100644 src/utils/api.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..1fc7204 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + }, +} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d600b6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..d326d43 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + diff --git a/index.html b/index.html new file mode 100644 index 0000000..52ef5f7 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Amba Checkout + + +
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..372bbdc --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "amba-checkout", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "react-hook-form": "^7.48.2", + "react-icons": "^4.12.0", + "zustand": "^4.4.7" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} + diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..b4a6220 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..473a786 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,35 @@ +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'; + +function App() { + return ( + + + + + + + } + /> + } /> + } /> + } /> + } /> + + + + ); +} + +export default App; + diff --git a/src/components/admin/AccountFlagging.tsx b/src/components/admin/AccountFlagging.tsx new file mode 100644 index 0000000..6ab9e0d --- /dev/null +++ b/src/components/admin/AccountFlagging.tsx @@ -0,0 +1,269 @@ +import React, { useEffect, useState } from 'react'; +import { api } from '../../utils/api'; +import type { Account } from '../../types'; +import { Card } from '../ui/Card'; +import { Button } from '../ui/Button'; +import { Input } from '../ui/Input'; +import { FiFlag, FiX, FiSearch, FiUser, FiBriefcase } from 'react-icons/fi'; + +export const AccountFlagging: React.FC = () => { + const [accounts, setAccounts] = useState([]); + const [filteredAccounts, setFilteredAccounts] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [flaggingAccount, setFlaggingAccount] = useState(null); + const [flagReason, setFlagReason] = useState(''); + + useEffect(() => { + const fetchAccounts = async () => { + try { + const data = await api.getAccounts(); + setAccounts(data); + setFilteredAccounts(data); + } catch (error) { + console.error('Failed to fetch accounts:', error); + } finally { + setLoading(false); + } + }; + + fetchAccounts(); + }, []); + + useEffect(() => { + if (searchTerm) { + const filtered = accounts.filter( + (a) => + a.email.toLowerCase().includes(searchTerm.toLowerCase()) || + a.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + a.id.toLowerCase().includes(searchTerm.toLowerCase()) + ); + setFilteredAccounts(filtered); + } else { + setFilteredAccounts(accounts); + } + }, [searchTerm, accounts]); + + const handleFlag = async (accountId: string, reason: string) => { + try { + await api.flagAccount(accountId, reason); + const updated = await api.getAccounts(); + setAccounts(updated); + setFlaggingAccount(null); + setFlagReason(''); + } catch (error) { + console.error('Failed to flag account:', error); + } + }; + + const handleUnflag = async (accountId: string) => { + try { + await api.unflagAccount(accountId); + const updated = await api.getAccounts(); + setAccounts(updated); + } catch (error) { + console.error('Failed to unflag account:', error); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+

Account Management

+

Flag or unflag accounts for suspicious activity

+
+ + {/* Search */} + +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" + /> +
+
+ + {/* Accounts List */} +
+ {filteredAccounts.map((account) => ( + +
+
+
+ {account.accountType === 'business' ? ( + + ) : ( + + )} +
+
+

+ {account.name || 'Unknown User'} +

+

{account.email}

+
+
+ {account.isFlagged && ( + + Flagged + + )} +
+ +
+
+ Account Type + + {account.accountType} + +
+
+ Created + + {formatDate(account.createdAt)} + +
+ {account.isFlagged && account.flaggedReason && ( +
+

Flag Reason

+

+ {account.flaggedReason} +

+
+ )} + {account.isFlagged && account.flaggedAt && ( +
+ Flagged At + + {formatDate(account.flaggedAt)} + +
+ )} +
+ +
+ {account.isFlagged ? ( + + ) : ( + + )} +
+
+ ))} +
+ + {filteredAccounts.length === 0 && ( + +

No accounts found

+
+ )} + + {/* Flag Account Modal */} + {flaggingAccount && ( +
+ +
+
+

Flag Account

+ +
+ +
+

+ Flagging account: {flaggingAccount.email} +

+ setFlagReason(e.target.value)} + required + /> +
+ +
+ + +
+
+
+
+ )} +
+ ); +}; + diff --git a/src/components/admin/AdminDashboard.tsx b/src/components/admin/AdminDashboard.tsx new file mode 100644 index 0000000..5cd6a35 --- /dev/null +++ b/src/components/admin/AdminDashboard.tsx @@ -0,0 +1,221 @@ +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../../utils/api'; +import type { Transaction, Account } from '../../types'; +import { Card } from '../ui/Card'; +import { FiDollarSign, FiAlertTriangle, FiCheckCircle, FiClock, FiUsers } from 'react-icons/fi'; + +export const AdminDashboard: React.FC = () => { + const [transactions, setTransactions] = useState([]); + const [accounts, setAccounts] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const [txs, accts] = await Promise.all([ + api.getTransactions(), + api.getAccounts(), + ]); + setTransactions(txs); + setAccounts(accts); + } catch (error) { + console.error('Failed to fetch data:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const stats = { + totalTransactions: transactions.length, + pending: transactions.filter(t => t.status === 'pending').length, + suspicious: transactions.filter(t => t.status === 'suspicious').length, + approved: transactions.filter(t => t.status === 'approved').length, + totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0), + flaggedAccounts: accounts.filter(a => a.isFlagged).length, + totalAccounts: accounts.length, + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(amount); + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+

Admin Dashboard

+

Overview of transactions and accounts

+
+ + {/* Stats Grid */} +
+ +
+
+

Total Transactions

+

{stats.totalTransactions}

+
+
+ +
+
+
+ + +
+
+

Pending Review

+

{stats.pending}

+
+
+ +
+
+
+ + +
+
+

Suspicious

+

{stats.suspicious}

+
+
+ +
+
+
+ + +
+
+

Approved

+

{stats.approved}

+
+
+ +
+
+
+
+ + {/* Additional Stats */} +
+ +
+
+

Total Amount

+

+ {formatCurrency(stats.totalAmount)} +

+
+
+
+ + +
+
+

Flagged Accounts

+

{stats.flaggedAccounts}

+
+
+ +
+
+
+ + +
+
+

Total Accounts

+

{stats.totalAccounts}

+
+
+ +
+
+
+
+ + {/* Quick Actions */} +
+ +

Quick Actions

+
+ +
+ Review Suspicious Transactions + +
+ + +
+ View All Transactions + +
+ + +
+ Manage Accounts + +
+ +
+
+ + +

Recent Activity

+
+ {transactions.slice(0, 5).map((transaction) => ( +
+
+

+ {formatCurrency(transaction.amount)} +

+

{transaction.recipient}

+
+ + {transaction.status} + +
+ ))} +
+
+
+
+ ); +}; + diff --git a/src/components/admin/SuspiciousTransactions.tsx b/src/components/admin/SuspiciousTransactions.tsx new file mode 100644 index 0000000..2be230b --- /dev/null +++ b/src/components/admin/SuspiciousTransactions.tsx @@ -0,0 +1,190 @@ +import React, { useEffect, useState } from 'react'; +import { api } from '../../utils/api'; +import type { Transaction } from '../../types'; +import { Card } from '../ui/Card'; +import { Button } from '../ui/Button'; +import { FiAlertTriangle, FiCheckCircle, FiXCircle } from 'react-icons/fi'; + +export const SuspiciousTransactions: React.FC = () => { + const [suspiciousTransactions, setSuspiciousTransactions] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchSuspicious = async () => { + try { + const allTransactions = await api.getTransactions(); + const suspicious = allTransactions.filter( + (t) => t.status === 'suspicious' || t.status === 'rejected' + ); + setSuspiciousTransactions(suspicious); + } catch (error) { + console.error('Failed to fetch suspicious transactions:', error); + } finally { + setLoading(false); + } + }; + + fetchSuspicious(); + }, []); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(amount); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const handleApprove = async (id: string) => { + try { + await api.updateTransactionStatus(id, 'approved'); + const allTransactions = await api.getTransactions(); + const suspicious = allTransactions.filter( + (t) => t.status === 'suspicious' || t.status === 'rejected' + ); + setSuspiciousTransactions(suspicious); + } catch (error) { + console.error('Failed to approve transaction:', error); + } + }; + + const handleReject = async (id: string) => { + try { + await api.updateTransactionStatus(id, 'rejected', 'Flagged as suspicious by admin'); + const allTransactions = await api.getTransactions(); + const suspicious = allTransactions.filter( + (t) => t.status === 'suspicious' || t.status === 'rejected' + ); + setSuspiciousTransactions(suspicious); + } catch (error) { + console.error('Failed to reject transaction:', error); + } + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+
+ +

Suspicious Transactions

+
+

Review and manage flagged transactions

+
+ + {suspiciousTransactions.length === 0 ? ( + + +

All Clear!

+

No suspicious transactions to review.

+
+ ) : ( +
+ {suspiciousTransactions.map((transaction) => ( + +
+
+
+ + + {transaction.id.slice(0, 8)}... + +
+

+ {formatCurrency(transaction.amount)} +

+
+ + {transaction.status} + +
+ +
+
+

Recipient

+

{transaction.recipient}

+ {transaction.recipientEmail && ( +

{transaction.recipientEmail}

+ )} +
+ +
+
+

Account Type

+

+ {transaction.accountType} +

+
+
+

Date

+

+ {formatDate(transaction.createdAt)} +

+
+
+ + {transaction.description && ( +
+

Description

+

{transaction.description}

+
+ )} + + {transaction.rejectionReason && ( +
+

Reason

+

{transaction.rejectionReason}

+
+ )} +
+ +
+ + +
+
+ ))} +
+ )} +
+ ); +}; + diff --git a/src/components/admin/TransactionList.tsx b/src/components/admin/TransactionList.tsx new file mode 100644 index 0000000..0ed4405 --- /dev/null +++ b/src/components/admin/TransactionList.tsx @@ -0,0 +1,361 @@ +import React, { useEffect, useState } from 'react'; +import { api } from '../../utils/api'; +import type { Transaction } from '../../types'; +import { Card } from '../ui/Card'; +import { Button } from '../ui/Button'; +import { FiSearch, FiFilter, FiEye } from 'react-icons/fi'; + +export const TransactionList: React.FC = () => { + const [transactions, setTransactions] = useState([]); + const [filteredTransactions, setFilteredTransactions] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [selectedTransaction, setSelectedTransaction] = useState(null); + + useEffect(() => { + const fetchTransactions = async () => { + try { + const data = await api.getTransactions(); + setTransactions(data); + setFilteredTransactions(data); + } catch (error) { + console.error('Failed to fetch transactions:', error); + } finally { + setLoading(false); + } + }; + + fetchTransactions(); + }, []); + + useEffect(() => { + let filtered = transactions; + + if (searchTerm) { + filtered = filtered.filter( + (t) => + t.recipient.toLowerCase().includes(searchTerm.toLowerCase()) || + t.id.toLowerCase().includes(searchTerm.toLowerCase()) || + t.recipientEmail?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + if (statusFilter !== 'all') { + filtered = filtered.filter((t) => t.status === statusFilter); + } + + setFilteredTransactions(filtered); + }, [searchTerm, statusFilter, transactions]); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(amount); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const getStatusColor = (status: Transaction['status']) => { + switch (status) { + case 'approved': + return 'bg-green-100 text-green-800'; + case 'rejected': + return 'bg-red-100 text-red-800'; + case 'suspicious': + return 'bg-yellow-100 text-yellow-800'; + default: + return 'bg-blue-100 text-blue-800'; + } + }; + + const handleStatusUpdate = async (id: string, status: Transaction['status']) => { + try { + await api.updateTransactionStatus(id, status); + const updated = await api.getTransactions(); + setTransactions(updated); + } catch (error) { + console.error('Failed to update transaction:', error); + } + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+

Transactions

+

View and manage all transactions

+
+ + {/* Filters */} + +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" + /> +
+
+ + +
+
+
+ + {/* Transaction Table */} + +
+ + + + + + + + + + + + + + {filteredTransactions.map((transaction) => ( + + + + + + + + + + ))} + +
+ ID + + Recipient + + Amount + + Type + + Status + + Date + + Actions +
+
+ {transaction.id.slice(0, 8)}... +
+
+
+ {transaction.recipient} +
+ {transaction.recipientEmail && ( +
{transaction.recipientEmail}
+ )} +
+
+ {formatCurrency(transaction.amount)} +
+
+ + {transaction.accountType} + + + + {transaction.status} + + + {formatDate(transaction.createdAt)} + +
+ + {transaction.status === 'pending' && ( + <> + + + + )} +
+
+
+ + {filteredTransactions.length === 0 && ( +
+

No transactions found

+
+ )} +
+ + {/* Transaction Detail Modal */} + {selectedTransaction && ( +
+ +
+
+

Transaction Details

+ +
+ +
+
+

Transaction ID

+

+ {selectedTransaction.id} +

+
+ +
+
+

Amount

+

+ {formatCurrency(selectedTransaction.amount)} +

+
+
+

Status

+ + {selectedTransaction.status} + +
+
+ +
+

Recipient

+

{selectedTransaction.recipient}

+ {selectedTransaction.recipientEmail && ( +

{selectedTransaction.recipientEmail}

+ )} +
+ +
+

Account Type

+

+ {selectedTransaction.accountType} +

+
+ + {selectedTransaction.description && ( +
+

Description

+

+ {selectedTransaction.description} +

+
+ )} + +
+

Created At

+

+ {formatDate(selectedTransaction.createdAt)} +

+
+ + {selectedTransaction.rejectionReason && ( +
+

Rejection Reason

+

+ {selectedTransaction.rejectionReason} +

+
+ )} +
+ +
+ {selectedTransaction.status === 'pending' && ( + <> + + + + )} + +
+
+
+
+ )} +
+ ); +}; + diff --git a/src/components/checkout/AccountSelection.tsx b/src/components/checkout/AccountSelection.tsx new file mode 100644 index 0000000..a67a5d0 --- /dev/null +++ b/src/components/checkout/AccountSelection.tsx @@ -0,0 +1,87 @@ +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 = ({ + selectedAccountType, + onSelect, + onNext, +}) => { + return ( +
+
+
+
+
+
+
+
+
+
+

+ Choose your account type +

+

+ It is a long established fact that a reader will be distracted by the readable content of a page +

+
+ +
+ onSelect('business')} + className="p-6" + > +
+
+
+ +
+
+
+

Business

+

Search or find a home

+
+
+
+ + onSelect('personal')} + className="p-6" + > +
+
+
+ +
+
+
+

Personal

+

Post or share your home

+
+
+
+
+ + {selectedAccountType && onNext && ( +
+ +
+ )} +
+ ); +}; + diff --git a/src/components/checkout/CheckoutFlow.tsx b/src/components/checkout/CheckoutFlow.tsx new file mode 100644 index 0000000..9ff3c57 --- /dev/null +++ b/src/components/checkout/CheckoutFlow.tsx @@ -0,0 +1,163 @@ +import React 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'; + +export const CheckoutFlow: React.FC = () => { + const { + currentStep, + fundRequest, + selectedAccountType, + transaction, + isLoading, + error, + setFundRequest, + setAccountType, + submitTransaction, + reset, + goToStep, + } = useCheckout(); + + const handleFundRequestSubmit = (data: any) => { + setFundRequest(data); + goToStep(2); + }; + + const handleAccountSelect = (accountType: 'business' | 'personal') => { + setAccountType(accountType); + }; + + const handleConfirm = async () => { + await submitTransaction(); + }; + + const handleCancel = () => { + goToStep(1); + }; + + 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' }, + ]; + + return ( +
+
+ {/* Progress Indicator - Hidden on account selection step to match design */} + {currentStep !== 2 && ( +
+
+ {steps.map((step, index) => ( + +
+
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 ? ( + + ) : ( + {step.number} + )} +
+ +
+ {index < steps.length - 1 && ( +
step.number + ? 'bg-primary-500' + : 'bg-gray-200' + } + `} + /> + )} + + ))} +
+
+ )} + + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} + + {/* Step Content */} + {currentStep === 2 ? ( +
+
+ +
+
+ goToStep(3)} + /> +
+
+ ) : ( +
+ {currentStep === 1 && ( + + )} + + {currentStep === 3 && fundRequest && selectedAccountType && ( + + )} + + {currentStep === 4 && transaction && ( + + )} +
+ )} +
+
+ ); +}; + diff --git a/src/components/checkout/FundRequestForm.tsx b/src/components/checkout/FundRequestForm.tsx new file mode 100644 index 0000000..2f37ce8 --- /dev/null +++ b/src/components/checkout/FundRequestForm.tsx @@ -0,0 +1,111 @@ +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; +} + +export const FundRequestForm: React.FC = ({ + onSubmit, + initialData, +}) => { + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: initialData, + }); + + return ( +
+
+

+ Request Funds Transfer +

+

+ Enter the details for the funds you want to transfer +

+
+ +
+ + + + + + +
+ +