Compare commits

...

2 Commits

Author SHA1 Message Date
2cff25c55b 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.
2025-12-11 02:24:01 -05:00
51074d02a9 Refactor liability schemas and enhance asset/invoice repositories
- Updated LiabilityController and LiabilityService to consolidate liability properties into a single 'balance' field for improved clarity.
- Added new methods in AssetRepository to retrieve assets grouped by type.
- Introduced a method in InvoiceRepository to calculate user-specific invoice statistics, including total, paid, outstanding, and overdue invoices.
- Adjusted DashboardService to reflect changes in asset value calculations.
2025-12-11 02:22:48 -05:00
26 changed files with 223 additions and 221 deletions

View File

@@ -253,7 +253,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
### Authentication
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | -------------------- | ---------------------------------- |
| POST | `/api/auth/register` | Register new user |
| POST | `/api/auth/login` | Login, returns JWT + refresh 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ----------------- | ------------------------ |
| GET | `/api/assets` | List all assets for user |
| POST | `/api/assets` | Create new asset |
| 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ---------------------- | ----------------------------- |
| GET | `/api/liabilities` | List all liabilities for user |
| POST | `/api/liabilities` | Create new liability |
| 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | -------------------------- | ---------------------------------------- |
| GET | `/api/net-worth/snapshots` | List snapshots (with date range filter) |
| POST | `/api/net-worth/snapshots` | Create snapshot (auto-calculates totals) |
| 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | --------------------------- | ------------------------------------------- |
| GET | `/api/debts/categories` | List all categories |
| POST | `/api/debts/categories` | Create 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ------------------------- | -------------------------------- |
| GET | `/api/debts/accounts` | List all debt accounts |
| POST | `/api/debts/accounts` | Create debt account |
| 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ---------------------------------- | --------------------------------- |
| GET | `/api/debts/accounts/:id/payments` | List payments for account |
| POST | `/api/debts/accounts/:id/payments` | Record payment (updates 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ------------------ | ----------------------------- |
| GET | `/api/clients` | List all clients |
| POST | `/api/clients` | Create client |
| 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | -------------------------- | -------------------------------------------- |
| GET | `/api/invoices` | List invoices (filterable by status, client) |
| POST | `/api/invoices` | Create 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | -------------------------- | -------------------- |
| GET | `/api/cashflow/income` | List income sources |
| POST | `/api/cashflow/income` | Create 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ---------------------------- | -------------- |
| GET | `/api/cashflow/expenses` | List expenses |
| POST | `/api/cashflow/expenses` | Create 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | -------------------------------- | ----------------------------------------------- |
| GET | `/api/cashflow/transactions` | List transactions (with date range, pagination) |
| POST | `/api/cashflow/transactions` | Create 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
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ------------------------ | ---------------------------- |
| GET | `/api/dashboard/summary` | Get aggregated summary stats |
### Health Check
| Method | Endpoint | Description |
|--------|----------|-------------|
| ------ | ------------- | ------------------------------- |
| 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
3. **JWT Payload:**
```typescript
interface JWTPayload {
sub: string; // user ID
@@ -521,13 +522,13 @@ import path from 'path';
if (process.env.NODE_ENV === 'production') {
app.register(fastifyStatic, {
root: path.join(__dirname, '../public'),
prefix: '/',
prefix: '/'
});
// SPA fallback - serve index.html for all non-API routes
app.setNotFoundHandler((request, reply) => {
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 {
reply.sendFile('index.html');
}
@@ -535,9 +536,9 @@ if (process.env.NODE_ENV === 'production') {
}
// Register all API routes with /api prefix
app.register(authRoutes, { prefix: '/api/auth' });
app.register(assetsRoutes, { prefix: '/api/assets' });
app.register(liabilitiesRoutes, { prefix: '/api/liabilities' });
app.register(authRoutes, {prefix: '/api/auth'});
app.register(assetsRoutes, {prefix: '/api/assets'});
app.register(liabilitiesRoutes, {prefix: '/api/liabilities'});
// ... etc
```
@@ -641,14 +642,14 @@ services:
POSTGRES_PASSWORD: wealth_dev
POSTGRES_DB: wealth
ports:
- "5432:5432"
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
api:
build: .
ports:
- "3000:3000"
- '3000:3000'
environment:
DATABASE_URL: postgresql://wealth:wealth_dev@db:5432/wealth
JWT_SECRET: dev-secret-change-in-production
@@ -807,4 +808,3 @@ interface Transaction {
note?: string;
}
```

View File

@@ -12,7 +12,7 @@ class DatabaseConnection {
public static getInstance(): PrismaClient {
if (!DatabaseConnection.instance) {
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

@@ -6,29 +6,13 @@ import {z} from 'zod';
const createLiabilitySchema = z.object({
name: z.string().min(1).max(255),
type: z.string().min(1),
currentBalance: z.number().min(0),
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
creditor: z.string().max(255).optional(),
notes: z.string().optional()
balance: z.number().min(0)
});
const updateLiabilitySchema = z.object({
name: z.string().min(1).max(255).optional(),
type: z.string().min(1).optional(),
currentBalance: z.number().min(0).optional(),
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
creditor: z.string().max(255).optional(),
notes: z.string().optional()
balance: z.number().min(0).optional()
});
/**

View File

@@ -46,4 +46,24 @@ export class AssetRepository {
return result._sum.value || 0;
}
/**
* Get assets grouped by type
*/
async getByType(userId: string): Promise<Record<string, Asset[]>> {
const assets = await this.findAllByUser(userId);
return assets.reduce(
(acc, asset) => {
const type = asset.type;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(asset);
return acc;
},
{} as Record<string, Asset[]>
);
}
}

View File

@@ -76,4 +76,27 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
return `INV-${year}-${String(count + 1).padStart(3, '0')}`;
}
/**
* Get invoice statistics for a user
*/
async getStats(userId: string): Promise<{
totalInvoices: number;
paidInvoices: number;
outstandingAmount: number;
overdueInvoices: number;
}> {
const invoices = await prisma.invoice.findMany({
where: {userId},
select: {status: true, total: true, dueDate: true}
});
const now = new Date();
const totalInvoices = invoices.length;
const paidInvoices = invoices.filter(inv => inv.status === 'paid').length;
const outstandingAmount = invoices.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled').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};
}
}

View File

@@ -55,7 +55,7 @@ export class DashboardService {
const assetAllocation = Object.entries(assetsByType).map(([type, assets]) => ({
type,
count: assets.length,
totalValue: assets.reduce((sum, asset) => sum + asset.currentValue, 0)
totalValue: assets.reduce((sum, asset) => sum + asset.value, 0)
}));
return {

View File

@@ -5,23 +5,13 @@ import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
export interface CreateLiabilityDTO {
name: string;
type: string;
currentBalance: number;
interestRate?: number;
minimumPayment?: number;
dueDate?: Date;
creditor?: string;
notes?: string;
balance: number;
}
export interface UpdateLiabilityDTO {
name?: string;
type?: string;
currentBalance?: number;
interestRate?: number;
minimumPayment?: number;
dueDate?: Date;
creditor?: string;
notes?: string;
balance?: number;
}
/**
@@ -41,12 +31,7 @@ export class LiabilityService {
return this.liabilityRepository.create({
name: data.name,
type: data.type,
currentBalance: data.currentBalance,
interestRate: data.interestRate,
minimumPayment: data.minimumPayment,
dueDate: data.dueDate,
creditor: data.creditor,
notes: data.notes,
balance: data.balance,
user: {
connect: {id: userId}
}
@@ -85,7 +70,7 @@ export class LiabilityService {
// Verify ownership
await this.getById(id, userId);
if (data.currentBalance !== undefined || data.interestRate !== undefined || data.minimumPayment !== undefined) {
if (data.balance !== undefined) {
this.validateLiabilityData(data as CreateLiabilityDTO);
}
@@ -120,16 +105,8 @@ export class LiabilityService {
* Validate liability data
*/
private validateLiabilityData(data: CreateLiabilityDTO | UpdateLiabilityDTO): void {
if (data.currentBalance !== undefined && data.currentBalance < 0) {
throw new ValidationError('Current balance cannot be negative');
}
if (data.interestRate !== undefined && (data.interestRate < 0 || data.interestRate > 100)) {
throw new ValidationError('Interest rate must be between 0 and 100');
}
if (data.minimumPayment !== undefined && data.minimumPayment < 0) {
throw new ValidationError('Minimum payment cannot be negative');
if (data.balance !== undefined && data.balance < 0) {
throw new ValidationError('Balance cannot be negative');
}
}
}

View File

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

View File

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

View File

@@ -57,8 +57,7 @@ export default function Layout() {
</div>
<button
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" />
<span className="text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">Log out</span>
</button>

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
data: {
name: form.name.trim(),
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: {
name: form.name.trim(),
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 [form, setForm] = useState({
email: '',
password: '',
password: ''
});
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -36,7 +36,7 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
await dispatch(
loginUser({
email: form.email,
password: form.password,
password: form.password
})
).unwrap();

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ class ApiClient {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
...(options.headers as Record<string, string>)
};
if (token) {
@@ -38,7 +38,7 @@ class ApiClient {
try {
const response = await fetch(url, {
...options,
headers,
headers
});
// Handle non-JSON responses
@@ -48,7 +48,7 @@ class ApiClient {
throw {
message: 'Request failed',
statusCode: response.status,
error: response.statusText,
error: response.statusText
} as ApiError;
}
@@ -61,7 +61,7 @@ class ApiClient {
throw {
message: data.message || 'Request failed',
statusCode: response.status,
error: data.error,
error: data.error
} as ApiError;
}
@@ -73,7 +73,7 @@ class ApiClient {
throw {
message: 'Network error',
statusCode: 0,
error: String(error),
error: String(error)
} as ApiError;
}
}
@@ -85,21 +85,21 @@ class ApiClient {
async post<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
body: data ? JSON.stringify(data) : undefined
});
}
async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
body: data ? JSON.stringify(data) : undefined
});
}
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
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> {
return apiClient.delete<void>(`/assets/${id}`);
},
}
};
export const liabilityService = {
@@ -112,7 +112,7 @@ export const liabilityService = {
async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/liabilities/${id}`);
},
}
};
export const snapshotService = {
@@ -124,13 +124,7 @@ export const snapshotService = {
return apiClient.get<{snapshot: NetWorthSnapshot}>(`/networth/snapshots/${id}`);
},
async create(data: {
date: string;
totalAssets: number;
totalLiabilities: number;
netWorth: number;
notes?: string;
}): Promise<{snapshot: NetWorthSnapshot}> {
async create(data: {date: string; totalAssets: number; totalLiabilities: number; netWorth: number; notes?: string}): Promise<{snapshot: NetWorthSnapshot}> {
return apiClient.post<{snapshot: NetWorthSnapshot}>('/networth/snapshots', data);
},
@@ -140,5 +134,5 @@ export const snapshotService = {
async delete(id: string): Promise<void> {
return apiClient.delete<void>(`/networth/snapshots/${id}`);
},
}
};

View File

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

View File

@@ -9,33 +9,33 @@ const features = [
{
icon: TrendingUp,
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,
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,
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,
title: 'Invoicing',
description: 'Create professional invoices and track payments from your clients.',
description: 'Create professional invoices and track payments from your clients.'
},
{
icon: BarChart3,
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,
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() {
@@ -64,9 +64,7 @@ export default function LandingPage() {
{/* Hero */}
<section className="py-20 px-6">
<div className="max-w-3xl mx-auto text-center">
<h1 className="text-4xl font-semibold tracking-tight mb-4">
Take control of your finances
</h1>
<h1 className="text-4xl font-semibold tracking-tight mb-4">Take control of your finances</h1>
<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.
</p>
@@ -86,7 +84,7 @@ export default function LandingPage() {
<div className="max-w-5xl mx-auto">
<h2 className="text-xl font-semibold text-center mb-10">Everything you need</h2>
<div className="grid grid-cols-3 gap-4">
{features.map((feature) => (
{features.map(feature => (
<Card key={feature.title} className="card-elevated">
<CardContent className="p-5">
<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">
<div className="max-w-xl mx-auto text-center">
<h2 className="text-xl font-semibold mb-3">Ready to build wealth?</h2>
<p className="text-muted-foreground mb-6">
Start tracking your finances today. It's free to get started.
</p>
<p className="text-muted-foreground mb-6">Start tracking your finances today. It's free to get started.</p>
<Button size="lg" onClick={() => setSignUpOpen(true)}>
Create your account
</Button>
@@ -116,11 +112,10 @@ export default function LandingPage() {
<section className="py-8 px-6 border-t border-border bg-muted/30">
<div className="max-w-3xl mx-auto">
<p className="text-xs text-muted-foreground text-center leading-relaxed">
<strong>Disclaimer:</strong> This application is for informational and personal tracking purposes only.
It does not constitute financial, investment, tax, or legal advice. The information provided should not
be relied upon for making financial decisions. Always consult with qualified professionals before making
any financial decisions. We make no guarantees about the accuracy or completeness of the data you enter
or the calculations performed. Use at your own risk. Past performance is not indicative of future results.
<strong>Disclaimer:</strong> This application is for informational and personal tracking purposes only. It does not constitute financial,
investment, tax, or legal advice. The information provided should not be relied upon for making financial decisions. Always consult with qualified
professionals before making any financial decisions. We make no guarantees about the accuracy or completeness of the data you enter or the
calculations performed. Use at your own risk. Past performance is not indicative of future results.
</p>
</div>
</section>
@@ -136,9 +131,22 @@ export default function LandingPage() {
</div>
</footer>
<LoginDialog open={loginOpen} onOpenChange={setLoginOpen} onSwitchToSignUp={() => { setLoginOpen(false); setSignUpOpen(true); }} />
<SignUpDialog open={signUpOpen} onOpenChange={setSignUpOpen} onSwitchToLogin={() => { setSignUpOpen(false); setLoginOpen(true); }} />
<LoginDialog
open={loginOpen}
onOpenChange={setLoginOpen}
onSwitchToSignUp={() => {
setLoginOpen(false);
setSignUpOpen(true);
}}
/>
<SignUpDialog
open={signUpOpen}
onOpenChange={setSignUpOpen}
onSwitchToLogin={() => {
setSignUpOpen(false);
setLoginOpen(true);
}}
/>
</div>
);
}

View File

@@ -1,5 +1,12 @@
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 {
id: string;
@@ -66,7 +73,7 @@ const mapApiIncomeToIncome = (apiIncome: ApiIncome): IncomeSource => ({
category: 'Income',
nextDate: new Date().toISOString(),
isActive: true,
createdAt: apiIncome.createdAt || new Date().toISOString(),
createdAt: apiIncome.createdAt || new Date().toISOString()
});
const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
@@ -78,7 +85,7 @@ const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
nextDate: new Date().toISOString(),
isActive: true,
isEssential: apiExpense.isEssential || false,
createdAt: apiExpense.createdAt || new Date().toISOString(),
createdAt: apiExpense.createdAt || new Date().toISOString()
});
const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transaction => ({
@@ -88,7 +95,7 @@ const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transac
amount: apiTransaction.amount,
category: apiTransaction.category || 'Other',
date: apiTransaction.date,
note: apiTransaction.notes,
note: apiTransaction.notes
});
// Async thunks
@@ -210,7 +217,7 @@ const cashflowSlice = createSlice({
state.isLoading = false;
state.error = action.payload as string;
});
},
}
});
export const {

View File

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