From 45166f9d20e7f5aeb5a5eee04db6a4b5e6837927 Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Wed, 1 Apr 2026 10:43:50 -0600 Subject: [PATCH] Add privacy mode toggle to blur sensitive financial amounts Eye/EyeOff icon next to theme toggle. Persists in localStorage. Applies CSS blur to all elements marked with data-sensitive attribute across Dashboard, Budget, Pensions, Salarios, and Transactions pages. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 9 ++++--- frontend/src/PrivacyContext.tsx | 27 +++++++++++++++++++ frontend/src/components/DashboardSection.tsx | 2 +- frontend/src/components/Layout.tsx | 7 +++++ frontend/src/components/TransactionList.tsx | 1 + .../src/components/budget/MonthlyDetail.tsx | 23 ++++++++-------- .../budget/RecurringItemsManager.tsx | 2 +- .../src/components/budget/YearlyOverview.tsx | 21 ++++++++------- .../transactions/transaction-columns.tsx | 1 + frontend/src/index.css | 7 +++++ frontend/src/pages/Budget.tsx | 7 ++--- frontend/src/pages/Dashboard.tsx | 16 +++++------ frontend/src/pages/Pensions.tsx | 17 ++++++------ frontend/src/pages/Salarios.tsx | 4 +-- frontend/src/pages/Transactions.tsx | 4 +-- 15 files changed, 100 insertions(+), 48 deletions(-) create mode 100644 frontend/src/PrivacyContext.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d395d51..bb0ec3b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider, useAuth } from './AuthContext'; import { ThemeProvider } from './ThemeContext'; +import { PrivacyProvider } from './PrivacyContext'; import Layout from './components/Layout'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; @@ -47,9 +48,11 @@ export default function App() { return ( - - - + + + + + ); diff --git a/frontend/src/PrivacyContext.tsx b/frontend/src/PrivacyContext.tsx new file mode 100644 index 0000000..9850596 --- /dev/null +++ b/frontend/src/PrivacyContext.tsx @@ -0,0 +1,27 @@ +import { createContext, useContext, useEffect, useState } from 'react'; + +const PrivacyContext = createContext<{ + privacyMode: boolean; + togglePrivacy: () => void; +}>({ privacyMode: false, togglePrivacy: () => {} }); + +export function PrivacyProvider({ children }: { children: React.ReactNode }) { + const [privacyMode, setPrivacyMode] = useState(() => { + return localStorage.getItem('privacyMode') === 'true'; + }); + + useEffect(() => { + document.documentElement.classList.toggle('privacy', privacyMode); + localStorage.setItem('privacyMode', String(privacyMode)); + }, [privacyMode]); + + const togglePrivacy = () => setPrivacyMode((p) => !p); + + return ( + + {children} + + ); +} + +export const usePrivacy = () => useContext(PrivacyContext); diff --git a/frontend/src/components/DashboardSection.tsx b/frontend/src/components/DashboardSection.tsx index eb6a95a..4594440 100644 --- a/frontend/src/components/DashboardSection.tsx +++ b/frontend/src/components/DashboardSection.tsx @@ -58,7 +58,7 @@ export default function DashboardSection({ {settings.label} {total != null && totalCurrency && ( - + {formatAmount(total, totalCurrency)} )} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 0f0e3ac..802f49a 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -10,10 +10,13 @@ import { Menu, Sun, Moon, + Eye, + EyeOff, } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useAuth } from '../AuthContext'; import { useTheme } from '../ThemeContext'; +import { usePrivacy } from '../PrivacyContext'; import { subscribeToPush } from '../pushNotifications'; import { Button } from '@/components/ui/button'; import { @@ -37,6 +40,7 @@ const navItems = [ export default function Layout() { const { logout } = useAuth(); const { theme, toggleTheme } = useTheme(); + const { privacyMode, togglePrivacy } = usePrivacy(); const navigate = useNavigate(); const [mobileOpen, setMobileOpen] = useState(false); @@ -86,6 +90,9 @@ export default function Layout() {
+ diff --git a/frontend/src/components/TransactionList.tsx b/frontend/src/components/TransactionList.tsx index 090992c..94be7d4 100644 --- a/frontend/src/components/TransactionList.tsx +++ b/frontend/src/components/TransactionList.tsx @@ -128,6 +128,7 @@ export default function TransactionList({

(
{item.name} - + {formatAmount(item.amount, 'CRC')}
@@ -70,7 +70,7 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) {
Total - + {formatAmount(detail.total_projected_income, 'CRC')}
@@ -96,7 +96,7 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) { )} -
+
{formatAmount(item.amount, 'CRC')} {item.used_actual && item.projected_amount != null && ( @@ -112,7 +112,7 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) {
Total Fijos - + {formatAmount(detail.total_projected_expenses, 'CRC')}
@@ -139,7 +139,7 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) { {meta.label} ({src.count})
- + {formatAmount(src.net, 'CRC')}
@@ -153,7 +153,7 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) { No cubierto por fijos - {formatAmount(detail.uncovered_actual, 'CRC')} + {formatAmount(detail.uncovered_actual, 'CRC')} )} @@ -176,13 +176,13 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) { {detail.savings_items.map((item) => (
{item.name} - {formatAmount(item.amount, 'CRC')} + {formatAmount(item.amount, 'CRC')}
))}
Total Ahorro - + {formatAmount(detail.total_projected_savings, 'CRC')}
@@ -198,19 +198,19 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) {
Total Ingresos - + +{formatAmount(detail.total_projected_income, 'CRC')}
Gran Total Egresos - + -{formatAmount(detail.gran_total_egresos, 'CRC')}
Ahorro - + -{formatAmount(detail.total_projected_savings, 'CRC')}
@@ -218,6 +218,7 @@ export default function MonthlyDetail({ detail, loading }: MonthlyDetailProps) {
Balance Neto = 0 ? 'text-primary' : 'text-destructive', diff --git a/frontend/src/components/budget/RecurringItemsManager.tsx b/frontend/src/components/budget/RecurringItemsManager.tsx index 417181b..3cff817 100644 --- a/frontend/src/components/budget/RecurringItemsManager.tsx +++ b/frontend/src/components/budget/RecurringItemsManager.tsx @@ -106,7 +106,7 @@ export default function RecurringItemsManager({ ), cell: ({ row }) => ( - + {formatAmount(row.original.amount, row.original.currency)} ), diff --git a/frontend/src/components/budget/YearlyOverview.tsx b/frontend/src/components/budget/YearlyOverview.tsx index 5fd6dc5..7b56acc 100644 --- a/frontend/src/components/budget/YearlyOverview.tsx +++ b/frontend/src/components/budget/YearlyOverview.tsx @@ -120,19 +120,19 @@ export default function YearlyOverview({ )} - + {formatAmount(m.projected_income, 'CRC')} - + {formatAmount(m.projected_fixed_expenses, 'CRC')} - + {formatAmount(m.uncovered_actual, 'CRC')} - + {formatAmount(m.gran_total_egresos, 'CRC')} - + {formatAmount(m.projected_savings, 'CRC')} {isBeforeFreshStart ? '—' - : <> + : {m.carryover_balance >= 0 ? '+' : ''} {formatAmount(m.carryover_balance, 'CRC')} - + } = 0 ? 'text-primary' : 'text-destructive', @@ -193,8 +194,10 @@ export default function YearlyOverview({ {m.balance_overridden && ( )} - {m.cumulative_balance >= 0 ? '+' : ''} - {formatAmount(m.cumulative_balance, 'CRC')} + + {m.cumulative_balance >= 0 ? '+' : ''} + {formatAmount(m.cumulative_balance, 'CRC')} + )} diff --git a/frontend/src/components/transactions/transaction-columns.tsx b/frontend/src/components/transactions/transaction-columns.tsx index eb31cc8..0bf7884 100644 --- a/frontend/src/components/transactions/transaction-columns.tsx +++ b/frontend/src/components/transactions/transaction-columns.tsx @@ -88,6 +88,7 @@ export function getTransactionColumns({ const tx = row.original; return (

Ingresos Anuales

-

+

{formatAmount(projection.annual_income, 'CRC')}

@@ -159,7 +159,7 @@ export default function Budget() {

Egresos Anuales

-

+

{formatAmount(projection.annual_expenses, 'CRC')}

@@ -167,7 +167,7 @@ export default function Budget() {

Ahorro Anual

-

+

{formatAmount(projection.annual_savings, 'CRC')}

@@ -176,6 +176,7 @@ export default function Budget() {

Balance Neto Anual

= 0 ? 'text-primary' : 'text-destructive', diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index c3b9154..3a42d27 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -94,7 +94,7 @@ function AccountRow({

) : (
- + {formatAmount(account.balance, account.currency)} {isLiability && account.next_payment != null && ( - + Next: {formatAmount(account.next_payment, account.currency)} )} @@ -215,10 +215,10 @@ export default function Dashboard() {
- Net {netWorthBreakdown.net < 0 ? '-' : ''}{formatAmount(netWorthBreakdown.net, 'CRC')} + Net {netWorthBreakdown.net < 0 ? '-' : ''}{formatAmount(netWorthBreakdown.net, 'CRC')}
- Assets {formatAmount(netWorthBreakdown.assets, 'CRC')} - Liabilities {formatAmount(netWorthBreakdown.liabilities, 'CRC')} + Assets {formatAmount(netWorthBreakdown.assets, 'CRC')} + Liabilities {formatAmount(netWorthBreakdown.liabilities, 'CRC')}
@@ -262,8 +262,8 @@ export default function Dashboard() { USD/CRC Exchange Rate
- Buy: ₡{exchangeRate.buy_rate.toFixed(2)} - Sell: ₡{exchangeRate.sell_rate.toFixed(2)} + Buy: ₡{exchangeRate.buy_rate.toFixed(2)} + Sell: ₡{exchangeRate.sell_rate.toFixed(2)}
@@ -306,7 +306,7 @@ export default function Dashboard() {

- diff --git a/frontend/src/pages/Pensions.tsx b/frontend/src/pages/Pensions.tsx index dec49cc..8691fb6 100644 --- a/frontend/src/pages/Pensions.tsx +++ b/frontend/src/pages/Pensions.tsx @@ -229,7 +229,7 @@ function ChartTooltipContent({ /> {entry.name} - {formatCRC(entry.value)} + {formatCRC(entry.value)} ))} @@ -440,7 +440,7 @@ export default function Pensions() {

Balance actual

-

+

{formatCRC(fund.startBalance)}

{snap && ( @@ -456,19 +456,19 @@ export default function Pensions() {
Aportes - + {formatCRC(snap.aportes)}
Rendimientos - + {formatCRC(snap.rendimientos)}
Comisión - + {formatCRC(snap.comision)}
@@ -477,7 +477,7 @@ export default function Pensions() {
Aporte mensual - + {formatCRC(fund.monthlyContribution)}
@@ -606,7 +606,7 @@ export default function Pensions() { {fund.isDividend ? `Dividendos ${fund.annualRate}%` : `${fund.annualRate}% anual`}

)} -

+

{earned >= 0 ? '+' : ''}{formatCRC(earned)}

en rendimientos

@@ -686,6 +686,7 @@ export default function Pensions() { Valor en {years} {years === 1 ? 'año' : 'años'}

@@ -855,7 +856,7 @@ export default function Pensions() { {' — '} {new Date(snap.period_end).toLocaleDateString('es-CR', { month: 'short', year: '2-digit' })} - {formatCRC(snap.saldo_final)} + {formatCRC(snap.saldo_final)}

))}
diff --git a/frontend/src/pages/Salarios.tsx b/frontend/src/pages/Salarios.tsx index d9be189..16a185c 100644 --- a/frontend/src/pages/Salarios.tsx +++ b/frontend/src/pages/Salarios.tsx @@ -64,7 +64,7 @@ export default function Salarios() { accessorKey: 'amount', header: ({ column }) => , cell: ({ row }) => ( - + +{formatAmount(row.original.amount, row.original.currency)} ), @@ -126,7 +126,7 @@ export default function Salarios() { Total acumulado - + {formatAmount(summary.total_amount, 'CRC')}
diff --git a/frontend/src/pages/Transactions.tsx b/frontend/src/pages/Transactions.tsx index 4c2c2fe..0f40b3f 100644 --- a/frontend/src/pages/Transactions.tsx +++ b/frontend/src/pages/Transactions.tsx @@ -72,10 +72,10 @@ export default function Transactions() {

{transactions.length} transactions {totalCRC !== 0 && ( - <> · {formatAmount(totalCRC, 'CRC')} + <> · {formatAmount(totalCRC, 'CRC')} )} {totalUSD !== 0 && ( - <> · {formatAmount(totalUSD, 'USD')} + <> · {formatAmount(totalUSD, 'USD')} )}