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:
parent
72c50b2c92
commit
38a550bd5f
6 changed files with 73 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -32,3 +32,4 @@ Thumbs.db
|
|||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.bak
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
36
frontend/src/components/ErrorBoundary.tsx
Normal file
36
frontend/src/components/ErrorBoundary.tsx
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue