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:
GW_MC
2026-05-28 04:05:07 +00:00
parent be74bf5fc1
commit 6d983b6cd9
17 changed files with 6355 additions and 215 deletions

2
.gitignore vendored
View File

@@ -28,3 +28,5 @@ dist-ssr
# target
target/
.react-router/

25
components.json Normal file
View 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": {}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
allowBuilds:
esbuild: true
msw: true
onlyBuiltDependencies:
- esbuild

8
react-router.config.ts Normal file
View 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
View 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;

View File

@@ -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;
}
}

View File

@@ -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;

View 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
View 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
View 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))
}

View File

@@ -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
View 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
View 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
View 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>;
}

View File

@@ -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/**'],
},
},
}));