From 4cb1a35f10936d440f63054cc79d96ab0756a3c6 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Mon, 20 Apr 2026 22:03:58 +0800 Subject: [PATCH] feat: wire connection banner, monitor, and lazyWithRetry into App --- apps/web/src/App.tsx | 90 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 38c51240..eb6f8992 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,39 +1,55 @@ -import { Component, type ErrorInfo, lazy, type ReactNode, Suspense } from "react"; +import { Component, type ErrorInfo, type ReactNode, Suspense } from "react"; import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom"; import { Toaster } from "sonner"; +import { ConnectionBanner } from "./components/common/connection-banner"; import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider"; import { useAuth } from "./hooks/use-auth"; +import { useConnectionMonitor } from "./hooks/use-connection-monitor"; +import { lazyWithRetry } from "./lib/lazy-with-retry"; -// Lazy-load all pages so each page's JS (and its icons/deps) is only -// downloaded when the user navigates there, shrinking the main bundle. -const AutomatePage = lazy(() => +// Lazy-load all pages with automatic retry so chunk failures from +// deployments are recovered transparently instead of white-screening. +const AutomatePage = lazyWithRetry(() => import("./pages/automate-page").then((m) => ({ default: m.AutomatePage })), ); -const ChangePasswordPage = lazy(() => +const ChangePasswordPage = lazyWithRetry(() => import("./pages/change-password-page").then((m) => ({ default: m.ChangePasswordPage })), ); -const FilesPage = lazy(() => import("./pages/files-page").then((m) => ({ default: m.FilesPage }))); -const FullscreenGridPage = lazy(() => +const FilesPage = lazyWithRetry(() => + import("./pages/files-page").then((m) => ({ default: m.FilesPage })), +); +const FullscreenGridPage = lazyWithRetry(() => import("./pages/fullscreen-grid-page").then((m) => ({ default: m.FullscreenGridPage })), ); -const HomePage = lazy(() => import("./pages/home-page").then((m) => ({ default: m.HomePage }))); -const LoginPage = lazy(() => import("./pages/login-page").then((m) => ({ default: m.LoginPage }))); -const PrivacyPolicyPage = lazy(() => +const HomePage = lazyWithRetry(() => + import("./pages/home-page").then((m) => ({ default: m.HomePage })), +); +const LoginPage = lazyWithRetry(() => + import("./pages/login-page").then((m) => ({ default: m.LoginPage })), +); +const PrivacyPolicyPage = lazyWithRetry(() => import("./pages/privacy-policy-page").then((m) => ({ default: m.PrivacyPolicyPage })), ); -const ToolPage = lazy(() => import("./pages/tool-page").then((m) => ({ default: m.ToolPage }))); +const ToolPage = lazyWithRetry(() => + import("./pages/tool-page").then((m) => ({ default: m.ToolPage })), +); class ErrorBoundary extends Component< { children: ReactNode }, - { hasError: boolean; error: Error | null } + { hasError: boolean; error: Error | null; isChunkError: boolean } > { constructor(props: { children: ReactNode }) { super(props); - this.state = { hasError: false, error: null }; + this.state = { hasError: false, error: null, isChunkError: false }; } static getDerivedStateFromError(error: Error) { - return { hasError: true, error }; + const msg = error.message.toLowerCase(); + const isChunkError = + msg.includes("dynamically imported module") || + msg.includes("loading chunk") || + msg.includes("failed to fetch"); + return { hasError: true, error, isChunkError }; } componentDidCatch(error: Error, info: ErrorInfo) { @@ -42,6 +58,43 @@ class ErrorBoundary extends Component< render() { if (this.state.hasError) { + if (this.state.isChunkError) { + return ( +
+
+
+ + Refresh + + +
+

Update Available

+

+ A new version of ashim has been deployed. +

+ +
+
+ ); + } return (
@@ -52,7 +105,7 @@ class ErrorBoundary extends Component<