diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index afcb2ce..0662457 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -10,7 +10,9 @@
},
"runArgs": ["--dns", "8.8.8.8", "--dns", "8.8.4.4", "--network=host", "--dns-search=."],
"containerEnv": {
- "HOSTALIASES": "/etc/host.aliases"
+ "HOSTALIASES": "/etc/host.aliases",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${localEnv:GITHUB_PERSONAL_ACCESS_TOKEN}",
+ "TZ": "America/New_York"
},
"customizations": {
"vscode": {
@@ -63,14 +65,10 @@
},
"terminal.integrated.defaultProfile.linux": "bash",
"mcp.servers.github": {
- "command": "docker",
+ "command": "bash",
"args": [
- "run",
- "-i",
- "--rm",
- "--env-file",
- "${containerWorkspaceFolder}/.devcontainer/.env",
- "ghcr.io/github/github-mcp-server"
+ "-c",
+ "source ${containerWorkspaceFolder}/.devcontainer/library-scripts/load-env.sh && docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN=\"$GITHUB_PERSONAL_ACCESS_TOKEN\" ghcr.io/github/github-mcp-server"
],
"env": {}
}
@@ -87,5 +85,8 @@
"remoteEnv": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${localEnv:GITHUB_PERSONAL_ACCESS_TOKEN}"
},
- "postStartCommand": "gh auth status || gh auth login"
+ "initializeCommand": {
+ "load-env": "echo 'Preparing environment variables for container startup'"
+ },
+ "postStartCommand": "source ${containerWorkspaceFolder}/.devcontainer/library-scripts/load-env.sh && gh auth status || gh auth login"
}
diff --git a/README.md b/README.md
index 89a547b..7f256b6 100644
--- a/README.md
+++ b/README.md
@@ -75,4 +75,20 @@ import type { Transaction } from '@types';
// ❌ DON'T use relative imports like this
import { transactionService } from '../../../../data/db.service';
-```
\ No newline at end of file
+```
+
+### Enforcing Path Aliases with Biome.js
+
+This project uses [Biome.js](https://biomejs.dev/) for code formatting and linting. Biome enforces the use of path aliases instead of relative imports. To run Biome checks:
+
+```bash
+npm run check
+```
+
+To automatically fix issues:
+
+```bash
+npm run check -- --apply
+```
+
+The Biome configuration (in `biome.json`) includes rules for import sorting and path alias enforcement. To customize the rules, edit the `biome.json` file.
\ No newline at end of file
diff --git a/src/components/AccountSummary.astro b/src/components/AccountSummary.astro
index 8cd4b6e..fac95da 100644
--- a/src/components/AccountSummary.astro
+++ b/src/components/AccountSummary.astro
@@ -1,13 +1,19 @@
---
-import type { Account } from '../types';
-import { formatCurrency } from '../utils';
+import type { Account } from '@types';
+// biome-ignore lint/correctness/noUnusedImports: formatCurrency is used in the template
+import { formatCurrency } from '@utils/formatters';
interface Props {
account: Account;
}
const { account } = Astro.props;
---
+
-
Account Summary
-
Balance: {formatCurrency(account.balance)}
-
\ No newline at end of file
+ Account Summary
+
+ Balance: {formatCurrency(Number(account.balance))}
+
+
diff --git a/src/components/AccountSummary.tsx b/src/components/AccountSummary.tsx
index f96fac9..3664471 100644
--- a/src/components/AccountSummary.tsx
+++ b/src/components/AccountSummary.tsx
@@ -1,8 +1,8 @@
import { useStore } from '@nanostores/react';
+import { currentAccountId as currentAccountIdStore, refreshKey } from '@stores/transactionStore';
+import type { Account } from '@types';
+import { formatCurrency } from '@utils/formatters';
import { useEffect, useState } from 'react';
-import { currentAccountId as currentAccountIdStore, refreshKey } from '../stores/transactionStore';
-import type { Account } from '../types';
-import { formatCurrency } from '../utils';
export default function AccountSummary() {
const currentAccountId = useStore(currentAccountIdStore);
@@ -11,6 +11,7 @@ export default function AccountSummary() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
+ // biome-ignore lint/correctness/useExhaustiveDependencies: refreshCounter is needed to trigger refresh on transaction changes
useEffect(() => {
if (!currentAccountId) {
setAccount(null);
@@ -51,17 +52,17 @@ export default function AccountSummary() {
} else if (error) {
balanceContent = Error: {error};
} else if (account) {
- balanceContent = formatCurrency(account.balance);
+ balanceContent = formatCurrency(Number(account.balance));
} else {
balanceContent = 'N/A';
}
return (
-
Account Summary
-
- Balance: {balanceContent}
-
+
+
Account Summary
+
{balanceContent}
+
);
}
diff --git a/src/components/AddTransactionForm.tsx b/src/components/AddTransactionForm.tsx
index c4e7e98..dc54f78 100644
--- a/src/components/AddTransactionForm.tsx
+++ b/src/components/AddTransactionForm.tsx
@@ -1,13 +1,16 @@
import { useStore } from '@nanostores/react';
-import React, { useEffect, useState } from 'react';
+import * as Form from '@radix-ui/react-form';
+import * as Toast from '@radix-ui/react-toast';
import {
cancelEditingTransaction,
currentAccountId,
loadTransactionsForAccount,
transactionSaved,
transactionToEdit,
- triggerRefresh,
-} from '../stores/transactionStore';
+} from '@stores/transactionStore';
+import type { Transaction } from '@types';
+import type React from 'react';
+import { useEffect, useState } from 'react';
export default function AddTransactionForm() {
const accountId = useStore(currentAccountId);
@@ -18,10 +21,10 @@ export default function AddTransactionForm() {
const [description, setDescription] = useState('');
const [amount, setAmount] = useState('');
const [category, setCategory] = useState('');
- const [type, setType] = useState('WITHDRAWAL');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
+ const [toastOpen, setToastOpen] = useState(false);
// Set initial date only on client-side after component mounts
useEffect(() => {
@@ -30,14 +33,15 @@ export default function AddTransactionForm() {
const today = new Date();
setDate(today.toISOString().split('T')[0]);
}
- }, []);
+ }, [date]);
// Reset form when accountId changes or when switching from edit to add mode
+ // biome-ignore lint/correctness/useExhaustiveDependencies: accountId is needed to trigger form reset when account changes
useEffect(() => {
if (!editingTransaction) {
resetForm();
}
- }, [accountId, editingTransaction === null]);
+ }, [accountId, editingTransaction]); // accountId is intentionally included for form reset on account change
// Populate form when editing a transaction
useEffect(() => {
@@ -60,11 +64,11 @@ export default function AddTransactionForm() {
setDate(dateStr);
setDescription(editingTransaction.description);
- setAmount(String(Math.abs(editingTransaction.amount)));
+ // Set amount directly as positive or negative value
+ setAmount(String(editingTransaction.amount));
setCategory(editingTransaction.category || '');
- setType(editingTransaction.amount < 0 ? 'WITHDRAWAL' : 'DEPOSIT');
}
- }, [editingTransaction]);
+ }, [editingTransaction]); // Add editingTransaction to dependencies
const resetForm = () => {
// Get today's date in YYYY-MM-DD format for the date input
@@ -73,21 +77,86 @@ export default function AddTransactionForm() {
setDescription('');
setAmount('');
setCategory('');
- setType('WITHDRAWAL');
setError(null);
setSuccessMessage(null);
};
+ const validateFormData = () => {
+ if (!editingTransaction && !accountId) {
+ setError('No account selected. Please select an account before adding a transaction.');
+ return false;
+ }
+ if (!date || !description || !amount) {
+ setError('Date, description, and amount are required fields.');
+ return false;
+ }
+ return true;
+ };
+
+ const prepareTransactionData = () => {
+ const isEditing = !!editingTransaction;
+ return {
+ transaction: {
+ accountId: editingTransaction ? editingTransaction.accountId : accountId,
+ date,
+ description,
+ amount: Number.parseFloat(amount),
+ category: category || undefined,
+ },
+ url: isEditing ? `/api/transactions/${editingTransaction.id}` : '/api/transactions',
+ method: isEditing ? 'PUT' : 'POST',
+ };
+ };
+
+ const saveTransaction = async (url: string, method: string, data: Omit) => {
+ const response = await fetch(url, {
+ method,
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ const isEditing = !!editingTransaction;
+ throw new Error(errorData.error || `Failed to ${isEditing ? 'update' : 'add'} transaction`);
+ }
+
+ return response.json() as Promise;
+ };
+
+ const handleSuccess = async (savedTransaction: Transaction) => {
+ const isEditing = !!editingTransaction;
+ setSuccessMessage(`Transaction ${isEditing ? 'updated' : 'added'} successfully!`);
+ setToastOpen(true);
+ resetForm();
+
+ if (isEditing) {
+ cancelEditingTransaction();
+ }
+
+ transactionSaved(savedTransaction);
+
+ // Reload transactions for affected accounts
+ const accountToReload = isEditing ? editingTransaction.accountId : accountId;
+ if (accountToReload) {
+ await loadTransactionsForAccount(accountToReload);
+ }
+ if (isEditing && accountId && editingTransaction.accountId !== accountId) {
+ await loadTransactionsForAccount(accountId);
+ }
+ };
+
+ const handleError = (err: unknown) => {
+ const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred.';
+ setError(errorMessage);
+ const isEditing = !!editingTransaction;
+ console.error(`Transaction ${isEditing ? 'update' : 'submission'} error:`, err);
+ };
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- if (!accountId) {
- setError('No account selected');
- return;
- }
-
- if (!date || !description || !amount) {
- setError('Date, description and amount are required');
+ if (!validateFormData()) {
return;
}
@@ -95,76 +164,12 @@ export default function AddTransactionForm() {
setError(null);
setSuccessMessage(null);
- // Calculate final amount based on type
- const finalAmount = type === 'DEPOSIT' ? Math.abs(Number(amount)) : -Math.abs(Number(amount));
-
try {
- let response;
-
- if (editingTransaction) {
- // Update existing transaction
- response = await fetch(`/api/transactions/${editingTransaction.id}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- accountId,
- date,
- description,
- amount: finalAmount,
- category: category || undefined,
- type,
- }),
- });
- } else {
- // Create new transaction
- response = await fetch('/api/transactions', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- accountId,
- date,
- description,
- amount: finalAmount,
- category: category || undefined,
- type,
- }),
- });
- }
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error || 'Transaction operation failed');
- }
-
- const savedTransaction = await response.json();
-
- // Handle success
- setSuccessMessage(
- editingTransaction
- ? 'Transaction updated successfully!'
- : 'Transaction added successfully!',
- );
-
- // Reset form
- resetForm();
-
- // Clear editing state
- if (editingTransaction) {
- cancelEditingTransaction();
- }
-
- // Notify about saved transaction
- transactionSaved(savedTransaction);
-
- // Reload transactions to ensure the list is up to date
- await loadTransactionsForAccount(accountId);
+ const { url, method, transaction } = prepareTransactionData();
+ const savedTransaction = await saveTransaction(url, method, transaction);
+ await handleSuccess(savedTransaction);
} catch (err) {
- setError(err instanceof Error ? err.message : 'An unknown error occurred');
- console.error('Transaction error:', err);
+ handleError(err);
} finally {
setIsSubmitting(false);
}
@@ -181,89 +186,138 @@ export default function AddTransactionForm() {
{editingTransaction ? 'Edit Transaction' : 'Add Transaction'}
- {successMessage &&
{successMessage}
}
-
- {error &&
{error}
}
-
-
);
}
diff --git a/src/data/db.service.ts b/src/data/db.service.ts
index 8f3c8dd..4f971ec 100644
--- a/src/data/db.service.ts
+++ b/src/data/db.service.ts
@@ -1,5 +1,3 @@
-import type { Prisma, PrismaClient } from '@prisma/client';
-import { Decimal } from '@prisma/client/runtime/library';
import type { Account, Transaction } from '@types';
import { prisma } from './prisma';
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index fc23a11..e6d1e2b 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -14,6 +14,7 @@ interface Props {
const { title } = Astro.props;
---
+
@@ -23,9 +24,16 @@ const { title } = Astro.props;
{title}
-
+
+
+
-
+
+
+
-
\ No newline at end of file
+