Skip to content

feat: error boundaries, loading states, and /privacy page#27

Merged
timothylee58 merged 2 commits into
mainfrom
claude/funny-johnson-bVave
May 30, 2026
Merged

feat: error boundaries, loading states, and /privacy page#27
timothylee58 merged 2 commits into
mainfrom
claude/funny-johnson-bVave

Conversation

@timothylee58

@timothylee58 timothylee58 commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • 3 new shared UI primitives: ErrorBoundary (React class component), Skeleton/SkeletonCard/SkeletonText (shimmer loading), DataError (inline error display with retry, compact + full variants)
  • Hook error states: useRecentTransactions, useBuddies, and useBudget now all expose an error field — previously they swallowed failures silently or always returned null
  • Dashboard error handling: compact DataError banner when budget/freeze/gamification API calls fail; DataError in recent transactions section on fetch failure; DashboardClient wrapped in ErrorBoundary
  • App layout: <main> in (app)/layout.tsx wrapped in ErrorBoundary — catches unexpected render errors on any authenticated page
  • Budget page: try/catch + error state for Supabase queries with retry button; removed unused Save import
  • /privacy: PDPA-compliant privacy policy covering data collection, AI processing (Anthropic/DeepSeek), OCR, retention, user rights, and contact info

Test plan

  • Kill the API server → dashboard shows compact error banner instead of crashing
  • Budget page with no DB connection → DataError with retry button appears
  • Throw an error inside a child component → ErrorBoundary catches it and shows "Try again" card
  • Visit /privacy → full policy page renders without TopBar/BottomNav
  • Skeleton components: hard-refresh dashboard → skeleton shimmers briefly then content loads

https://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp


Generated by Claude Code


Summary by cubic

Adds app-wide error handling with error boundaries and visible loading/error states, plus a new PDPA-compliant /privacy page. Users now see skeletons and clear retry options instead of crashes or silent failures.

  • New Features

    • ErrorBoundary wraps app <main> and DashboardClient to catch render errors with a “Try again” reset; supports onReset for cleanup.
    • Skeleton, SkeletonCard, and SkeletonText for shimmer loading.
    • DataError banner/card with optional Retry (compact and full).
    • New /privacy page detailing data collection, AI processing (Anthropic/DeepSeek), OCR, retention, and user rights.
  • Reliability

    • Hooks surface errors: useRecentTransactions, useBuddies (adds refresh), useBudget.
    • Dashboard shows a compact error banner for failed budget/freeze/gamification fetches and handles recent transactions failures.
    • Budget page adds try/catch for Supabase queries with a retry button; removed unused Save import.
    • Review fixes: removed duplicate useDashboardPulse() call, null-safe guard on pulseErrors, and active flag in useBudget to avoid setState on unmounted.

Written for commit f363fd8. Summary will update on new commits.

Review in cubic

New shared UI primitives:
- ErrorBoundary (class component) — catches unexpected render errors with "Try again" reset
- Skeleton / SkeletonCard / SkeletonText — shimmer loading placeholders
- DataError — inline error display with optional retry button (compact + full variants)

Hook improvements:
- useRecentTransactions: add error state (was silently swallowing failures)
- useBuddies: add error state + expose refresh (was silently falling back to mock)
- useBudget: propagate real error instead of always returning null

Dashboard error handling:
- Show compact DataError banner when useDashboardPulse reports API errors
- Show DataError in recent transactions section on fetch failure
- Wrap DashboardClient in ErrorBoundary
- Wrap (app) layout <main> in ErrorBoundary for all authenticated pages

Budget page:
- Add try/catch + error state for Supabase queries
- Show DataError with retry when budget load fails
- Remove unused Save import

Privacy policy:
- /privacy — full PDPA-compliant privacy policy page
- Covers data collection, AI processing, retention, user rights, contact info

https://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
@netlify

netlify Bot commented May 30, 2026

Copy link
Copy Markdown

Deploy Preview for bajet-buddy failed.

Name Link
🔨 Latest commit f363fd8
🔍 Latest deploy log https://app.netlify.com/projects/bajet-buddy/deploys/6a1b1076ee87de00081d3fce

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
bajet-buddy f363fd8 May 30 2026, 04:30 PM

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces robust error handling and error boundaries across the application, including the addition of reusable DataError, ErrorBoundary, and Skeleton components, as well as updating several hooks (useBuddies, useBudget, useRecentTransactions) to track and return error states. It also adds a new Privacy Policy page. Feedback on these changes includes consolidating a redundant duplicate call to useDashboardPulse() in BehaviorDashboard.tsx, implementing an active flag in useBudget.ts to prevent race conditions on unmounted components, and adding an optional onReset callback prop to the ErrorBoundary component to allow proper state cleanup during retries.

