Update formatting and improve consistency across configuration and documentation files

- Adjusted formatting in .prettierrc for consistent newline handling.
- Enhanced API documentation in BACKEND_PROMPT.md for better readability and structure.
- Updated docker-compose.yml to standardize quotes and improve health check commands.
- Refactored ESLint configuration for better readability and consistency.
- Made minor formatting adjustments in various frontend components for improved user experience and code clarity.
This commit is contained in:
2025-12-11 02:24:01 -05:00
parent 51074d02a9
commit 2cff25c55b
22 changed files with 173 additions and 179 deletions

View File

@@ -253,7 +253,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Authentication ### Authentication
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | -------------------- | ---------------------------------- |
| POST | `/api/auth/register` | Register new user | | POST | `/api/auth/register` | Register new user |
| POST | `/api/auth/login` | Login, returns JWT + refresh token | | POST | `/api/auth/login` | Login, returns JWT + refresh token |
| POST | `/api/auth/refresh` | Refresh access token | | POST | `/api/auth/refresh` | Refresh access token |
@@ -263,7 +263,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Assets ### Assets
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ----------------- | ------------------------ |
| GET | `/api/assets` | List all assets for user | | GET | `/api/assets` | List all assets for user |
| POST | `/api/assets` | Create new asset | | POST | `/api/assets` | Create new asset |
| GET | `/api/assets/:id` | Get asset by ID | | GET | `/api/assets/:id` | Get asset by ID |
@@ -273,7 +273,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Liabilities ### Liabilities
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ---------------------- | ----------------------------- |
| GET | `/api/liabilities` | List all liabilities for user | | GET | `/api/liabilities` | List all liabilities for user |
| POST | `/api/liabilities` | Create new liability | | POST | `/api/liabilities` | Create new liability |
| GET | `/api/liabilities/:id` | Get liability by ID | | GET | `/api/liabilities/:id` | Get liability by ID |
@@ -283,7 +283,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Net Worth Snapshots ### Net Worth Snapshots
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | -------------------------- | ---------------------------------------- |
| GET | `/api/net-worth/snapshots` | List snapshots (with date range filter) | | GET | `/api/net-worth/snapshots` | List snapshots (with date range filter) |
| POST | `/api/net-worth/snapshots` | Create snapshot (auto-calculates totals) | | POST | `/api/net-worth/snapshots` | Create snapshot (auto-calculates totals) |
| GET | `/api/net-worth/current` | Get current net worth calculation | | GET | `/api/net-worth/current` | Get current net worth calculation |
@@ -291,7 +291,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Debt Categories ### Debt Categories
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | --------------------------- | ------------------------------------------- |
| GET | `/api/debts/categories` | List all categories | | GET | `/api/debts/categories` | List all categories |
| POST | `/api/debts/categories` | Create category | | POST | `/api/debts/categories` | Create category |
| PUT | `/api/debts/categories/:id` | Update category | | PUT | `/api/debts/categories/:id` | Update category |
@@ -300,7 +300,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Debt Accounts ### Debt Accounts
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ------------------------- | -------------------------------- |
| GET | `/api/debts/accounts` | List all debt accounts | | GET | `/api/debts/accounts` | List all debt accounts |
| POST | `/api/debts/accounts` | Create debt account | | POST | `/api/debts/accounts` | Create debt account |
| GET | `/api/debts/accounts/:id` | Get account with payment history | | GET | `/api/debts/accounts/:id` | Get account with payment history |
@@ -310,7 +310,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Debt Payments ### Debt Payments
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ---------------------------------- | --------------------------------- |
| GET | `/api/debts/accounts/:id/payments` | List payments for account | | GET | `/api/debts/accounts/:id/payments` | List payments for account |
| POST | `/api/debts/accounts/:id/payments` | Record payment (updates balance) | | POST | `/api/debts/accounts/:id/payments` | Record payment (updates balance) |
| DELETE | `/api/debts/payments/:id` | Delete payment (restores balance) | | DELETE | `/api/debts/payments/:id` | Delete payment (restores balance) |
@@ -318,7 +318,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Clients ### Clients
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ------------------ | ----------------------------- |
| GET | `/api/clients` | List all clients | | GET | `/api/clients` | List all clients |
| POST | `/api/clients` | Create client | | POST | `/api/clients` | Create client |
| GET | `/api/clients/:id` | Get client with invoice stats | | GET | `/api/clients/:id` | Get client with invoice stats |
@@ -328,7 +328,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Invoices ### Invoices
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | -------------------------- | -------------------------------------------- |
| GET | `/api/invoices` | List invoices (filterable by status, client) | | GET | `/api/invoices` | List invoices (filterable by status, client) |
| POST | `/api/invoices` | Create invoice with line items | | POST | `/api/invoices` | Create invoice with line items |
| GET | `/api/invoices/:id` | Get invoice with line items | | GET | `/api/invoices/:id` | Get invoice with line items |
@@ -339,7 +339,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Income Sources ### Income Sources
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | -------------------------- | -------------------- |
| GET | `/api/cashflow/income` | List income sources | | GET | `/api/cashflow/income` | List income sources |
| POST | `/api/cashflow/income` | Create income source | | POST | `/api/cashflow/income` | Create income source |
| PUT | `/api/cashflow/income/:id` | Update income source | | PUT | `/api/cashflow/income/:id` | Update income source |
@@ -348,7 +348,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Expenses ### Expenses
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ---------------------------- | -------------- |
| GET | `/api/cashflow/expenses` | List expenses | | GET | `/api/cashflow/expenses` | List expenses |
| POST | `/api/cashflow/expenses` | Create expense | | POST | `/api/cashflow/expenses` | Create expense |
| PUT | `/api/cashflow/expenses/:id` | Update expense | | PUT | `/api/cashflow/expenses/:id` | Update expense |
@@ -357,7 +357,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Transactions ### Transactions
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | -------------------------------- | ----------------------------------------------- |
| GET | `/api/cashflow/transactions` | List transactions (with date range, pagination) | | GET | `/api/cashflow/transactions` | List transactions (with date range, pagination) |
| POST | `/api/cashflow/transactions` | Create transaction | | POST | `/api/cashflow/transactions` | Create transaction |
| DELETE | `/api/cashflow/transactions/:id` | Delete transaction | | DELETE | `/api/cashflow/transactions/:id` | Delete transaction |
@@ -365,13 +365,13 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Dashboard / Summary ### Dashboard / Summary
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ------------------------ | ---------------------------- |
| GET | `/api/dashboard/summary` | Get aggregated summary stats | | GET | `/api/dashboard/summary` | Get aggregated summary stats |
### Health Check ### Health Check
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| | ------ | ------------- | ------------------------------- |
| GET | `/api/health` | Health check (no auth required) | | GET | `/api/health` | Health check (no auth required) |
--- ---
@@ -390,6 +390,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
- Store refresh token hash in DB or Redis - Store refresh token hash in DB or Redis
3. **JWT Payload:** 3. **JWT Payload:**
```typescript ```typescript
interface JWTPayload { interface JWTPayload {
sub: string; // user ID sub: string; // user ID
@@ -521,13 +522,13 @@ import path from 'path';
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
app.register(fastifyStatic, { app.register(fastifyStatic, {
root: path.join(__dirname, '../public'), root: path.join(__dirname, '../public'),
prefix: '/', prefix: '/'
}); });
// SPA fallback - serve index.html for all non-API routes // SPA fallback - serve index.html for all non-API routes
app.setNotFoundHandler((request, reply) => { app.setNotFoundHandler((request, reply) => {
if (request.url.startsWith('/api')) { if (request.url.startsWith('/api')) {
reply.status(404).send({ success: false, error: { code: 'NOT_FOUND', message: 'Route not found' } }); reply.status(404).send({success: false, error: {code: 'NOT_FOUND', message: 'Route not found'}});
} else { } else {
reply.sendFile('index.html'); reply.sendFile('index.html');
} }
@@ -535,9 +536,9 @@ if (process.env.NODE_ENV === 'production') {
} }
// Register all API routes with /api prefix // Register all API routes with /api prefix
app.register(authRoutes, { prefix: '/api/auth' }); app.register(authRoutes, {prefix: '/api/auth'});
app.register(assetsRoutes, { prefix: '/api/assets' }); app.register(assetsRoutes, {prefix: '/api/assets'});
app.register(liabilitiesRoutes, { prefix: '/api/liabilities' }); app.register(liabilitiesRoutes, {prefix: '/api/liabilities'});
// ... etc // ... etc
``` ```
@@ -641,14 +642,14 @@ services:
POSTGRES_PASSWORD: wealth_dev POSTGRES_PASSWORD: wealth_dev
POSTGRES_DB: wealth POSTGRES_DB: wealth
ports: ports:
- "5432:5432" - '5432:5432'
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
api: api:
build: . build: .
ports: ports:
- "3000:3000" - '3000:3000'
environment: environment:
DATABASE_URL: postgresql://wealth:wealth_dev@db:5432/wealth DATABASE_URL: postgresql://wealth:wealth_dev@db:5432/wealth
JWT_SECRET: dev-secret-change-in-production JWT_SECRET: dev-secret-change-in-production
@@ -807,4 +808,3 @@ interface Transaction {
note?: string; note?: string;
} }
``` ```

View File

@@ -12,7 +12,7 @@ class DatabaseConnection {
public static getInstance(): PrismaClient { public static getInstance(): PrismaClient {
if (!DatabaseConnection.instance) { if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new PrismaClient({ DatabaseConnection.instance = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
}); });
} }

View File

@@ -94,12 +94,8 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
const now = new Date(); const now = new Date();
const totalInvoices = invoices.length; const totalInvoices = invoices.length;
const paidInvoices = invoices.filter(inv => inv.status === 'paid').length; const paidInvoices = invoices.filter(inv => inv.status === 'paid').length;
const outstandingAmount = invoices const outstandingAmount = invoices.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled').reduce((sum, inv) => sum + inv.total, 0);
.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled') const overdueInvoices = invoices.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled' && inv.dueDate < now).length;
.reduce((sum, inv) => sum + inv.total, 0);
const overdueInvoices = invoices.filter(
inv => inv.status !== 'paid' && inv.status !== 'cancelled' && inv.dueDate < now
).length;
return {totalInvoices, paidInvoices, outstandingAmount, overdueInvoices}; return {totalInvoices, paidInvoices, outstandingAmount, overdueInvoices};
} }

View File

@@ -10,11 +10,11 @@ services:
POSTGRES_PASSWORD: wealth_dev POSTGRES_PASSWORD: wealth_dev
POSTGRES_DB: wealth_dev POSTGRES_DB: wealth_dev
ports: ports:
- "5432:5432" - '5432:5432'
volumes: volumes:
- postgres_dev_data:/var/lib/postgresql/data - postgres_dev_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U wealth -d wealth_dev"] test: ['CMD-SHELL', 'pg_isready -U wealth -d wealth_dev']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -28,11 +28,11 @@ services:
POSTGRES_PASSWORD: ${STAGING_DB_PASSWORD:-wealth_staging} POSTGRES_PASSWORD: ${STAGING_DB_PASSWORD:-wealth_staging}
POSTGRES_DB: wealth_staging POSTGRES_DB: wealth_staging
ports: ports:
- "5433:5432" - '5433:5432'
volumes: volumes:
- postgres_staging_data:/var/lib/postgresql/data - postgres_staging_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U wealth -d wealth_staging"] test: ['CMD-SHELL', 'pg_isready -U wealth -d wealth_staging']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -46,11 +46,11 @@ services:
POSTGRES_PASSWORD: ${PROD_DB_PASSWORD:?PROD_DB_PASSWORD is required} POSTGRES_PASSWORD: ${PROD_DB_PASSWORD:?PROD_DB_PASSWORD is required}
POSTGRES_DB: wealth_prod POSTGRES_DB: wealth_prod
ports: ports:
- "5434:5432" - '5434:5432'
volumes: volumes:
- postgres_prod_data:/var/lib/postgresql/data - postgres_prod_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U wealth -d wealth_prod"] test: ['CMD-SHELL', 'pg_isready -U wealth -d wealth_prod']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -63,4 +63,3 @@ volumes:
postgres_dev_data: postgres_dev_data:
postgres_staging_data: postgres_staging_data:
postgres_prod_data: postgres_prod_data:

View File

@@ -28,12 +28,7 @@ export default defineConfig([
// Frontend Web - TypeScript/React files // Frontend Web - TypeScript/React files
{ {
files: ['frontend-web/**/*.{ts,tsx}'], files: ['frontend-web/**/*.{ts,tsx}'],
extends: [ extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite
],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser globals: globals.browser

View File

@@ -57,8 +57,7 @@ export default function Layout() {
</div> </div>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="flex h-9 w-full items-center gap-3 rounded-lg px-2.5 text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors" className="flex h-9 w-full items-center gap-3 rounded-lg px-2.5 text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors">
>
<LogOut className="h-[18px] w-[18px] shrink-0" /> <LogOut className="h-[18px] w-[18px] shrink-0" />
<span className="text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">Log out</span> <span className="text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">Log out</span>
</button> </button>

View File

@@ -10,4 +10,3 @@ export default function ProtectedRoute() {
return <Outlet />; return <Outlet />;
} }

View File

@@ -50,7 +50,7 @@ export default function AddAssetDialog({open, onOpenChange}: Props) {
createAsset({ createAsset({
name: sanitizeString(form.name), name: sanitizeString(form.name),
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER', type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
value: valueNum, value: valueNum
}) })
); );
onOpenChange(false); onOpenChange(false);

View File

@@ -23,7 +23,7 @@ export default function AddLiabilityDialog({open, onOpenChange}: Props) {
createLiability({ createLiability({
name: form.name, name: form.name,
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER', type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
currentBalance: parseFloat(form.balance) || 0, currentBalance: parseFloat(form.balance) || 0
}) })
); );
onOpenChange(false); onOpenChange(false);

View File

@@ -66,7 +66,7 @@ export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
data: { data: {
name: form.name.trim(), name: form.name.trim(),
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER', type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
value: valueNum, value: valueNum
} }
}) })
); );

