Add VSCode task for automatic build on project open and update dependencies

Remove .vscode from .gitignore to allow for project-specific settings. Introduce a VSCode task to run `npm run dev` automatically when the project is opened. Update environment configuration files and dependencies to enhance support for the Node adapter.

Fixes #13
This commit is contained in:
GitHub Copilot
2025-05-01 21:05:24 +00:00
parent 8058df41fd
commit b51fe35a16
23 changed files with 342 additions and 401 deletions

View File

@@ -3,7 +3,7 @@ import { formatCurrency } from '../utils';
import type { Account } from '../types';
interface Props {
account: Account;
account: Account;
}
const { account } = Astro.props;
---

View File

@@ -1,11 +1,8 @@
import React, { useState, useEffect } from "react";
import { useStore } from "@nanostores/react";
import type { Account } from "../types";
import { formatCurrency } from "../utils";
import {
currentAccountId as currentAccountIdStore,
refreshKey,
} from "../stores/transactionStore";
import React, { useState, useEffect } from 'react';
import { useStore } from '@nanostores/react';
import type { Account } from '../types';
import { formatCurrency } from '../utils';
import { currentAccountId as currentAccountIdStore, refreshKey } from '../stores/transactionStore';
interface AccountSummaryProps {
// No props needed, data comes from store and fetch
@@ -32,14 +29,12 @@ export default function AccountSummary({}: AccountSummaryProps) {
try {
const response = await fetch(`/api/accounts/${currentAccountId}`);
if (!response.ok) {
throw new Error("Failed to fetch account details");
throw new Error('Failed to fetch account details');
}
const data: Account = await response.json();
setAccount(data);
} catch (err) {
setError(
err instanceof Error ? err.message : "An unknown error occurred"
);
setError(err instanceof Error ? err.message : 'An unknown error occurred');
setAccount(null);
} finally {
setIsLoading(false);
@@ -58,7 +53,7 @@ export default function AccountSummary({}: AccountSummaryProps) {
} else if (account) {
balanceContent = formatCurrency(account.balance);
} else {
balanceContent = "N/A"; // Or some placeholder
balanceContent = 'N/A'; // Or some placeholder
}
return (

View File

@@ -1,13 +1,13 @@
import React, { useState, useEffect } from "react";
import { useStore } from "@nanostores/react";
import type { Transaction } from "../types";
import React, { useState, useEffect } from 'react';
import { useStore } from '@nanostores/react';
import type { Transaction } from '../types';
// Import store atoms and actions
import {
currentAccountId as currentAccountIdStore,
transactionToEdit as transactionToEditStore,
cancelEditingTransaction,
transactionSaved,
} from "../stores/transactionStore";
} from '../stores/transactionStore';
// Remove props that now come from the store
interface AddTransactionFormProps {}
@@ -18,9 +18,9 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
const transactionToEdit = useStore(transactionToEditStore);
// --- State Variables ---
const [date, setDate] = useState("");
const [description, setDescription] = useState("");
const [amount, setAmount] = useState("");
const [date, setDate] = useState('');
const [description, setDescription] = useState('');
const [amount, setAmount] = useState('');
const [editingId, setEditingId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -32,7 +32,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
useEffect(() => {
// Only set default date if not editing
if (!transactionToEdit) {
setDate(new Date().toISOString().split("T")[0]);
setDate(new Date().toISOString().split('T')[0]);
}
}, [transactionToEdit]); // Rerun if edit mode changes
@@ -48,17 +48,14 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
// Directly format the date object (usually interpreted as UTC midnight)
// into the YYYY-MM-DD format required by the input.
// No timezone adjustment needed here.
setDate(dateObj.toISOString().split("T")[0]);
setDate(dateObj.toISOString().split('T')[0]);
} else {
console.warn(
"Invalid date received for editing:",
transactionToEdit.date
);
setDate(""); // Set to empty if invalid
console.warn('Invalid date received for editing:', transactionToEdit.date);
setDate(''); // Set to empty if invalid
}
} catch (e) {
console.error("Error parsing date for editing:", e);
setDate(""); // Set to empty on error
console.error('Error parsing date for editing:', e);
setDate(''); // Set to empty on error
}
setDescription(transactionToEdit.description);
setAmount(transactionToEdit.amount.toString());
@@ -75,9 +72,9 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
// --- Helper Functions ---
const resetForm = () => {
setEditingId(null);
setDate(new Date().toISOString().split("T")[0]);
setDescription("");
setAmount("");
setDate(new Date().toISOString().split('T')[0]);
setDescription('');
setAmount('');
setError(null);
// Don't reset isLoading here, it's handled in submit/cancel
};
@@ -85,28 +82,28 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
const validateForm = (): string[] => {
const errors: string[] = [];
if (!description || description.trim().length < 2) {
errors.push("Description must be at least 2 characters long");
errors.push('Description must be at least 2 characters long');
}
if (!amount) {
errors.push("Amount is required");
errors.push('Amount is required');
} else {
const amountNum = parseFloat(amount);
if (isNaN(amountNum)) {
errors.push("Amount must be a valid number");
errors.push('Amount must be a valid number');
} else if (amountNum === 0) {
errors.push("Amount cannot be zero");
errors.push('Amount cannot be zero');
}
}
if (!date) {
errors.push("Date is required");
errors.push('Date is required');
} else {
try {
const dateObj = new Date(date + "T00:00:00"); // Treat input as local date
const dateObj = new Date(date + 'T00:00:00'); // Treat input as local date
if (isNaN(dateObj.getTime())) {
errors.push("Invalid date format");
errors.push('Invalid date format');
}
} catch (e) {
errors.push("Invalid date format");
errors.push('Invalid date format');
}
}
return errors;
@@ -118,13 +115,13 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
setError(null);
if (isLoading || !currentAccountId) {
if (!currentAccountId) setError("No account selected.");
if (!currentAccountId) setError('No account selected.');
return;
}
const validationErrors = validateForm();
if (validationErrors.length > 0) {
setError(validationErrors.join(". "));
setError(validationErrors.join('. '));
return;
}
@@ -140,21 +137,17 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
amount: parseFloat(amount),
};
const method = editingId ? "PUT" : "POST";
const url = editingId
? `/api/transactions/${editingId}`
: "/api/transactions";
const method = editingId ? 'PUT' : 'POST';
const url = editingId ? `/api/transactions/${editingId}` : '/api/transactions';
const response = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(transactionData),
});
if (!response.ok) {
let errorMsg = `Failed to ${
isEditMode ? "update" : "create"
} transaction`;
let errorMsg = `Failed to ${isEditMode ? 'update' : 'create'} transaction`;
try {
const errorData = await response.json();
errorMsg = errorData.error || errorMsg;
@@ -169,9 +162,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
transactionSaved(savedTransaction); // Call store action instead of prop callback
resetForm(); // Reset form on success
} catch (err) {
setError(
err instanceof Error ? err.message : "An unexpected error occurred"
);
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
} finally {
setIsLoading(false);
}
@@ -185,7 +176,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
// --- JSX ---
return (
<form id="add-transaction-form-react" onSubmit={handleSubmit} noValidate>
<h4>{isEditMode ? "Edit Transaction" : "New Transaction"}</h4>
<h4>{isEditMode ? 'Edit Transaction' : 'New Transaction'}</h4>
{error && <div className="error-message">{error}</div>}
<div className="form-group">
@@ -228,29 +219,18 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
placeholder="e.g. -25.50 or 1200.00"
disabled={isLoading}
/>
<small className="help-text">
Use negative numbers for expenses (e.g., -50.00)
</small>
<small className="help-text">Use negative numbers for expenses (e.g., -50.00)</small>
</div>
<div className="button-group">
<button
type="submit"
className={`form-submit-btn ${isLoading ? "loading" : ""}`}
className={`form-submit-btn ${isLoading ? 'loading' : ''}`}
disabled={isLoading}
>
{isLoading
? "Saving..."
: isEditMode
? "Update Transaction"
: "Save Transaction"}
{isLoading ? 'Saving...' : isEditMode ? 'Update Transaction' : 'Save Transaction'}
</button>
{isEditMode && (
<button
type="button"
className="cancel-btn"
onClick={handleCancel}
disabled={isLoading}
>
<button type="button" className="cancel-btn" onClick={handleCancel} disabled={isLoading}>
Cancel
</button>
)}

View File

@@ -3,7 +3,7 @@ import TransactionTable from './TransactionTable.tsx';
import type { Account } from '../types';
interface Props {
account: Account;
account: Account;
}
const { account } = Astro.props;

View File

@@ -4,8 +4,8 @@ import AddTransactionForm from './AddTransactionForm.tsx';
import type { Account } from '../types';
interface Props {
accounts: Account[];
initialAccount: Account;
accounts: Account[];
initialAccount: Account;
}
const { accounts, initialAccount } = Astro.props;

View File

@@ -3,13 +3,15 @@ import { formatCurrency, formatDate } from '../utils';
import type { Transaction } from '../types';
interface Props {
transactions: Transaction[];
transactions: Transaction[];
}
const { transactions } = Astro.props;
// Sort transactions by date descending for display
const sortedTransactions = [...transactions].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const sortedTransactions = [...transactions].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
// TODO: UI/UX Improvements
// - Add sorting functionality for all columns

View File

@@ -1,13 +1,13 @@
import React, { useState, useEffect } from "react";
import { useStore } from "@nanostores/react";
import type { Transaction } from "../types";
import { formatCurrency, formatDate } from "../utils";
import React, { useState, useEffect } from 'react';
import { useStore } from '@nanostores/react';
import type { Transaction } from '../types';
import { formatCurrency, formatDate } from '../utils';
import {
startEditingTransaction,
currentAccountId as currentAccountIdStore,
triggerRefresh,
refreshKey,
} from "../stores/transactionStore";
} from '../stores/transactionStore';
interface TransactionTableProps {}
@@ -29,18 +29,14 @@ export default function TransactionTable({}: TransactionTableProps) {
setIsLoading(true);
setError(null);
try {
const response = await fetch(
`/api/accounts/${currentAccountId}/transactions`
);
const response = await fetch(`/api/accounts/${currentAccountId}/transactions`);
if (!response.ok) {
throw new Error("Failed to fetch transactions");
throw new Error('Failed to fetch transactions');
}
const data: Transaction[] = await response.json();
setTransactions(data);
} catch (err) {
setError(
err instanceof Error ? err.message : "An unknown error occurred"
);
setError(err instanceof Error ? err.message : 'An unknown error occurred');
setTransactions([]);
} finally {
setIsLoading(false);
@@ -55,7 +51,7 @@ export default function TransactionTable({}: TransactionTableProps) {
);
const handleDelete = async (txnId: string) => {
if (!confirm("Are you sure you want to delete this transaction?")) {
if (!confirm('Are you sure you want to delete this transaction?')) {
return;
}
@@ -63,11 +59,11 @@ export default function TransactionTable({}: TransactionTableProps) {
try {
const response = await fetch(`/api/transactions/${txnId}`, {
method: "DELETE",
method: 'DELETE',
});
if (!response.ok) {
let errorMsg = "Failed to delete transaction";
let errorMsg = 'Failed to delete transaction';
try {
const errorData = await response.json();
errorMsg = errorData.error || errorMsg;
@@ -85,10 +81,8 @@ export default function TransactionTable({}: TransactionTableProps) {
triggerRefresh();
} catch (error) {
alert(
error instanceof Error ? error.message : "Failed to delete transaction"
);
console.error("Delete error:", error);
alert(error instanceof Error ? error.message : 'Failed to delete transaction');
console.error('Delete error:', error);
} finally {
}
};
@@ -97,16 +91,14 @@ export default function TransactionTable({}: TransactionTableProps) {
console.log(`Attempting to edit transaction: ${transaction.id}`);
startEditingTransaction(transaction);
const addTransactionSection = document.getElementById(
"add-transaction-section"
);
const toggleAddTxnBtn = document.getElementById("toggle-add-txn-btn");
if (addTransactionSection?.classList.contains("collapsed")) {
addTransactionSection.classList.replace("collapsed", "expanded");
toggleAddTxnBtn?.setAttribute("aria-expanded", "true");
const addTransactionSection = document.getElementById('add-transaction-section');
const toggleAddTxnBtn = document.getElementById('toggle-add-txn-btn');
if (addTransactionSection?.classList.contains('collapsed')) {
addTransactionSection.classList.replace('collapsed', 'expanded');
toggleAddTxnBtn?.setAttribute('aria-expanded', 'true');
addTransactionSection.scrollIntoView({
behavior: "smooth",
block: "nearest",
behavior: 'smooth',
block: 'nearest',
});
}
};
@@ -114,7 +106,7 @@ export default function TransactionTable({}: TransactionTableProps) {
// Helper function to render loading state
const renderLoading = () => (
<tr>
<td colSpan={4} style={{ textAlign: "center", padding: "2rem" }}>
<td colSpan={4} style={{ textAlign: 'center', padding: '2rem' }}>
Loading transactions...
</td>
</tr>
@@ -126,9 +118,9 @@ export default function TransactionTable({}: TransactionTableProps) {
<td
colSpan={4}
style={{
textAlign: "center",
fontStyle: "italic",
color: "#777",
textAlign: 'center',
fontStyle: 'italic',
color: '#777',
}}
>
No transactions found for this account.
@@ -142,11 +134,7 @@ export default function TransactionTable({}: TransactionTableProps) {
<tr key={txn.id} data-txn-id={txn.id}>
<td>{formatDate(txn.date)}</td>
<td>{txn.description}</td>
<td
className={`amount-col ${
txn.amount >= 0 ? "amount-positive" : "amount-negative"
}`}
>
<td className={`amount-col ${txn.amount >= 0 ? 'amount-positive' : 'amount-negative'}`}>
{formatCurrency(txn.amount)}
</td>
<td>
@@ -169,9 +157,9 @@ export default function TransactionTable({}: TransactionTableProps) {
));
return (
<div id="transaction-section" className={isLoading ? "loading" : ""}>
<div id="transaction-section" className={isLoading ? 'loading' : ''}>
{error && (
<div className="error-message" style={{ padding: "1rem" }}>
<div className="error-message" style={{ padding: '1rem' }}>
Error loading transactions: {error}
</div>
)}
@@ -188,10 +176,10 @@ export default function TransactionTable({}: TransactionTableProps) {
{isLoading
? renderLoading()
: error
? null // Error message is shown above the table
: sortedTransactions.length === 0
? renderEmpty()
: renderRows()}
? null // Error message is shown above the table
: sortedTransactions.length === 0
? renderEmpty()
: renderRows()}
</tbody>
</table>
</div>