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 (
+
+ );
+ }
+
+ 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 (
+
+ );
+ }
+
+ 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 (
+
+ );
+ }
+
+ 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 (
+
+ );
+ }
+
+ 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 */}
+
+
+
+
+
+ |
+ ID
+ |
+
+ Recipient
+ |
+
+ Amount
+ |
+
+ Type
+ |
+
+ Status
+ |
+
+ Date
+ |
+
+ Actions
+ |
+
+
+
+ {filteredTransactions.map((transaction) => (
+
+ |
+
+ {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}
+ )}
+
+
= step.number
+ ? 'text-primary-600'
+ : 'text-gray-500'
+ }
+ `}
+ >
+ {step.label}
+
+
+ {index < steps.length - 1 && (
+ step.number
+ ? 'bg-primary-500'
+ : 'bg-gray-200'
+ }
+ `}
+ />
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Error Display */}
+ {error && (
+
+ )}
+
+ {/* Step Content */}
+ {currentStep === 2 ? (
+
+ ) : (
+
+ {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
+
+
+
+
+
+ );
+};
+
diff --git a/src/components/checkout/PromotionalPanel.tsx b/src/components/checkout/PromotionalPanel.tsx
new file mode 100644
index 0000000..f061544
--- /dev/null
+++ b/src/components/checkout/PromotionalPanel.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+export const PromotionalPanel: React.FC = () => {
+ return (
+
+
+
+
+
+ Start searching for homes or posting ads now.
+
+
+ Interfaces are well designed for all ages and target audiences are extremely simple and work with social media integrations
+
+
+
+
+
+ );
+};
+
diff --git a/src/components/checkout/TransactionConfirmation.tsx b/src/components/checkout/TransactionConfirmation.tsx
new file mode 100644
index 0000000..7533383
--- /dev/null
+++ b/src/components/checkout/TransactionConfirmation.tsx
@@ -0,0 +1,114 @@
+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 = ({
+ transactionData,
+ onConfirm,
+ onCancel,
+ isLoading = false,
+}) => {
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount);
+ };
+
+ return (
+
+
+
+ Review Transaction
+
+
+ Please review the details before confirming
+
+
+
+
+
+
+ Amount
+
+ {formatCurrency(transactionData.amount)}
+
+
+
+
+
+
Recipient
+
{transactionData.recipient}
+
+
+ {transactionData.recipientEmail && (
+
+
Email
+
{transactionData.recipientEmail}
+
+ )}
+
+
+
Account Type
+
+ {transactionData.accountType}
+
+
+
+ {transactionData.description && (
+
+
Description
+
{transactionData.description}
+
+ )}
+
+
+
+
+
+
+
+
+
+ Transaction Review
+
+
+ This transaction will be reviewed for suspicious activity. You will be notified once the review is complete.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
diff --git a/src/components/checkout/TransactionStatus.tsx b/src/components/checkout/TransactionStatus.tsx
new file mode 100644
index 0000000..0dac3dd
--- /dev/null
+++ b/src/components/checkout/TransactionStatus.tsx
@@ -0,0 +1,156 @@
+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 = ({
+ 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 (
+
+
+
+
+
+ {statusConfig.title}
+
+
+ {statusConfig.message}
+
+
+
+
+
+
+ Transaction ID
+
+ {transaction.id}
+
+
+
+
+ Amount
+
+ {formatCurrency(transaction.amount)}
+
+
+
+
+
+
Recipient
+
{transaction.recipient}
+
+
+ {transaction.recipientEmail && (
+
+
Email
+
{transaction.recipientEmail}
+
+ )}
+
+
+
Account Type
+
+ {transaction.accountType}
+
+
+
+
+
Status
+
+ {transaction.status}
+
+
+
+ {transaction.description && (
+
+
Description
+
{transaction.description}
+
+ )}
+
+
+
+
+ {onNewTransaction && (
+
+
+
+ )}
+
+
+ );
+};
+
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
new file mode 100644
index 0000000..2c2008c
--- /dev/null
+++ b/src/components/layout/Layout.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import { FiHome, FiShield, FiUsers, FiAlertTriangle } from 'react-icons/fi';
+
+interface LayoutProps {
+ children: React.ReactNode;
+}
+
+export const Layout: React.FC = ({ children }) => {
+ const location = useLocation();
+ const isAdmin = location.pathname.startsWith('/admin');
+
+ return (
+
+ {isAdmin && (
+
+ )}
+
{children}
+
+ );
+};
+
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
new file mode 100644
index 0000000..45363aa
--- /dev/null
+++ b/src/components/ui/Button.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary' | 'outline' | 'danger';
+ size?: 'sm' | 'md' | 'lg';
+ isLoading?: boolean;
+ children: React.ReactNode;
+}
+
+export const Button: React.FC = ({
+ variant = 'primary',
+ size = 'md',
+ isLoading = false,
+ className = '',
+ disabled,
+ children,
+ ...props
+}) => {
+ const baseStyles = 'font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
+
+ const variantStyles = {
+ primary: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500',
+ secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500',
+ outline: 'border-2 border-primary-500 text-primary-500 hover:bg-primary-50 focus:ring-primary-500',
+ danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-500',
+ };
+
+ const sizeStyles = {
+ sm: 'px-3 py-1.5 text-sm',
+ md: 'px-4 py-2 text-base',
+ lg: 'px-6 py-3 text-lg',
+ };
+
+ return (
+
+ );
+};
+
diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx
new file mode 100644
index 0000000..9001354
--- /dev/null
+++ b/src/components/ui/Card.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+interface CardProps {
+ children: React.ReactNode;
+ className?: string;
+ onClick?: () => void;
+ selected?: boolean;
+}
+
+export const Card: React.FC = ({
+ children,
+ className = '',
+ onClick,
+ selected = false,
+}) => {
+ const baseStyles = 'rounded-lg border transition-all';
+
+ const selectedStyles = selected
+ ? 'border-primary-500 bg-primary-50'
+ : 'border-gray-200 bg-white hover:border-gray-300';
+
+ const clickableStyles = onClick ? 'cursor-pointer' : '';
+
+ return (
+
+ {children}
+
+ );
+};
+
diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx
new file mode 100644
index 0000000..92897d3
--- /dev/null
+++ b/src/components/ui/Input.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+
+interface InputProps extends React.InputHTMLAttributes {
+ label?: string;
+ error?: string;
+ helperText?: string;
+}
+
+export const Input: React.FC = ({
+ label,
+ error,
+ helperText,
+ className = '',
+ id,
+ ...props
+}) => {
+ const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
+
+ return (
+
+ {label && (
+
+ )}
+
+ {error && (
+
{error}
+ )}
+ {helperText && !error && (
+
{helperText}
+ )}
+
+ );
+};
+
diff --git a/src/contexts/CheckoutContext.tsx b/src/contexts/CheckoutContext.tsx
new file mode 100644
index 0000000..7187ab0
--- /dev/null
+++ b/src/contexts/CheckoutContext.tsx
@@ -0,0 +1,107 @@
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+import type { FundRequest, AccountType, Transaction } from '../types';
+import { api } from '../utils/api';
+
+interface CheckoutContextType {
+ currentStep: number;
+ fundRequest: Partial | null;
+ selectedAccountType: AccountType | null;
+ transaction: Transaction | null;
+ isLoading: boolean;
+ error: string | null;
+ setFundRequest: (data: Partial) => void;
+ setAccountType: (type: AccountType) => void;
+ submitTransaction: () => Promise;
+ reset: () => void;
+ goToStep: (step: number) => void;
+}
+
+const CheckoutContext = createContext(undefined);
+
+export const CheckoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+ const [currentStep, setCurrentStep] = useState(1);
+ const [fundRequest, setFundRequestState] = useState | null>(null);
+ const [selectedAccountType, setSelectedAccountType] = useState(null);
+ const [transaction, setTransaction] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const setFundRequest = (data: Partial) => {
+ setFundRequestState(data);
+ setError(null);
+ };
+
+ const setAccountType = (type: AccountType) => {
+ setSelectedAccountType(type);
+ setError(null);
+ };
+
+ const submitTransaction = async () => {
+ if (!fundRequest || !selectedAccountType) {
+ setError('Please complete all required fields');
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const transactionData: FundRequest = {
+ ...fundRequest,
+ accountType: selectedAccountType,
+ } as FundRequest;
+
+ const newTransaction = await api.createTransaction(transactionData);
+ setTransaction(newTransaction);
+ setCurrentStep(4);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to create transaction');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const reset = () => {
+ setCurrentStep(1);
+ setFundRequestState(null);
+ setSelectedAccountType(null);
+ setTransaction(null);
+ setError(null);
+ setIsLoading(false);
+ };
+
+ const goToStep = (step: number) => {
+ if (step >= 1 && step <= 4) {
+ setCurrentStep(step);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useCheckout = () => {
+ const context = useContext(CheckoutContext);
+ if (context === undefined) {
+ throw new Error('useCheckout must be used within a CheckoutProvider');
+ }
+ return context;
+};
+
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..0a3620b
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,17 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
+
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..9aa0f48
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.tsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
+
diff --git a/src/pages/AccountsPage.tsx b/src/pages/AccountsPage.tsx
new file mode 100644
index 0000000..44f4e60
--- /dev/null
+++ b/src/pages/AccountsPage.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { AccountFlagging } from '../components/admin/AccountFlagging';
+
+export const AccountsPage: React.FC = () => {
+ return ;
+};
+
diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx
new file mode 100644
index 0000000..35685cd
--- /dev/null
+++ b/src/pages/AdminPage.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { AdminDashboard } from '../components/admin/AdminDashboard';
+
+export const AdminPage: React.FC = () => {
+ return ;
+};
+
diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx
new file mode 100644
index 0000000..f624948
--- /dev/null
+++ b/src/pages/CheckoutPage.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { CheckoutFlow } from '../components/checkout/CheckoutFlow';
+
+export const CheckoutPage: React.FC = () => {
+ return (
+
+
+
+ );
+};
+
diff --git a/src/pages/SuspiciousPage.tsx b/src/pages/SuspiciousPage.tsx
new file mode 100644
index 0000000..fdd7fc1
--- /dev/null
+++ b/src/pages/SuspiciousPage.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { SuspiciousTransactions } from '../components/admin/SuspiciousTransactions';
+
+export const SuspiciousPage: React.FC = () => {
+ return ;
+};
+
diff --git a/src/pages/TransactionsPage.tsx b/src/pages/TransactionsPage.tsx
new file mode 100644
index 0000000..9bb5e6c
--- /dev/null
+++ b/src/pages/TransactionsPage.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { TransactionList } from '../components/admin/TransactionList';
+
+export const TransactionsPage: React.FC = () => {
+ return ;
+};
+
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..db7a5f2
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,38 @@
+export type AccountType = 'business' | 'personal';
+
+export type TransactionStatus = 'pending' | 'approved' | 'rejected' | 'suspicious';
+
+export interface Transaction {
+ id: string;
+ amount: number;
+ recipient: string;
+ recipientEmail?: string;
+ description?: string;
+ accountType: AccountType;
+ status: TransactionStatus;
+ createdAt: string;
+ reviewedBy?: string;
+ reviewedAt?: string;
+ rejectionReason?: string;
+}
+
+export interface Account {
+ id: string;
+ email: string;
+ name?: string;
+ accountType: AccountType;
+ isFlagged: boolean;
+ flaggedReason?: string;
+ flaggedAt?: string;
+ flaggedBy?: string;
+ createdAt: string;
+}
+
+export interface FundRequest {
+ amount: number;
+ recipient: string;
+ recipientEmail?: string;
+ description?: string;
+ accountType?: AccountType;
+}
+
diff --git a/src/utils/api.ts b/src/utils/api.ts
new file mode 100644
index 0000000..323172a
--- /dev/null
+++ b/src/utils/api.ts
@@ -0,0 +1,156 @@
+import type { Transaction, Account, FundRequest } from '../types';
+
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';
+
+// Mock data for development
+let mockTransactions: Transaction[] = [
+ {
+ id: '1',
+ amount: 5000,
+ recipient: 'John Doe',
+ recipientEmail: 'john@example.com',
+ description: 'Payment for services',
+ accountType: 'business',
+ status: 'pending',
+ createdAt: new Date().toISOString(),
+ },
+ {
+ id: '2',
+ amount: 1200,
+ recipient: 'Jane Smith',
+ recipientEmail: 'jane@example.com',
+ description: 'Refund',
+ accountType: 'personal',
+ status: 'suspicious',
+ createdAt: new Date(Date.now() - 86400000).toISOString(),
+ },
+];
+
+let mockAccounts: Account[] = [
+ {
+ id: '1',
+ email: 'user1@example.com',
+ name: 'User One',
+ accountType: 'business',
+ isFlagged: false,
+ createdAt: new Date().toISOString(),
+ },
+ {
+ id: '2',
+ email: 'user2@example.com',
+ name: 'User Two',
+ accountType: 'personal',
+ isFlagged: true,
+ flaggedReason: 'Multiple suspicious transactions',
+ flaggedAt: new Date(Date.now() - 172800000).toISOString(),
+ createdAt: new Date(Date.now() - 2592000000).toISOString(),
+ },
+];
+
+// API Client
+class ApiClient {
+ private async request(endpoint: string, options?: RequestInit): Promise {
+ const url = `${API_BASE_URL}${endpoint}`;
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`API Error: ${response.statusText}`);
+ }
+
+ return response.json();
+ }
+
+ // Transaction endpoints
+ async createTransaction(data: FundRequest): Promise {
+ // Mock implementation
+ const transaction: Transaction = {
+ id: Date.now().toString(),
+ ...data,
+ accountType: data.accountType || 'personal',
+ status: 'pending',
+ createdAt: new Date().toISOString(),
+ };
+ mockTransactions.unshift(transaction);
+ return Promise.resolve(transaction);
+ }
+
+ async getTransactions(): Promise {
+ // Mock implementation
+ return Promise.resolve(mockTransactions);
+ }
+
+ async getTransaction(id: string): Promise {
+ // Mock implementation
+ const transaction = mockTransactions.find(t => t.id === id);
+ if (!transaction) {
+ throw new Error('Transaction not found');
+ }
+ return Promise.resolve(transaction);
+ }
+
+ async updateTransactionStatus(
+ id: string,
+ status: Transaction['status'],
+ reason?: string
+ ): Promise {
+ // Mock implementation
+ const transaction = mockTransactions.find(t => t.id === id);
+ if (!transaction) {
+ throw new Error('Transaction not found');
+ }
+ transaction.status = status;
+ transaction.reviewedAt = new Date().toISOString();
+ if (reason) {
+ transaction.rejectionReason = reason;
+ }
+ return Promise.resolve(transaction);
+ }
+
+ // Account endpoints
+ async getAccounts(): Promise {
+ // Mock implementation
+ return Promise.resolve(mockAccounts);
+ }
+
+ async getAccount(id: string): Promise {
+ // Mock implementation
+ const account = mockAccounts.find(a => a.id === id);
+ if (!account) {
+ throw new Error('Account not found');
+ }
+ return Promise.resolve(account);
+ }
+
+ async flagAccount(id: string, reason: string): Promise {
+ // Mock implementation
+ const account = mockAccounts.find(a => a.id === id);
+ if (!account) {
+ throw new Error('Account not found');
+ }
+ account.isFlagged = true;
+ account.flaggedReason = reason;
+ account.flaggedAt = new Date().toISOString();
+ return Promise.resolve(account);
+ }
+
+ async unflagAccount(id: string): Promise {
+ // Mock implementation
+ const account = mockAccounts.find(a => a.id === id);
+ if (!account) {
+ throw new Error('Account not found');
+ }
+ account.isFlagged = false;
+ account.flaggedReason = undefined;
+ account.flaggedAt = undefined;
+ return Promise.resolve(account);
+ }
+}
+
+export const api = new ApiClient();
+
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..dc1dbc6
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,28 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ DEFAULT: '#10B981',
+ 50: '#ECFDF5',
+ 100: '#D1FAE5',
+ 200: '#A7F3D0',
+ 300: '#6EE7B7',
+ 400: '#34D399',
+ 500: '#10B981',
+ 600: '#059669',
+ 700: '#047857',
+ 800: '#065F46',
+ 900: '#064E3B',
+ },
+ },
+ },
+ },
+ plugins: [],
+}
+
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..256e8d8
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
+
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..e428d50
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
+
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..962333c
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
+