View File

@@ -66,7 +66,7 @@ export default function EditLiabilityDialog({open, onOpenChange, liability}: Pro
data: { data: {
name: form.name.trim(), name: form.name.trim(),
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER', type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
currentBalance: balanceNum, currentBalance: balanceNum
} }
}) })
); );

View File

@@ -16,7 +16,7 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [form, setForm] = useState({ const [form, setForm] = useState({
email: '', email: '',
password: '', password: ''
}); });
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -36,7 +36,7 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
await dispatch( await dispatch(
loginUser({ loginUser({
email: form.email, email: form.email,
password: form.password, password: form.password
}) })
).unwrap(); ).unwrap();

View File

@@ -18,7 +18,7 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
name: '', name: '',
email: '', email: '',
password: '', password: '',
confirmPassword: '', confirmPassword: ''
}); });
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -45,7 +45,7 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
registerUser({ registerUser({
email: form.email, email: form.email,
password: form.password, password: form.password,
name: form.name, name: form.name
}) })
).unwrap(); ).unwrap();
@@ -117,9 +117,7 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
/> />
</div> </div>
{error && <p className="text-sm text-red-400">{error}</p>} {error && <p className="text-sm text-red-400">{error}</p>}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">By signing up, you agree to our Terms of Service and Privacy Policy.</p>
By signing up, you agree to our Terms of Service and Privacy Policy.
</p>
</div> </div>
<DialogFooter className="flex-col gap-2 sm:flex-col"> <DialogFooter className="flex-col gap-2 sm:flex-col">
<Button type="submit" className="w-full" disabled={isLoading}> <Button type="submit" className="w-full" disabled={isLoading}>
@@ -134,4 +132,3 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
</Dialog> </Dialog>
); );
} }