Comment on lines +77 to +79
const pulse = useDashboardPulse();
const name = useUserName();
const { budget, gamification, loading: pulseLoading } = useDashboardPulse();
const { transactions: recentTx, loading: txLoading } =
const { budget, gamification, loading: pulseLoading, errors: pulseErrors } = useDashboardPulse();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The useDashboardPulse() hook is called twice in this component. The first call assigns the result to pulse (which is unused), and the second call destructures the properties. This is redundant and can cause duplicate side effects or unnecessary re-renders depending on the hook's implementation.

We should remove the unused pulse variable and the duplicate hook call.

Suggested change
const pulse = useDashboardPulse();
const name = useUserName();
const { budget, gamification, loading: pulseLoading } = useDashboardPulse();
const { transactions: recentTx, loading: txLoading } =
const { budget, gamification, loading: pulseLoading, errors: pulseErrors } = useDashboardPulse();
const name = useUserName();
const { budget, gamification, loading: pulseLoading, errors: pulseErrors } = useDashboardPulse();

</div>

{/* API error banner — shown when budget/freeze/gamification fetches fail */}
{pulseErrors.length > 0 && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime errors if pulseErrors is undefined or null, use optional chaining or a logical AND check before accessing the length property.

Suggested change
{pulseErrors.length > 0 && (
{pulseErrors && pulseErrors.length > 0 && (

Comment on lines 42 to 53
useEffect(() => {
if (guestSummary) return;

setFetchError(null);
getBudgetSummary()
.then((data) => setFetchedSummary(data as BudgetSummary))
.catch(() => {
.catch((err) => {
setFetchError(err instanceof Error ? err.message : "Unable to load budget");
setFetchedSummary(MOCK_SUMMARY);
})
.finally(() => setFetchLoading(false));
}, [guestSummary]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a potential race condition and state update on an unmounted component if getBudgetSummary() resolves after the component has unmounted or if guestSummary changes rapidly.

We should use an active flag to ignore the promise resolution if the effect is cleaned up, matching the robust pattern already used in useRecentTransactions.ts.

  useEffect(() => {
    if (guestSummary) return;

    let active = true;
    setFetchError(null);
    getBudgetSummary()
      .then((data) => {
        if (!active) return;
        setFetchedSummary(data as BudgetSummary);
      })
      .catch((err) => {
        if (!active) return;
        setFetchError(err instanceof Error ? err.message : "Unable to load budget");
        setFetchedSummary(MOCK_SUMMARY);
      })
      .finally(() => {
        if (!active) return;
        setFetchLoading(false);
      });

    return () => {
      active = false;
    };
  }, [guestSummary]);

Comment on lines +5 to +9
interface Props {
children: React.ReactNode;
fallback?: React.ReactNode;
onError?: (error: Error, info: React.ErrorInfo) => void;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add an optional onReset callback prop to the ErrorBoundary component. This allows parent components to reset any error-causing state or perform cleanup actions when the user clicks the "Try again" button, preventing immediate re-throws of the same error.

Suggested change
interface Props {
children: React.ReactNode;
fallback?: React.ReactNode;
onError?: (error: Error, info: React.ErrorInfo) => void;
}
interface Props {
children: React.ReactNode;
fallback?: React.ReactNode;
onError?: (error: Error, info: React.ErrorInfo) => void;
onReset?: () => void;
}

this.props.onError?.(error, info);
}

private reset = () => this.setState({ hasError: false, error: null });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Invoke the new onReset callback prop when resetting the error boundary state.

  private reset = () => {
    this.props.onReset?.();
    this.setState({ hasError: false, error: null });
  };

- Remove duplicate useDashboardPulse() call in BehaviorDashboard (was polling API twice)
- Guard pulseErrors.length with nullish check (pulseErrors && pulseErrors.length > 0)
- Add onReset callback prop to ErrorBoundary so parents can clean up state on retry
- Add active flag in useBudget useEffect to prevent state updates on unmounted component

https://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
@timothylee58
timothylee58 marked this pull request as ready for review May 30, 2026 16:34
@timothylee58
timothylee58 merged commit d8de95a into main May 30, 2026
2 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants