Phase 8: Polish — error boundaries, loading states, Handsontable v17 fixes

- ErrorBoundary component for top-level render error recovery
- SheetPage: sheetError + loading states before table render
- main.tsx: registerAllModules() for Handsontable v17
- index.html: Montserrat font preconnect
- App.tsx: AdminRoute + ErrorBoundary wrappers
- .gitignore: exclude *.bak files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-23 13:36:12 +00:00
parent 72c50b2c92
commit 38a550bd5f
6 changed files with 73 additions and 1 deletions

1
.gitignore vendored
View file

@ -32,3 +32,4 @@ Thumbs.db
.idea/
.vscode/
*.swp
*.bak

View file

@ -5,6 +5,9 @@
<link rel="icon" type="image/svg+xml" href="/ac-helper/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AC Tool — Oliver Agency</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>

View file

@ -1,5 +1,6 @@
import { useEffect } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import ErrorBoundary from './components/ErrorBoundary'
import { useMsal } from '@azure/msal-react'
import { InteractionStatus } from '@azure/msal-browser'
import { Toaster } from 'react-hot-toast'
@ -64,6 +65,7 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
export default function App() {
return (
<ErrorBoundary>
<BrowserRouter basename="/ac-helper/">
<Toaster
position="bottom-right"
@ -85,5 +87,6 @@ export default function App() {
</AppShell>
</AuthGate>
</BrowserRouter>
</ErrorBoundary>
)
}

View file

@ -0,0 +1,36 @@
import { Component, type ReactNode } from 'react'
interface Props { children: ReactNode }
interface State { error: Error | null }
export default class ErrorBoundary extends Component<Props, State> {
state: State = { error: null }
static getDerivedStateFromError(error: Error): State {
return { error }
}
render() {
if (this.state.error) {
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
height: '100vh', background: '#000', flexDirection: 'column', gap: 16,
}}>
<div style={{ fontSize: 32 }}></div>
<div style={{ color: 'var(--text-primary)', fontWeight: 600 }}>Something went wrong</div>
<div style={{ color: 'var(--text-muted)', fontSize: 12, maxWidth: 400, textAlign: 'center' }}>
{this.state.error.message}
</div>
<button
onClick={() => window.location.reload()}
style={{ background: 'var(--accent)', color: '#000', border: 'none', borderRadius: 6, padding: '8px 20px', cursor: 'pointer', fontSize: 13 }}
>
Reload
</button>
</div>
)
}
return this.props.children
}
}

View file

@ -2,9 +2,13 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { MsalProvider } from '@azure/msal-react'
import { PublicClientApplication } from '@azure/msal-browser'
import { registerAllModules } from 'handsontable/registry'
import './index.css'
import App from './App'
// Register all Handsontable modules (autocomplete, dropdown, context menu, etc.)
registerAllModules()
// MSAL is configured dynamically from /api/auth/config
// We create a placeholder instance here; the real config is loaded in App.tsx
const msalInstance = new PublicClientApplication({

View file

@ -26,14 +26,17 @@ export default function SheetPage() {
const [yolo, setYolo] = useState(false)
const [history, setHistory] = useState('')
const [logs, setLogs] = useState<LogEntry[]>([])
const [sheetError, setSheetError] = useState(false)
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const sheetMeta = sheets.find(s => s.id === sheetId)
const { loading } = useSheetStore()
useEffect(() => {
setSheetError(false)
fetchCategories()
if (sheetId && activeSheetId !== sheetId) {
loadSheet(sheetId)
loadSheet(sheetId).catch(() => setSheetError(true))
}
}, [sheetId])
@ -142,6 +145,26 @@ export default function SheetPage() {
toast.success('Sheet cleared')
}
if (sheetError) {
return (
<div className="flex flex-col items-center justify-center h-full gap-3">
<div style={{ fontSize: 32 }}></div>
<div style={{ color: 'var(--text-primary)', fontWeight: 600 }}>Sheet not found</div>
<div style={{ color: 'var(--text-muted)', fontSize: 13 }}>
This sheet may have been deleted or you don't have access.
</div>
</div>
)
}
if (loading && deliverables.length === 0) {
return (
<div className="flex items-center justify-center h-full">
<div style={{ color: 'var(--text-muted)', fontSize: 13 }}>Loading</div>
</div>
)
}
return (
<div className="flex flex-col h-full gap-3">
{/* Header */}
@ -195,6 +218,8 @@ export default function SheetPage() {
licenseKey="non-commercial-and-evaluation"
stretchH="last"
themeName="ht-theme-main"
autoRowSize={false}
autoColumnSize={false}
/>
)}
</div>