Files
bedolaga-cabinet/src/components/ErrorBoundary.tsx
c0mrade 8e0b63bac8 feat(a11y): cross-platform hardening + modal focus-trap from impeccable audit
Responsive / viewport:
- add .min-h-viewport / .h-viewport utilities (100dvh + Telegram --tg-viewport-stable-height)
- migrate min-h-screen / h-screen / 100vh across 18 files
- reveal hover-only controls on focus-within + touch (desktop nav, admin settings)
- grid breakpoints (TopUpAmount, Contests), larger touch targets, nav aria-labels

Platform routing (Telegram WebView correctness):
- clipboard -> copyToClipboard util with execCommand fallback (10 files)
- window.open -> openTelegramLink / openLink (Profile, Referral, ChannelSubscriptionScreen)
- window.confirm -> useNativeDialog (admin pages); PromoOffersSection -> useDestructiveConfirm
- window.prompt -> usePrompt / PromptDialogHost (TipTap link editors)
- ESLint no-restricted-properties guard against regressions (adapters/clipboard util exempt)

Modal accessibility:
- new useFocusTrap hook (focus trap, Esc, scroll lock, focus restore)
- role=dialog / aria-modal / focus-trap: Polls, SuccessNotificationModal, AdminPolicies,
  AdminPromoGroups, AdminCampaigns, BroadcastPreview (Telegram + Email)
- new global usePrompt store + PromptDialogHost
2026-05-25 23:16:07 +03:00

122 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Component, type ErrorInfo, type ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
level?: 'app' | 'page' | 'widget';
onReset?: () => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
function isChunkLoadError(error: Error): boolean {
const msg = error.message || '';
return (
msg.includes('dynamically imported module') ||
msg.includes('Importing a module script failed') ||
msg.includes('Failed to fetch dynamically imported module') ||
msg.includes('Loading chunk') ||
msg.includes('ChunkLoadError') ||
error.name === 'ChunkLoadError'
);
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[ErrorBoundary]', error, errorInfo);
// Auto-reload on chunk load failures (stale deploy)
if (isChunkLoadError(error)) {
const reloadKey = 'chunk_reload_ts';
const lastReload = Number(sessionStorage.getItem(reloadKey) || '0');
const now = Date.now();
// Prevent reload loop — only auto-reload once per 30 seconds
if (now - lastReload > 30_000) {
sessionStorage.setItem(reloadKey, String(now));
window.location.reload();
return;
}
}
}
handleReset = () => {
this.setState({ hasError: false, error: null });
this.props.onReset?.();
};
render() {
if (!this.state.hasError) {
return this.props.children;
}
const { level = 'page' } = this.props;
const isChunk = this.state.error ? isChunkLoadError(this.state.error) : false;
if (level === 'app') {
return (
<div className="min-h-viewport flex items-center justify-center bg-dark-900 p-4">
<div className="max-w-md text-center">
<div className="mb-4 text-4xl"></div>
<h1 className="mb-2 text-xl font-bold text-dark-50">Something went wrong</h1>
<p className="mb-6 text-dark-400">
An unexpected error occurred. Please try reloading the page.
</p>
<button
onClick={() => window.location.reload()}
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-white transition-colors hover:bg-accent-600"
>
Reload page
</button>
</div>
</div>
);
}
if (level === 'widget') {
return (
<div className="rounded-xl border border-error-500/30 bg-error-500/10 p-4 text-center">
<p className="text-sm text-error-400">Failed to load this section</p>
<button
onClick={this.handleReset}
className="mt-2 text-sm text-accent-400 hover:text-accent-300"
>
Try again
</button>
</div>
);
}
// level === 'page' (default)
return (
<div className="flex min-h-[50vh] items-center justify-center p-4">
<div className="max-w-md text-center">
<div className="mb-4 text-4xl"></div>
<h1 className="mb-2 text-xl font-bold text-dark-50">Something went wrong</h1>
<p className="mb-6 text-sm text-dark-400">
{isChunk
? 'App was updated. Reloading...'
: this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
onClick={() => window.location.reload()}
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-white transition-colors hover:bg-accent-600"
>
{isChunk ? 'Reload' : 'Try again'}
</button>
</div>
</div>
);
}
}