Remove duplicate utility code via shared formatting module #1
@@ -3,32 +3,10 @@ import {
|
|||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
|
||||||
Cell, ResponsiveContainer, ReferenceLine,
|
Cell, ResponsiveContainer, ReferenceLine,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import { MONTH_NAMES, PALETTE, fmt, formatCurrencyShort } from '../utils/formatting.js';
|
||||||
const MONTH_NAMES = [
|
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
|
||||||
'July', 'August', 'September', 'October', 'November', 'December',
|
|
||||||
];
|
|
||||||
|
|
||||||
const MONTH_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
const MONTH_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
|
||||||
const PALETTE = ['#3b82f6', '#8b5cf6', '#ec4899', '#f97316', '#22c55e', '#14b8a6', '#eab308', '#64748b'];
|
|
||||||
|
|
||||||
function fmt(value) {
|
|
||||||
if (value == null) return '—';
|
|
||||||
const num = Number(value);
|
|
||||||
if (isNaN(num)) return '—';
|
|
||||||
const abs = Math.abs(num).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
||||||
return num < 0 ? `-$${abs}` : `$${abs}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCurrencyShort(value) {
|
|
||||||
if (value == null || isNaN(value)) return '';
|
|
||||||
const abs = Math.abs(value);
|
|
||||||
const sign = value < 0 ? '-' : '';
|
|
||||||
if (abs >= 1000) return `${sign}$${(abs / 1000).toFixed(1)}k`;
|
|
||||||
return `${sign}$${abs.toFixed(0)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function surplusClass(value) {
|
function surplusClass(value) {
|
||||||
if (value == null || isNaN(Number(value))) return '';
|
if (value == null || isNaN(Number(value))) return '';
|
||||||
return Number(value) >= 0 ? 'text-success' : 'text-danger';
|
return Number(value) >= 0 ? 'text-success' : 'text-danger';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatCurrency, ordinal } from '../utils/formatting.js';
|
||||||
|
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
'Housing', 'Utilities', 'Subscriptions', 'Insurance',
|
'Housing', 'Utilities', 'Subscriptions', 'Insurance',
|
||||||
@@ -14,19 +15,6 @@ const EMPTY_FORM = {
|
|||||||
variable_amount: false,
|
variable_amount: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatCurrency(value) {
|
|
||||||
const num = parseFloat(value);
|
|
||||||
if (isNaN(num)) return '$0.00';
|
|
||||||
return num.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function ordinal(n) {
|
|
||||||
const int = parseInt(n, 10);
|
|
||||||
if (isNaN(int)) return n;
|
|
||||||
const suffix = ['th', 'st', 'nd', 'rd'];
|
|
||||||
const v = int % 100;
|
|
||||||
return int + (suffix[(v - 20) % 10] || suffix[v] || suffix[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Bills() {
|
function Bills() {
|
||||||
const [bills, setBills] = useState([]);
|
const [bills, setBills] = useState([]);
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { fmt } from '../utils/formatting.js';
|
||||||
function fmt(value) {
|
|
||||||
const num = parseFloat(value) || 0;
|
|
||||||
return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProgressBar({ paid, total }) {
|
function ProgressBar({ paid, total }) {
|
||||||
const pct = total > 0 ? Math.min(100, (paid / total) * 100) : 0;
|
const pct = total > 0 ? Math.min(100, (paid / total) * 100) : 0;
|
||||||
|
|||||||
@@ -3,25 +3,7 @@ import {
|
|||||||
PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer,
|
PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer,
|
||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid,
|
BarChart, Bar, XAxis, YAxis, CartesianGrid,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import { MONTH_NAMES, PALETTE, formatCurrency, formatCurrencyShort } from '../utils/formatting.js';
|
||||||
const MONTH_NAMES = [
|
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
|
||||||
'July', 'August', 'September', 'October', 'November', 'December',
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatCurrency(value) {
|
|
||||||
const num = parseFloat(value) || 0;
|
|
||||||
return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCurrencyShort(value) {
|
|
||||||
const num = parseFloat(value) || 0;
|
|
||||||
if (Math.abs(num) >= 1000) return '$' + (num / 1000).toFixed(1) + 'k';
|
|
||||||
return '$' + num.toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accessible palette that works in light and dark
|
|
||||||
const PALETTE = ['#3b82f6', '#8b5cf6', '#ec4899', '#f97316', '#22c55e', '#14b8a6', '#eab308', '#64748b'];
|
|
||||||
|
|
||||||
function StatCard({ label, value, valueClass }) {
|
function StatCard({ label, value, valueClass }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { MONTH_NAMES, formatCurrency, ordinal } from '../utils/formatting.js';
|
||||||
const MONTH_NAMES = [
|
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
|
||||||
'July', 'August', 'September', 'October', 'November', 'December',
|
|
||||||
];
|
|
||||||
|
|
||||||
function ordinal(n) {
|
|
||||||
const s = ['th', 'st', 'nd', 'rd'];
|
|
||||||
const v = n % 100;
|
|
||||||
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCurrency(value) {
|
|
||||||
const num = parseFloat(value) || 0;
|
|
||||||
return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPayDate(dateStr) {
|
function formatPayDate(dateStr) {
|
||||||
const [year, month, day] = dateStr.split('-').map(Number);
|
const [year, month, day] = dateStr.split('-').map(Number);
|
||||||
|
|||||||
34
client/src/utils/formatting.js
Normal file
34
client/src/utils/formatting.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export const MONTH_NAMES = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Accessible palette that works in light and dark
|
||||||
|
export const PALETTE = ['#3b82f6', '#8b5cf6', '#ec4899', '#f97316', '#22c55e', '#14b8a6', '#eab308', '#64748b'];
|
||||||
|
|
||||||
|
export function formatCurrency(value) {
|
||||||
|
const num = parseFloat(value) || 0;
|
||||||
|
return '$' + num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCurrencyShort(value) {
|
||||||
|
if (value == null || isNaN(value)) return '';
|
||||||
|
const abs = Math.abs(value);
|
||||||
|
const sign = value < 0 ? '-' : '';
|
||||||
|
if (abs >= 1000) return `${sign}$${(abs / 1000).toFixed(1)}k`;
|
||||||
|
return `${sign}$${abs.toFixed(0)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ordinal(n) {
|
||||||
|
const s = ['th', 'st', 'nd', 'rd'];
|
||||||
|
const v = n % 100;
|
||||||
|
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmt(value) {
|
||||||
|
if (value == null) return '—';
|
||||||
|
const num = Number(value);
|
||||||
|
if (isNaN(num)) return '—';
|
||||||
|
const abs = Math.abs(num).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
return num < 0 ? `-$${abs}` : `$${abs}`;
|
||||||
|
}
|
||||||
12
server/src/constants.js
Normal file
12
server/src/constants.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const CONFIG_KEYS = [
|
||||||
|
'paycheck1_day',
|
||||||
|
'paycheck2_day',
|
||||||
|
'paycheck1_gross',
|
||||||
|
'paycheck1_net',
|
||||||
|
'paycheck2_gross',
|
||||||
|
'paycheck2_net',
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = { CONFIG_KEYS };
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
|
const { CONFIG_KEYS } = require('../constants');
|
||||||
const CONFIG_KEYS = [
|
|
||||||
'paycheck1_day',
|
|
||||||
'paycheck2_day',
|
|
||||||
'paycheck1_gross',
|
|
||||||
'paycheck1_net',
|
|
||||||
'paycheck2_gross',
|
|
||||||
'paycheck2_net',
|
|
||||||
];
|
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
paycheck1_day: 1,
|
paycheck1_day: 1,
|
||||||
|
|||||||
@@ -2,15 +2,7 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
const { calcPaymentAmount } = require('./financing');
|
const { calcPaymentAmount } = require('./financing');
|
||||||
|
const { CONFIG_KEYS } = require('../constants');
|
||||||
const CONFIG_KEYS = [
|
|
||||||
'paycheck1_day',
|
|
||||||
'paycheck2_day',
|
|
||||||
'paycheck1_gross',
|
|
||||||
'paycheck1_net',
|
|
||||||
'paycheck2_gross',
|
|
||||||
'paycheck2_net',
|
|
||||||
];
|
|
||||||
|
|
||||||
const CONFIG_DEFAULTS = {
|
const CONFIG_DEFAULTS = {
|
||||||
paycheck1_day: 1,
|
paycheck1_day: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user