feat(frontend): Add balance visibility toggle with blur effect

Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-07 01:37:47 +00:00
committed by Elisiário Couto
parent fabea404ef
commit a592b827aa
13 changed files with 167 additions and 23 deletions

View File

@@ -23,6 +23,7 @@ import {
import { Button } from "./ui/button";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import AccountsSkeleton from "./AccountsSkeleton";
import { BlurredValue } from "./ui/blurred-value";
import type { Account, Balance } from "../types/api";
// Helper function to get status indicator color and styles
@@ -158,7 +159,7 @@ export default function AccountsOverview() {
Total Balance
</p>
<p className="text-2xl font-bold text-foreground">
{formatCurrency(totalBalance)}
<BlurredValue>{formatCurrency(totalBalance)}</BlurredValue>
</p>
</div>
<div className="p-3 bg-green-100 dark:bg-green-900/20 rounded-full">
@@ -369,7 +370,9 @@ export default function AccountsOverview() {
isPositive ? "text-green-600" : "text-red-600"
}`}
>
<BlurredValue>
{formatCurrency(balance, currency)}
</BlurredValue>
</p>
</div>
</div>

View File

@@ -16,6 +16,7 @@ import { apiClient } from "../lib/api";
import { formatCurrency } from "../lib/utils";
import { useState } from "react";
import type { Account } from "../types/api";
import { BlurredValue } from "./ui/blurred-value";
import {
Sidebar,
SidebarContent,
@@ -130,7 +131,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<div className="px-3 pb-2">
<p className="text-xl font-bold text-foreground">
{formatCurrency(totalBalance)}
<BlurredValue>{formatCurrency(totalBalance)}</BlurredValue>
</p>
<p className="text-sm text-muted-foreground">
{accounts?.length || 0} accounts
@@ -163,7 +164,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
"Unnamed Account"}
</p>
<p className="text-xs font-semibold text-foreground">
<BlurredValue>
{formatCurrency(primaryBalance, currency)}
</BlurredValue>
</p>
</div>
</div>

View File

@@ -3,6 +3,7 @@ import { Activity, Wifi, WifiOff } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { apiClient } from "../lib/api";
import { ThemeToggle } from "./ui/theme-toggle";
import { BalanceToggle } from "./ui/balance-toggle";
import { Separator } from "./ui/separator";
import { SidebarTrigger } from "./ui/sidebar";
@@ -77,6 +78,7 @@ export function SiteHeader() {
</>
)}
</div>
<BalanceToggle />
<ThemeToggle />
</div>
</div>

View File

@@ -31,7 +31,8 @@ import { DataTablePagination } from "./ui/data-table-pagination";
import { Card } from "./ui/card";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { Button } from "./ui/button";
import type { Account, Transaction, ApiResponse } from "../types/api";
import { BlurredValue } from "./ui/blurred-value";
import type { Account, Transaction, PaginatedResponse } from "../types/api";
export default function TransactionsTable() {
// Filter state consolidated into a single object
@@ -102,7 +103,7 @@ export default function TransactionsTable() {
isLoading: transactionsLoading,
error: transactionsError,
refetch: refetchTransactions,
} = useQuery<ApiResponse<Transaction[]>>({
} = useQuery<PaginatedResponse<Transaction>>({
queryKey: [
"transactions",
filterState.selectedAccount,
@@ -125,7 +126,16 @@ export default function TransactionsTable() {
});
const transactions = transactionsResponse?.data || [];
const pagination = transactionsResponse?.pagination;
const pagination = transactionsResponse
? {
page: transactionsResponse.page,
total_pages: transactionsResponse.total_pages,
per_page: transactionsResponse.per_page,
total: transactionsResponse.total,
has_next: transactionsResponse.has_next,
has_prev: transactionsResponse.has_prev,
}
: undefined;
// Check if search is currently debouncing
const isSearchLoading = filterState.searchTerm !== debouncedSearchTerm;
@@ -221,11 +231,13 @@ export default function TransactionsTable() {
isPositive ? "text-green-600" : "text-red-600"
}`}
>
<BlurredValue>
{isPositive ? "+" : ""}
{formatCurrency(
transaction.transaction_value,
transaction.transaction_currency,
)}
</BlurredValue>
</p>
</div>
);
@@ -525,11 +537,13 @@ export default function TransactionsTable() {
isPositive ? "text-green-600" : "text-red-600"
}`}
>
<BlurredValue>
{isPositive ? "+" : ""}
{formatCurrency(
transaction.transaction_value,
transaction.transaction_currency,
)}
</BlurredValue>
</p>
<Button
onClick={() => handleViewRaw(transaction)}

View File

@@ -8,6 +8,8 @@ import {
ResponsiveContainer,
Legend,
} from "recharts";
import { useBalanceVisibility } from "../../contexts/BalanceVisibilityContext";
import { cn } from "../../lib/utils";
import type { Balance, Account } from "../../types/api";
interface BalanceChartProps {
@@ -42,6 +44,8 @@ export default function BalanceChart({
accounts,
className,
}: BalanceChartProps) {
const { isBalanceVisible } = useBalanceVisibility();
// Create a lookup map for account info
const accountMap = accounts.reduce(
(map, account) => {
@@ -149,7 +153,7 @@ export default function BalanceChart({
<h3 className="text-lg font-medium text-foreground mb-4">
Balance Progress Over Time
</h3>
<div className="h-80">
<div className={cn("h-80", !isBalanceVisible && "blur-md select-none")}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={finalData}>
<CartesianGrid strokeDasharray="3 3" />

View File

@@ -8,6 +8,8 @@ import {
ResponsiveContainer,
} from "recharts";
import { useQuery } from "@tanstack/react-query";
import { useBalanceVisibility } from "../../contexts/BalanceVisibilityContext";
import { cn } from "../../lib/utils";
import apiClient from "../../lib/api";
interface MonthlyTrendsProps {
@@ -29,6 +31,8 @@ export default function MonthlyTrends({
className,
days = 365,
}: MonthlyTrendsProps) {
const { isBalanceVisible } = useBalanceVisibility();
// Get pre-calculated monthly stats from the new endpoint
const { data: monthlyData, isLoading } = useQuery({
queryKey: ["monthly-stats", days],
@@ -103,7 +107,7 @@ export default function MonthlyTrends({
<h3 className="text-lg font-medium text-foreground mb-4">
{getTitle(days)}
</h3>
<div className="h-80">
<div className={cn("h-80", !isBalanceVisible && "blur-md select-none")}>
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={displayData}

View File

@@ -1,5 +1,6 @@
import type { LucideIcon } from "lucide-react";
import { Card, CardContent } from "../ui/card";
import { BlurredValue } from "../ui/blurred-value";
import { cn } from "../../lib/utils";
interface StatCardProps {
@@ -13,6 +14,7 @@ interface StatCardProps {
};
className?: string;
iconColor?: "green" | "blue" | "red" | "purple" | "orange" | "default";
shouldBlur?: boolean;
}
export default function StatCard({
@@ -23,6 +25,7 @@ export default function StatCard({
trend,
className,
iconColor = "default",
shouldBlur = false,
}: StatCardProps) {
return (
<Card className={cn(className)}>
@@ -31,7 +34,9 @@ export default function StatCard({
<div>
<p className="text-sm font-medium text-muted-foreground">{title}</p>
<div className="flex items-baseline">
<p className="text-2xl font-bold text-foreground">{value}</p>
<p className="text-2xl font-bold text-foreground">
{shouldBlur ? <BlurredValue>{value}</BlurredValue> : value}
</p>
{trend && (
<div
className={cn(

View File

@@ -6,6 +6,8 @@ import {
Tooltip,
Legend,
} from "recharts";
import { useBalanceVisibility } from "../../contexts/BalanceVisibilityContext";
import { BlurredValue } from "../ui/blurred-value";
import type { Account } from "../../types/api";
interface TransactionDistributionProps {
@@ -31,6 +33,8 @@ export default function TransactionDistribution({
accounts,
className,
}: TransactionDistributionProps) {
const { isBalanceVisible } = useBalanceVisibility();
// Helper function to get bank name from institution_id
const getBankName = (institutionId: string): string => {
const bankMapping: Record<string, string> = {
@@ -85,7 +89,8 @@ export default function TransactionDistribution({
<div className="bg-card p-3 border rounded shadow-lg">
<p className="font-medium text-foreground">{data.name}</p>
<p className="text-primary">
Balance: {data.value.toLocaleString()}
Balance:{" "}
<BlurredValue>{data.value.toLocaleString()}</BlurredValue>
</p>
<p className="text-muted-foreground">{percentage}% of total</p>
</div>
@@ -138,7 +143,7 @@ export default function TransactionDistribution({
<span className="text-foreground">{item.name}</span>
</div>
<span className="font-medium text-foreground">
{item.value.toLocaleString()}
<BlurredValue>{item.value.toLocaleString()}</BlurredValue>
</span>
</div>
))}

View File

@@ -0,0 +1,26 @@
import { Eye, EyeOff } from "lucide-react";
import { Button } from "./button";
import { useBalanceVisibility } from "../../contexts/BalanceVisibilityContext";
export function BalanceToggle() {
const { isBalanceVisible, toggleBalanceVisibility } = useBalanceVisibility();
return (
<Button
variant="outline"
size="icon"
onClick={toggleBalanceVisibility}
className="h-8 w-8"
title={isBalanceVisible ? "Hide balances" : "Show balances"}
>
{isBalanceVisible ? (
<Eye className="h-4 w-4" />
) : (
<EyeOff className="h-4 w-4" />
)}
<span className="sr-only">
{isBalanceVisible ? "Hide balances" : "Show balances"}
</span>
</Button>
);
}

View File

@@ -0,0 +1,23 @@
import React from "react";
import { useBalanceVisibility } from "../../contexts/BalanceVisibilityContext";
import { cn } from "../../lib/utils";
interface BlurredValueProps {
children: React.ReactNode;
className?: string;
}
export function BlurredValue({ children, className }: BlurredValueProps) {
const { isBalanceVisible } = useBalanceVisibility();
return (
<span
className={cn(
isBalanceVisible ? "" : "blur-md select-none",
className,
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,48 @@
import React, { createContext, useContext, useEffect, useState } from "react";
interface BalanceVisibilityContextType {
isBalanceVisible: boolean;
toggleBalanceVisibility: () => void;
}
const BalanceVisibilityContext = createContext<
BalanceVisibilityContextType | undefined
>(undefined);
export function BalanceVisibilityProvider({
children,
}: {
children: React.ReactNode;
}) {
const [isBalanceVisible, setIsBalanceVisible] = useState<boolean>(() => {
const stored = localStorage.getItem("balanceVisible");
// Default to true (visible) if not set
return stored === null ? true : stored === "true";
});
useEffect(() => {
localStorage.setItem("balanceVisible", String(isBalanceVisible));
}, [isBalanceVisible]);
const toggleBalanceVisibility = () => {
setIsBalanceVisible((prev) => !prev);
};
return (
<BalanceVisibilityContext.Provider
value={{ isBalanceVisible, toggleBalanceVisibility }}
>
{children}
</BalanceVisibilityContext.Provider>
);
}
export function useBalanceVisibility() {
const context = useContext(BalanceVisibilityContext);
if (context === undefined) {
throw new Error(
"useBalanceVisibility must be used within a BalanceVisibilityProvider",
);
}
return context;
}

View File

@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "./contexts/ThemeContext";
import { BalanceVisibilityProvider } from "./contexts/BalanceVisibilityContext";
import "./index.css";
import { routeTree } from "./routeTree.gen";
import { registerSW } from "virtual:pwa-register";
@@ -73,7 +74,9 @@ createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<BalanceVisibilityProvider>
<RouterProvider router={router} />
</BalanceVisibilityProvider>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>,

View File

@@ -88,6 +88,7 @@ function AnalyticsDashboard() {
subtitle="Inflows this period"
icon={TrendingUp}
iconColor="green"
shouldBlur={true}
/>
<StatCard
title="Total Expenses"
@@ -95,6 +96,7 @@ function AnalyticsDashboard() {
subtitle="Outflows this period"
icon={TrendingDown}
iconColor="red"
shouldBlur={true}
/>
</div>
@@ -106,6 +108,7 @@ function AnalyticsDashboard() {
subtitle="Income minus expenses"
icon={CreditCard}
iconColor={(stats?.net_change || 0) >= 0 ? "green" : "red"}
shouldBlur={true}
/>
<StatCard
title="Average Transaction"
@@ -113,6 +116,7 @@ function AnalyticsDashboard() {
subtitle="Per transaction"
icon={Activity}
iconColor="purple"
shouldBlur={true}
/>
<StatCard
title="Active Accounts"