From 6726232e300b533d08f7b6977b5a687cb7c172e7 Mon Sep 17 00:00:00 2001 From: success-OG Date: Fri, 29 May 2026 23:14:09 +0100 Subject: [PATCH] Commit title: feat(frontend): ESLint config, mobile nav, skeleton loaders, error boundaries --- frontend/.eslintrc.json | 7 ++ frontend/app/components/ErrorBoundary.tsx | 57 ++++++++++++++++ frontend/app/components/Navbar.tsx | 65 +++++++++++++++++-- .../components/dashboard/ActivePoolList.tsx | 25 ++++++- .../dashboard/RecentTransactionsWidget.tsx | 46 +++++++++---- .../dashboard/WalletBalanceCard.tsx | 9 +-- frontend/app/components/ui/Skeleton.tsx | 21 ++++++ frontend/app/dashboard/layout.tsx | 5 +- frontend/app/globals.css | 23 +++++++ frontend/app/layout.tsx | 5 +- frontend/app/savings/layout.tsx | 3 +- frontend/package.json | 3 +- 12 files changed, 241 insertions(+), 28 deletions(-) create mode 100644 frontend/.eslintrc.json create mode 100644 frontend/app/components/ErrorBoundary.tsx create mode 100644 frontend/app/components/ui/Skeleton.tsx diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 000000000..61d70749b --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "extends": ["next/core-web-vitals", "plugin:jsx-a11y/recommended"], + "rules": { + "no-console": ["warn", { "allow": ["warn", "error"] }], + "react/no-unescaped-entities": "error" + } +} diff --git a/frontend/app/components/ErrorBoundary.tsx b/frontend/app/components/ErrorBoundary.tsx new file mode 100644 index 000000000..6dfe180a3 --- /dev/null +++ b/frontend/app/components/ErrorBoundary.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React, { Component, ErrorInfo, ReactNode } from "react"; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; +} + +/** + * React Error Boundary — catches render-time errors in the component tree. + * Note: does NOT catch errors in event handlers (use try/catch there). + */ +class ErrorBoundary extends Component { + state: State = { hasError: false }; + + static getDerivedStateFromError(): State { + return { hasError: true }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error("[ErrorBoundary]", error, info.componentStack); + } + + handleReset = () => this.setState({ hasError: false }); + + render() { + if (this.state.hasError) { + if (this.props.fallback) return this.props.fallback; + return ( +
+

Something went wrong

+

+ An unexpected error occurred. You can try again or refresh the page. +

+ +
+ ); + } + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/app/components/Navbar.tsx b/frontend/app/components/Navbar.tsx index 29739bc09..0e8cc3f2d 100644 --- a/frontend/app/components/Navbar.tsx +++ b/frontend/app/components/Navbar.tsx @@ -1,12 +1,13 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { Loader2, Wallet } from "lucide-react"; import ThemeToggle from "./ThemeToggle"; import { useWallet } from "../context/WalletContext"; import { useToast } from "../context/ToastContext"; +import { useFocusTrap } from "../hooks/useFocusTrap"; interface NavLink { label: string; @@ -36,6 +37,10 @@ const Navbar: React.FC = () => { const { address, network, isConnected, isLoading, error, connect } = useWallet(); const toast = useToast(); const previousConnectedRef = useRef(isConnected); + const menuRef = useRef(null); + const menuButtonRef = useRef(null); + + const closeMenu = useCallback(() => setIsMobileMenuOpen(false), []); const isActiveLink = (href: string): boolean => { return pathname === href || pathname?.startsWith(href + "/") || false; @@ -45,6 +50,45 @@ const Navbar: React.FC = () => { ? `${address.slice(0, 4)}...${address.slice(-4)}` : null; + // Focus trap inside mobile menu + useFocusTrap({ + isOpen: isMobileMenuOpen, + containerRef: menuRef, + onEscape: closeMenu, + }); + + // Body scroll lock when menu is open + useEffect(() => { + if (isMobileMenuOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [isMobileMenuOpen]); + + // Click-outside to close + useEffect(() => { + if (!isMobileMenuOpen) return; + + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if ( + menuRef.current && + !menuRef.current.contains(target) && + menuButtonRef.current && + !menuButtonRef.current.contains(target) + ) { + closeMenu(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [isMobileMenuOpen, closeMenu]); + useEffect(() => { if (error) { toast.error("Wallet connection failed", error); @@ -90,7 +134,10 @@ const Navbar: React.FC = () => { }; return ( -