feat: restructure application with new routing and layout components
- Added react-router configuration for improved navigation. - Created a new AppLayout component for consistent layout structure. - Implemented TopNavigation and BottomNavigation for enhanced user experience. - Introduced a new Button component with customizable variants and sizes. - Migrated existing App logic to a new structure, removing the old App.tsx. - Updated styles with Tailwind CSS and custom utility functions. - Added error boundary handling for better error management. - Integrated new routes for home and other pages. - Removed unused files and optimized the project structure.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -28,3 +28,5 @@ dist-ssr
|
|||||||
|
|
||||||
# target
|
# target
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
.react-router/
|
||||||
|
|||||||
25
components.json
Normal file
25
components.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "radix-luma",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/App.css",
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "#components",
|
||||||
|
"utils": "#lib/utils",
|
||||||
|
"ui": "#components/ui",
|
||||||
|
"lib": "#lib",
|
||||||
|
"hooks": "#hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
35
package.json
35
package.json
@@ -4,23 +4,44 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"build": "react-router build",
|
||||||
"build": "tsc && vite build",
|
"dev": "react-router dev",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"typecheck": "react-router typegen && tsc",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
|
"imports": {
|
||||||
|
"#components/*": "./src/components/*.tsx",
|
||||||
|
"#lib/*": "./src/lib/*.ts",
|
||||||
|
"#hooks/*": "./src/hooks/*.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
|
"@react-router/node": "^7.15.1",
|
||||||
|
"@react-router/serve": "^7.15.1",
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^1.16.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"@tauri-apps/api": "^2",
|
"react-router": "^7.15.1",
|
||||||
"@tauri-apps/plugin-opener": "^2"
|
"shadcn": "^4.8.1",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"isbot": "^5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.15.1",
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^7.0.4",
|
"vite": "^7.0.4"
|
||||||
"@tauri-apps/cli": "^2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5352
pnpm-lock.yaml
generated
5352
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
allowBuilds:
|
allowBuilds:
|
||||||
esbuild: true
|
esbuild: true
|
||||||
|
msw: true
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
- esbuild
|
- esbuild
|
||||||
|
|||||||
8
react-router.config.ts
Normal file
8
react-router.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Config } from '@react-router/dev/config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Config options...
|
||||||
|
// Server-side render by default, to enable SPA mode set this to `false`
|
||||||
|
ssr: false,
|
||||||
|
appDirectory: 'src',
|
||||||
|
} satisfies Config;
|
||||||
416
src/App copy.tsx
Normal file
416
src/App copy.tsx
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
interface Account {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
account_type: string;
|
||||||
|
account_category: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
id: number;
|
||||||
|
account_id: number;
|
||||||
|
amount: string;
|
||||||
|
transaction_type: string;
|
||||||
|
currency_code: string;
|
||||||
|
exchange_rate: string | null;
|
||||||
|
transaction_date: string;
|
||||||
|
from_account_id: number | null;
|
||||||
|
category_id: number | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'accounts' | 'transactions'>('accounts');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState('');
|
||||||
|
|
||||||
|
// Account states
|
||||||
|
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||||
|
const [accountName, setAccountName] = useState('');
|
||||||
|
const [accountType, setAccountType] = useState('Asset');
|
||||||
|
const [accountCategory, setAccountCategory] = useState('');
|
||||||
|
const [selectedAccountId, setSelectedAccountId] = useState('');
|
||||||
|
|
||||||
|
// Transaction states
|
||||||
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
|
const [txAccountId, setTxAccountId] = useState('');
|
||||||
|
const [txAmount, setTxAmount] = useState('');
|
||||||
|
const [txType, setTxType] = useState('STANDARD');
|
||||||
|
const [txCurrency, setTxCurrency] = useState('USD');
|
||||||
|
const [txExchangeRate, setTxExchangeRate] = useState('');
|
||||||
|
const [txDate, setTxDate] = useState(new Date().toISOString());
|
||||||
|
const [txFromAccountId, setTxFromAccountId] = useState('');
|
||||||
|
const [txCategoryId, setTxCategoryId] = useState('');
|
||||||
|
const [selectedTransactionId, setSelectedTransactionId] = useState('');
|
||||||
|
const [updateTxAmount, setUpdateTxAmount] = useState('');
|
||||||
|
|
||||||
|
const clearMessages = () => {
|
||||||
|
setError('');
|
||||||
|
setSuccess('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== ACCOUNT COMMANDS ====================
|
||||||
|
|
||||||
|
const handleGetAccounts = async () => {
|
||||||
|
clearMessages();
|
||||||
|
try {
|
||||||
|
const result = await invoke<Account[]>('get_accounts');
|
||||||
|
setAccounts(result);
|
||||||
|
setSuccess('Accounts fetched successfully');
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Error: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetAccount = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!selectedAccountId) {
|
||||||
|
setError('Please enter an account ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await invoke<Account | null>('get_account', {
|
||||||
|
id: parseInt(selectedAccountId),
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
setAccounts([result]);
|
||||||
|
setSuccess('Account fetched successfully');
|
||||||
|
} else {
|
||||||
|
setError('Account not found');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Error: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateAccount = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!accountName || !accountCategory) {
|
||||||
|
setError('Please fill in all account fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await invoke<number>('create_account', {
|
||||||
|
request: {
|
||||||
|
name: accountName,
|
||||||
|
category: {
|
||||||
|
name: accountCategory,
|
||||||
|
account_type: accountType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSuccess(`Account created with ID: ${result}`);
|
||||||
|
setAccountName('');
|
||||||
|
setAccountCategory('');
|
||||||
|
setAccountType('Asset');
|
||||||
|
await handleGetAccounts();
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Error: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== TRANSACTION COMMANDS ====================
|
||||||
|
|
||||||
|
const handleGetTransactionsForAccount = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!txAccountId) {
|
||||||
|
setError('Please enter an account ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await invoke<Transaction[]>('get_transactions_for_account', {
|
||||||
|
id: parseInt(txAccountId),
|
||||||
|
});
|
||||||
|
setTransactions(result);
|
||||||
|
setSuccess('Transactions fetched successfully');
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Error: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateTransaction = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!txAccountId || !txAmount || !txCurrency) {
|
||||||
|
setError('Please fill in all required transaction fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await invoke<number>('create_transaction', {
|
||||||
|
request: {
|
||||||
|
account_id: parseInt(txAccountId),
|
||||||
|
amount: txAmount,
|
||||||
|
transaction_type: txType,
|
||||||
|
currency_code: txCurrency,
|
||||||
|
exchange_rate: txExchangeRate || null,
|
||||||
|
transaction_date: txDate,
|
||||||
|
metadata: null,
|
||||||
|
from_account_id: txFromAccountId ? parseInt(txFromAccountId) : null,
|
||||||
|
category_id: txCategoryId ? parseInt(txCategoryId) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSuccess(`Transaction created with ID: ${result}`);
|
||||||
|
setTxAmount('');
|
||||||
|
setTxFromAccountId('');
|
||||||
|
setTxCategoryId('');
|
||||||
|
setTxExchangeRate('');
|
||||||
|
setTxDate(new Date().toISOString());
|
||||||
|
await handleGetTransactionsForAccount();
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Error: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateTransaction = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!selectedTransactionId) {
|
||||||
|
setError('Please select a transaction to update');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!updateTxAmount) {
|
||||||
|
setError('Please enter an amount to update');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await invoke('update_transaction', {
|
||||||
|
id: parseInt(selectedTransactionId),
|
||||||
|
request: {
|
||||||
|
amount: updateTxAmount,
|
||||||
|
account_id: null,
|
||||||
|
transaction_type: null,
|
||||||
|
currency_code: null,
|
||||||
|
exchange_rate: null,
|
||||||
|
transaction_date: null,
|
||||||
|
metadata: null,
|
||||||
|
from_account_id: null,
|
||||||
|
category_id: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSuccess('Transaction updated successfully');
|
||||||
|
setUpdateTxAmount('');
|
||||||
|
setSelectedTransactionId('');
|
||||||
|
await handleGetTransactionsForAccount();
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Error: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container">
|
||||||
|
<h1>Otter - Command Testing UI</h1>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="tabs">
|
||||||
|
<button className={`tab-button ${activeTab === 'accounts' ? 'active' : ''}`} onClick={() => setActiveTab('accounts')}>
|
||||||
|
Accounts
|
||||||
|
</button>
|
||||||
|
<button className={`tab-button ${activeTab === 'transactions' ? 'active' : ''}`} onClick={() => setActiveTab('transactions')}>
|
||||||
|
Transactions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
{error && <div className="message error">{error}</div>}
|
||||||
|
{success && <div className="message success">{success}</div>}
|
||||||
|
|
||||||
|
{/* ACCOUNTS TAB */}
|
||||||
|
{activeTab === 'accounts' && (
|
||||||
|
<div className="tab-content">
|
||||||
|
<h2>Account Management</h2>
|
||||||
|
|
||||||
|
{/* Create Account Form */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3>Create Account</h3>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Account Name:</label>
|
||||||
|
<input type="text" value={accountName} onChange={(e) => setAccountName(e.target.value)} placeholder="e.g., My Checking Account" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Category Name:</label>
|
||||||
|
<input type="text" value={accountCategory} onChange={(e) => setAccountCategory(e.target.value)} placeholder="e.g., Checking" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Account Type:</label>
|
||||||
|
<select value={accountType} onChange={(e) => setAccountType(e.target.value)}>
|
||||||
|
<option value="Asset">Asset</option>
|
||||||
|
<option value="Liability">Liability</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleCreateAccount} className="btn-primary">
|
||||||
|
Create Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Get Single Account */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3>Get Single Account</h3>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Account ID:</label>
|
||||||
|
<input type="number" value={selectedAccountId} onChange={(e) => setSelectedAccountId(e.target.value)} placeholder="Enter account ID" />
|
||||||
|
</div>
|
||||||
|
<button onClick={handleGetAccount} className="btn-primary">
|
||||||
|
Get Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Get All Accounts */}
|
||||||
|
<div className="form-section">
|
||||||
|
<button onClick={handleGetAccounts} className="btn-primary">
|
||||||
|
Get All Accounts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Accounts Table */}
|
||||||
|
{accounts.length > 0 && (
|
||||||
|
<div className="table-section">
|
||||||
|
<h3>Accounts</h3>
|
||||||
|
<table className="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Updated At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{accounts.map((account) => (
|
||||||
|
<tr key={account.id}>
|
||||||
|
<td>{account.id}</td>
|
||||||
|
<td>{account.name}</td>
|
||||||
|
<td>{account.account_type}</td>
|
||||||
|
<td>{account.account_category}</td>
|
||||||
|
<td>{new Date(account.created_at).toLocaleString()}</td>
|
||||||
|
<td>{new Date(account.updated_at).toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TRANSACTIONS TAB */}
|
||||||
|
{activeTab === 'transactions' && (
|
||||||
|
<div className="tab-content">
|
||||||
|
<h2>Transaction Management</h2>
|
||||||
|
|
||||||
|
{/* Create Transaction Form */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3>Create Transaction</h3>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Account ID:</label>
|
||||||
|
<input type="number" value={txAccountId} onChange={(e) => setTxAccountId(e.target.value)} placeholder="Account ID" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Amount:</label>
|
||||||
|
<input type="text" value={txAmount} onChange={(e) => setTxAmount(e.target.value)} placeholder="e.g., 100.50" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Type:</label>
|
||||||
|
<select value={txType} onChange={(e) => setTxType(e.target.value)}>
|
||||||
|
<option value="STANDARD">STANDARD</option>
|
||||||
|
<option value="RECONCILIATION">RECONCILIATION</option>
|
||||||
|
<option value="OPENING_BALANCE">OPENING_BALANCE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Currency Code:</label>
|
||||||
|
<input type="text" value={txCurrency} onChange={(e) => setTxCurrency(e.target.value)} placeholder="USD" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Exchange Rate (Optional):</label>
|
||||||
|
<input type="text" value={txExchangeRate} onChange={(e) => setTxExchangeRate(e.target.value)} placeholder="1.0" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Transaction Date:</label>
|
||||||
|
<input type="datetime-local" value={txDate.slice(0, 16)} onChange={(e) => setTxDate(new Date(e.target.value).toISOString())} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>From Account ID (Optional):</label>
|
||||||
|
<input type="number" value={txFromAccountId} onChange={(e) => setTxFromAccountId(e.target.value)} placeholder="Source account ID" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Category ID (Optional):</label>
|
||||||
|
<input type="number" value={txCategoryId} onChange={(e) => setTxCategoryId(e.target.value)} placeholder="Category ID" />
|
||||||
|
</div>
|
||||||
|
<button onClick={handleCreateTransaction} className="btn-primary">
|
||||||
|
Create Transaction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Update Transaction Form */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3>Update Transaction</h3>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Transaction ID:</label>
|
||||||
|
<input type="number" value={selectedTransactionId} onChange={(e) => setSelectedTransactionId(e.target.value)} placeholder="Enter transaction ID" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>New Amount:</label>
|
||||||
|
<input type="text" value={updateTxAmount} onChange={(e) => setUpdateTxAmount(e.target.value)} placeholder="e.g., 150.75" />
|
||||||
|
</div>
|
||||||
|
<button onClick={handleUpdateTransaction} className="btn-primary">
|
||||||
|
Update Transaction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Get Transactions for Account */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3>Get Transactions for Account</h3>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Account ID:</label>
|
||||||
|
<input type="number" value={txAccountId} onChange={(e) => setTxAccountId(e.target.value)} placeholder="Enter account ID" />
|
||||||
|
</div>
|
||||||
|
<button onClick={handleGetTransactionsForAccount} className="btn-primary">
|
||||||
|
Get Transactions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Transactions Table */}
|
||||||
|
{transactions.length > 0 && (
|
||||||
|
<div className="table-section">
|
||||||
|
<h3>Transactions</h3>
|
||||||
|
<table className="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Account ID</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Currency</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactions.map((tx) => (
|
||||||
|
<tr key={tx.id}>
|
||||||
|
<td>{tx.id}</td>
|
||||||
|
<td>{tx.account_id}</td>
|
||||||
|
<td>{tx.amount}</td>
|
||||||
|
<td>{tx.transaction_type}</td>
|
||||||
|
<td>{tx.currency_code}</td>
|
||||||
|
<td>{new Date(tx.transaction_date).toLocaleString()}</td>
|
||||||
|
<td>{new Date(tx.created_at).toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
438
src/App.css
438
src/App.css
@@ -1,10 +1,10 @@
|
|||||||
.logo.vite:hover {
|
@import 'tailwindcss';
|
||||||
filter: drop-shadow(0 0 2em #747bff);
|
@import "tw-animate-css";
|
||||||
}
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@fontsource-variable/inter";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafb);
|
|
||||||
}
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -19,80 +19,234 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.52 0.105 223.128);
|
||||||
|
--primary-foreground: oklch(0.984 0.019 200.873);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--chart-1: oklch(0.809 0.105 251.813);
|
||||||
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
|
--chart-4: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-5: oklch(0.424 0.199 265.638);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.609 0.126 221.723);
|
||||||
|
--sidebar-primary-foreground: oklch(0.984 0.019 200.873);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin: 0;
|
max-width: 1400px;
|
||||||
padding-top: 10vh;
|
margin: 0 auto;
|
||||||
display: flex;
|
padding: 2rem;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: 0.75s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.tauri:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #24c8db);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
color: #333;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
h2 {
|
||||||
button {
|
color: #555;
|
||||||
border-radius: 8px;
|
margin-top: 2rem;
|
||||||
border: 1px solid transparent;
|
margin-bottom: 1rem;
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #ffffff;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
h3 {
|
||||||
|
color: #777;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
.tab-button:hover {
|
||||||
border-color: #396cd8;
|
color: #333;
|
||||||
}
|
|
||||||
button:active {
|
|
||||||
border-color: #396cd8;
|
|
||||||
background-color: #e8e8e8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
.tab-button.active {
|
||||||
button {
|
color: #007bff;
|
||||||
|
border-bottom-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.message {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c33;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background-color: #efe;
|
||||||
|
color: #3c3;
|
||||||
|
border-left: 4px solid #3c3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-section {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#greet-input {
|
.form-group input::placeholder {
|
||||||
margin-right: 5px;
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-primary {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 86, 179, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.table-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab content */
|
||||||
|
.tab-content {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -101,16 +255,162 @@ button {
|
|||||||
background-color: #2f2f2f;
|
background-color: #2f2f2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
.container {
|
||||||
color: #24c8db;
|
color: #f6f6f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
h1,
|
||||||
button {
|
h2,
|
||||||
color: #ffffff;
|
h3 {
|
||||||
background-color: #0f0f0f98;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
button:active {
|
|
||||||
background-color: #0f0f0f69;
|
.form-section {
|
||||||
|
background: #3a3a3a;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
color: #f6f6f6;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
background: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
border-bottom-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
color: #d0d0d0;
|
||||||
|
border-bottom-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover {
|
||||||
|
background-color: #454545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
border-bottom-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button:hover {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
color: #24c8db;
|
||||||
|
border-bottom-color: #24c8db;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--font-sans: 'Inter Variable', sans-serif;
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.45 0.085 224.283);
|
||||||
|
--primary-foreground: oklch(0.984 0.019 200.873);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
|
--chart-1: oklch(0.809 0.105 251.813);
|
||||||
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
|
--chart-4: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-5: oklch(0.424 0.199 265.638);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.715 0.143 215.221);
|
||||||
|
--sidebar-primary-foreground: oklch(0.302 0.056 229.695);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/App.tsx
51
src/App.tsx
@@ -1,51 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
// import reactLogo from "./assets/react.svg";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import "./App.css";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [greetMsg, setGreetMsg] = useState("");
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
|
|
||||||
async function greet() {
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
|
||||||
setGreetMsg(await invoke("greet", { name }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="container">
|
|
||||||
<h1>Welcome to Tauri + React</h1>
|
|
||||||
|
|
||||||
<div className="row">
|
|
||||||
<a href="https://vite.dev" target="_blank">
|
|
||||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://tauri.app" target="_blank">
|
|
||||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://react.dev" target="_blank">
|
|
||||||
{/* <img src={reactLogo} className="logo react" alt="React logo" /> */}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className="row"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
greet();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="greet-input"
|
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
|
||||||
placeholder="Enter a name..."
|
|
||||||
/>
|
|
||||||
<button type="submit">Greet</button>
|
|
||||||
</form>
|
|
||||||
<p>{greetMsg}</p>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
65
src/components/ui/button.tsx
Normal file
65
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "#lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
outline:
|
||||||
|
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
|
||||||
|
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
52
src/layout.tsx
Normal file
52
src/layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Home, Bell, FileText, User, Menu } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from './components/ui/button';
|
||||||
|
|
||||||
|
function TopNavigation() {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shadow-sm">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-gray-600">Cisko bank</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">$35,500</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setIsMenuOpen(!isMenuOpen)} className="text-gray-700">
|
||||||
|
<Menu className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BottomNavigation() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white border-t border-gray-200 px-6 py-4 flex items-center justify-around">
|
||||||
|
<Button variant="ghost" size="icon" className="text-gray-600 hover:text-blue-500">
|
||||||
|
<Home className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="text-gray-600 hover:text-yellow-500">
|
||||||
|
<Bell className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="text-gray-600 hover:text-blue-500">
|
||||||
|
<FileText className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="text-gray-600 hover:text-blue-500">
|
||||||
|
<User className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-screen ">
|
||||||
|
{/* Top Bar */}
|
||||||
|
<TopNavigation />
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 overflow-auto">{children}</div>
|
||||||
|
|
||||||
|
{/* Bottom Navigation */}
|
||||||
|
<BottomNavigation />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
|
||||||
71
src/root.tsx
Normal file
71
src/root.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
|
||||||
|
|
||||||
|
import type { Route } from './+types/root';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import './App.css';
|
||||||
|
import { AppLayout } from './layout';
|
||||||
|
|
||||||
|
export const links: Route.LinksFunction = () => [
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||||
|
{
|
||||||
|
rel: 'preconnect',
|
||||||
|
href: 'https://fonts.gstatic.com',
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'stylesheet',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Outlet />
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
let message = 'Oops!';
|
||||||
|
let details = 'An unexpected error occurred.';
|
||||||
|
let stack: string | undefined;
|
||||||
|
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
message = error.status === 404 ? '404' : 'Error';
|
||||||
|
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
|
||||||
|
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||||
|
details = error.message;
|
||||||
|
stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="pt-16 p-4 container mx-auto">
|
||||||
|
<h1>{message}</h1>
|
||||||
|
<p>{details}</p>
|
||||||
|
{stack && (
|
||||||
|
<pre className="w-full p-4 overflow-x-auto">
|
||||||
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
src/routes.ts
Normal file
6
src/routes.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type RouteConfig, index } from '@react-router/dev/routes';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
//
|
||||||
|
index('routes/home.tsx'),
|
||||||
|
] satisfies RouteConfig;
|
||||||
14
src/routes/home.tsx
Normal file
14
src/routes/home.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Button } from '../components/ui/button';
|
||||||
|
import type { Route } from './+types/home';
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Home page loaded');
|
||||||
|
}, []);
|
||||||
|
return <Button>Go to the about page</Button>;
|
||||||
|
}
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
import { defineConfig } from "vite";
|
import { reactRouter } from '@react-router/dev/vite';
|
||||||
import react from "@vitejs/plugin-react";
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
// import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
// react(),
|
||||||
|
tailwindcss(),
|
||||||
|
reactRouter(),
|
||||||
|
],
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent Vite from obscuring rust errors
|
// 1. prevent Vite from obscuring rust errors
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
// 2. tauri expects a fixed port, fail if that port is not available
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
@@ -19,14 +28,14 @@ export default defineConfig(async () => ({
|
|||||||
host: host || false,
|
host: host || false,
|
||||||
hmr: host
|
hmr: host
|
||||||
? {
|
? {
|
||||||
protocol: "ws",
|
protocol: 'ws',
|
||||||
host,
|
host,
|
||||||
port: 1421,
|
port: 1421,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
watch: {
|
watch: {
|
||||||
// 3. tell Vite to ignore watching `src-tauri`
|
// 3. tell Vite to ignore watching `src-tauri`
|
||||||
ignored: ["**/src-tauri/**"],
|
ignored: ['**/src-tauri/**'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user