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/
|
||||
|
||||
.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": {}
|
||||
}
|
||||
33
package.json
33
package.json
@@ -4,23 +4,44 @@
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "react-router typegen && tsc",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"imports": {
|
||||
"#components/*": "./src/components/*.tsx",
|
||||
"#lib/*": "./src/lib/*.ts",
|
||||
"#hooks/*": "./src/hooks/*.ts"
|
||||
},
|
||||
"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-dom": "^19.1.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2"
|
||||
"react-router": "^7.15.1",
|
||||
"shadcn": "^4.8.1",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"isbot": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.15.1",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
"@tauri-apps/cli": "^2"
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
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:
|
||||
esbuild: true
|
||||
msw: true
|
||||
onlyBuiltDependencies:
|
||||
- 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 {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
@import 'tailwindcss';
|
||||
@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 {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
@@ -19,80 +19,234 @@
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-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 {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
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;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
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);
|
||||
h2 {
|
||||
color: #555;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
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;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
.tab-button:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
.tab-button.active {
|
||||
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;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
.form-group input::placeholder {
|
||||
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) {
|
||||
@@ -101,16 +255,162 @@ button {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
.container {
|
||||
color: #f6f6f6;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
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 react from "@vitejs/plugin-react";
|
||||
import { reactRouter } from '@react-router/dev/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
// import react from '@vitejs/plugin-react';
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
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`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
@@ -19,14 +28,14 @@ export default defineConfig(async () => ({
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
protocol: 'ws',
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
ignored: ['**/src-tauri/**'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user