View File

@@ -69,5 +69,5 @@ export const authService = {
} catch { } catch {
return null; return null;
} }
}, }
}; };

View File

@@ -52,7 +52,7 @@ export const incomeService = {
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/cashflow/income/${id}`); return apiClient.delete<void>(`/cashflow/income/${id}`);
}, }
}; };
export const expenseService = { export const expenseService = {
@@ -70,7 +70,7 @@ export const expenseService = {
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/cashflow/expenses/${id}`); return apiClient.delete<void>(`/cashflow/expenses/${id}`);
}, }
}; };
export const transactionService = { export const transactionService = {
@@ -84,5 +84,5 @@ export const transactionService = {
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/cashflow/transactions/${id}`); return apiClient.delete<void>(`/cashflow/transactions/${id}`);
}, }
}; };

View File

@@ -28,7 +28,7 @@ class ApiClient {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(options.headers as Record<string, string>), ...(options.headers as Record<string, string>)
}; };
if (token) { if (token) {
@@ -38,7 +38,7 @@ class ApiClient {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
...options, ...options,
headers, headers
}); });
// Handle non-JSON responses // Handle non-JSON responses
@@ -48,7 +48,7 @@ class ApiClient {
throw { throw {
message: 'Request failed', message: 'Request failed',
statusCode: response.status, statusCode: response.status,
error: response.statusText, error: response.statusText
} as ApiError; } as ApiError;
} }
@@ -61,7 +61,7 @@ class ApiClient {
throw { throw {
message: data.message || 'Request failed', message: data.message || 'Request failed',
statusCode: response.status, statusCode: response.status,
error: data.error, error: data.error
} as ApiError; } as ApiError;
} }
@@ -73,7 +73,7 @@ class ApiClient {
throw { throw {
message: 'Network error', message: 'Network error',
statusCode: 0, statusCode: 0,
error: String(error), error: String(error)
} as ApiError; } as ApiError;
} }
} }
@@ -85,21 +85,21 @@ class ApiClient {
async post<T>(endpoint: string, data?: unknown): Promise<T> { async post<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, { return this.request<T>(endpoint, {
method: 'POST', method: 'POST',
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined
}); });
} }
async put<T>(endpoint: string, data?: unknown): Promise<T> { async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, { return this.request<T>(endpoint, {
method: 'PUT', method: 'PUT',
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined
}); });
} }
async patch<T>(endpoint: string, data?: unknown): Promise<T> { async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, { return this.request<T>(endpoint, {
method: 'PATCH', method: 'PATCH',
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined
}); });
} }

View File

@@ -90,7 +90,7 @@ export const assetService = {
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/assets/${id}`); return apiClient.delete<void>(`/assets/${id}`);
}, }
}; };
export const liabilityService = { export const liabilityService = {
@@ -112,7 +112,7 @@ export const liabilityService = {
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/liabilities/${id}`); return apiClient.delete<void>(`/liabilities/${id}`);
}, }
}; };
export const snapshotService = { export const snapshotService = {
@@ -124,13 +124,7 @@ export const snapshotService = {
return apiClient.get<{snapshot: NetWorthSnapshot}>(`/networth/snapshots/${id}`); return apiClient.get<{snapshot: NetWorthSnapshot}>(`/networth/snapshots/${id}`);
}, },
async create(data: { async create(data: {date: string; totalAssets: number; totalLiabilities: number; netWorth: number; notes?: string}): Promise<{snapshot: NetWorthSnapshot}> {
date: string;
totalAssets: number;
totalLiabilities: number;
netWorth: number;
notes?: string;
}): Promise<{snapshot: NetWorthSnapshot}> {
return apiClient.post<{snapshot: NetWorthSnapshot}>('/networth/snapshots', data); return apiClient.post<{snapshot: NetWorthSnapshot}>('/networth/snapshots', data);
}, },
@@ -140,5 +134,5 @@ export const snapshotService = {
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/networth/snapshots/${id}`); return apiClient.delete<void>(`/networth/snapshots/${id}`);
}, }
}; };

View File

@@ -33,5 +33,5 @@ export const tokenStorage = {
clear(): void { clear(): void {
this.removeToken(); this.removeToken();
this.removeUser(); this.removeUser();
}, }
}; };

View File

@@ -9,33 +9,33 @@ const features = [
{ {
icon: TrendingUp, icon: TrendingUp,
title: 'Net Worth Tracking', title: 'Net Worth Tracking',
description: 'Monitor your assets and liabilities over time with beautiful charts and insights.', description: 'Monitor your assets and liabilities over time with beautiful charts and insights.'
}, },
{ {
icon: CreditCard, icon: CreditCard,
title: 'Debt Management', title: 'Debt Management',
description: 'Organize and track debt paydown across multiple accounts and categories.', description: 'Organize and track debt paydown across multiple accounts and categories.'
}, },
{ {
icon: ArrowLeftRight, icon: ArrowLeftRight,
title: 'Cashflow Analysis', title: 'Cashflow Analysis',
description: 'Understand your income and expenses to optimize your savings rate.', description: 'Understand your income and expenses to optimize your savings rate.'
}, },
{ {
icon: FileText, icon: FileText,
title: 'Invoicing', title: 'Invoicing',
description: 'Create professional invoices and track payments from your clients.', description: 'Create professional invoices and track payments from your clients.'
}, },
{ {
icon: BarChart3, icon: BarChart3,
title: 'Visual Reports', title: 'Visual Reports',
description: 'Clean, minimal dashboards that put your data front and center.', description: 'Clean, minimal dashboards that put your data front and center.'
}, },
{ {
icon: Shield, icon: Shield,
title: 'Private & Secure', title: 'Private & Secure',
description: 'Your financial data stays on your device. No cloud sync required.', description: 'Your financial data stays on your device. No cloud sync required.'
}, }
]; ];
export default function LandingPage() { export default function LandingPage() {
@@ -64,9 +64,7 @@ export default function LandingPage() {
{/* Hero */} {/* Hero */}
<section className="py-20 px-6"> <section className="py-20 px-6">
<div className="max-w-3xl mx-auto text-center"> <div className="max-w-3xl mx-auto text-center">
<h1 className="text-4xl font-semibold tracking-tight mb-4"> <h1 className="text-4xl font-semibold tracking-tight mb-4">Take control of your finances</h1>
Take control of your finances
</h1>
<p className="text-lg text-muted-foreground mb-8 max-w-xl mx-auto"> <p className="text-lg text-muted-foreground mb-8 max-w-xl mx-auto">
A clean, minimal tool to track your net worth, manage debt, monitor cashflow, and invoice clientsall in one place. A clean, minimal tool to track your net worth, manage debt, monitor cashflow, and invoice clientsall in one place.
</p> </p>
@@ -86,7 +84,7 @@ export default function LandingPage() {
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<h2 className="text-xl font-semibold text-center mb-10">Everything you need</h2> <h2 className="text-xl font-semibold text-center mb-10">Everything you need</h2>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
{features.map((feature) => ( {features.map(feature => (
<Card key={feature.title} className="card-elevated"> <Card key={feature.title} className="card-elevated">
<CardContent className="p-5"> <CardContent className="p-5">
<feature.icon className="h-5 w-5 mb-3 text-muted-foreground" /> <feature.icon className="h-5 w-5 mb-3 text-muted-foreground" />
@@ -103,9 +101,7 @@ export default function LandingPage() {
<section className="py-16 px-6 border-t border-border"> <section className="py-16 px-6 border-t border-border">
<div className="max-w-xl mx-auto text-center"> <div className="max-w-xl mx-auto text-center">
<h2 className="text-xl font-semibold mb-3">Ready to build wealth?</h2> <h2 className="text-xl font-semibold mb-3">Ready to build wealth?</h2>
<p className="text-muted-foreground mb-6"> <p className="text-muted-foreground mb-6">Start tracking your finances today. It's free to get started.</p>
Start tracking your finances today. It's free to get started.
</p>
<Button size="lg" onClick={() => setSignUpOpen(true)}> <Button size="lg" onClick={() => setSignUpOpen(true)}>
Create your account Create your account
</Button> </Button>
@@ -116,11 +112,10 @@ export default function LandingPage() {
<section className="py-8 px-6 border-t border-border bg-muted/30"> <section className="py-8 px-6 border-t border-border bg-muted/30">
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<p className="text-xs text-muted-foreground text-center leading-relaxed"> <p className="text-xs text-muted-foreground text-center leading-relaxed">
<strong>Disclaimer:</strong> This application is for informational and personal tracking purposes only. <strong>Disclaimer:</strong> This application is for informational and personal tracking purposes only. It does not constitute financial,
It does not constitute financial, investment, tax, or legal advice. The information provided should not investment, tax, or legal advice. The information provided should not be relied upon for making financial decisions. Always consult with qualified
be relied upon for making financial decisions. Always consult with qualified professionals before making professionals before making any financial decisions. We make no guarantees about the accuracy or completeness of the data you enter or the
any financial decisions. We make no guarantees about the accuracy or completeness of the data you enter calculations performed. Use at your own risk. Past performance is not indicative of future results.
or the calculations performed. Use at your own risk. Past performance is not indicative of future results.
</p> </p>
</div> </div>
</section> </section>
@@ -136,9 +131,22 @@ export default function LandingPage() {
</div> </div>
</footer> </footer>
<LoginDialog open={loginOpen} onOpenChange={setLoginOpen} onSwitchToSignUp={() => { setLoginOpen(false); setSignUpOpen(true); }} /> <LoginDialog
<SignUpDialog open={signUpOpen} onOpenChange={setSignUpOpen} onSwitchToLogin={() => { setSignUpOpen(false); setLoginOpen(true); }} /> open={loginOpen}
onOpenChange={setLoginOpen}
onSwitchToSignUp={() => {
setLoginOpen(false);
setSignUpOpen(true);
}}
/>
<SignUpDialog
open={signUpOpen}
onOpenChange={setSignUpOpen}
onSwitchToLogin={() => {
setSignUpOpen(false);
setLoginOpen(true);
}}
/>
</div> </div>
); );
} }

View File

@@ -1,5 +1,12 @@
import {createSlice, createAsyncThunk, type PayloadAction} from '@reduxjs/toolkit'; import {createSlice, createAsyncThunk, type PayloadAction} from '@reduxjs/toolkit';
import {incomeService, expenseService, transactionService, type IncomeSource as ApiIncome, type Expense as ApiExpense, type Transaction as ApiTransaction} from '@/lib/api/cashflow.service'; import {
incomeService,
expenseService,
transactionService,
type IncomeSource as ApiIncome,
type Expense as ApiExpense,
type Transaction as ApiTransaction
} from '@/lib/api/cashflow.service';
export interface IncomeSource { export interface IncomeSource {
id: string; id: string;
@@ -66,7 +73,7 @@ const mapApiIncomeToIncome = (apiIncome: ApiIncome): IncomeSource => ({
category: 'Income', category: 'Income',
nextDate: new Date().toISOString(), nextDate: new Date().toISOString(),
isActive: true, isActive: true,
createdAt: apiIncome.createdAt || new Date().toISOString(), createdAt: apiIncome.createdAt || new Date().toISOString()
}); });
const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({ const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
@@ -78,7 +85,7 @@ const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
nextDate: new Date().toISOString(), nextDate: new Date().toISOString(),
isActive: true, isActive: true,
isEssential: apiExpense.isEssential || false, isEssential: apiExpense.isEssential || false,
createdAt: apiExpense.createdAt || new Date().toISOString(), createdAt: apiExpense.createdAt || new Date().toISOString()
}); });
const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transaction => ({ const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transaction => ({
@@ -88,7 +95,7 @@ const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transac
amount: apiTransaction.amount, amount: apiTransaction.amount,
category: apiTransaction.category || 'Other', category: apiTransaction.category || 'Other',
date: apiTransaction.date, date: apiTransaction.date,
note: apiTransaction.notes, note: apiTransaction.notes
}); });
// Async thunks // Async thunks
@@ -210,7 +217,7 @@ const cashflowSlice = createSlice({
state.isLoading = false; state.isLoading = false;
state.error = action.payload as string; state.error = action.payload as string;
}); });
}, }
}); });
export const { export const {

View File

@@ -8,7 +8,7 @@ import {
type CreateAssetRequest, type CreateAssetRequest,
type UpdateAssetRequest, type UpdateAssetRequest,
type CreateLiabilityRequest, type CreateLiabilityRequest,
type UpdateLiabilityRequest, type UpdateLiabilityRequest
} from '@/lib/api/networth.service'; } from '@/lib/api/networth.service';
export interface Asset { export interface Asset {
@@ -57,7 +57,7 @@ const mapApiAssetToAsset = (apiAsset: ApiAsset): Asset => ({
name: apiAsset.name, name: apiAsset.name,
type: apiAsset.type.toLowerCase() as Asset['type'], type: apiAsset.type.toLowerCase() as Asset['type'],
value: apiAsset.value, value: apiAsset.value,
updatedAt: apiAsset.updatedAt || new Date().toISOString(), updatedAt: apiAsset.updatedAt || new Date().toISOString()
}); });
const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({ const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({
@@ -65,7 +65,7 @@ const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({
name: apiLiability.name, name: apiLiability.name,
type: apiLiability.type.toLowerCase() as Liability['type'], type: apiLiability.type.toLowerCase() as Liability['type'],
balance: apiLiability.currentBalance, balance: apiLiability.currentBalance,
updatedAt: apiLiability.updatedAt || new Date().toISOString(), updatedAt: apiLiability.updatedAt || new Date().toISOString()
}); });
// Async thunks for assets // Async thunks for assets
@@ -191,7 +191,7 @@ const netWorthSlice = createSlice({
}, },
setError: (state, action: PayloadAction<string | null>) => { setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload; state.error = action.payload;
}, }
}, },
extraReducers: builder => { extraReducers: builder => {
// Fetch assets // Fetch assets
@@ -267,7 +267,7 @@ const netWorthSlice = createSlice({
state.isLoading = false; state.isLoading = false;
state.error = action.payload as string; state.error = action.payload as string;
}); });
}, }
}); });
export const {setLoading, setError} = netWorthSlice.actions; export const {setLoading, setError} = netWorthSlice.actions;