WellNuo/app/(tabs)/dashboard.tsx
Sergei 7feca4d54b Add debouncing for refresh buttons to prevent duplicate API calls
Implemented a reusable useDebounce hook to prevent rapid-fire clicks
on refresh buttons throughout the application.

Changes:
- Created hooks/useDebounce.ts with configurable delay and leading/trailing edge options
- Added comprehensive unit tests in hooks/__tests__/useDebounce.test.ts
- Applied debouncing to dashboard WebView refresh button (app/(tabs)/dashboard.tsx)
- Applied debouncing to beneficiary detail pull-to-refresh (app/(tabs)/beneficiaries/[id]/index.tsx)
- Applied debouncing to equipment screen refresh (app/(tabs)/beneficiaries/[id]/equipment.tsx)
- Applied debouncing to all error retry buttons (components/ui/ErrorMessage.tsx)
- Fixed jest.setup.js to properly mock React Native modules
- Added implementation documentation in docs/DEBOUNCE_IMPLEMENTATION.md

Technical details:
- Default 1-second debounce delay
- Leading edge execution (immediate first call, then debounce)
- Type-safe with TypeScript generics
- Automatic cleanup on component unmount

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-29 11:44:16 -08:00

192 lines
5.3 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
import { WebView } from 'react-native-webview';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
import { FullScreenError } from '@/components/ui/ErrorMessage';
import { useDebounce } from '@/hooks/useDebounce';
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
// BUILD VERSION INDICATOR - видно на экране для проверки версии
const BUILD_VERSION = 'v2.1.0';
const BUILD_TIMESTAMP = '2026-01-27 17:05';
export default function DashboardScreen() {
const webViewRef = useRef<WebView>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [canGoBack, setCanGoBack] = useState(false);
const handleRefreshInternal = () => {
setError(null);
setIsLoading(true);
webViewRef.current?.reload();
};
const { debouncedFn: handleRefresh } = useDebounce(handleRefreshInternal, { delay: 1000 });
const handleBack = () => {
if (canGoBack) {
webViewRef.current?.goBack();
}
};
const handleNavigationStateChange = (navState: any) => {
setCanGoBack(navState.canGoBack);
};
const handleError = () => {
setError('Failed to load dashboard. Please check your internet connection.');
setIsLoading(false);
};
if (error) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Dashboard</Text>
</View>
<FullScreenError message={error} onRetry={handleRefresh} />
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top']}>
{/* Header */}
<View style={styles.header}>
{canGoBack && (
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
)}
<Text style={[styles.headerTitle, canGoBack && styles.headerTitleWithBack]}>
Dashboard
</Text>
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
<Ionicons name="refresh" size={22} color={AppColors.primary} />
</TouchableOpacity>
</View>
{/* Build Version Indicator - ALWAYS VISIBLE */}
<View style={styles.buildVersionBadge}>
<Text style={styles.buildVersionText}>
{BUILD_VERSION} {BUILD_TIMESTAMP}
</Text>
</View>
{/* WebView */}
<View style={styles.webViewContainer}>
<WebView
ref={webViewRef}
source={{ uri: DASHBOARD_URL }}
style={styles.webView}
onLoadStart={() => setIsLoading(true)}
onLoadEnd={() => setIsLoading(false)}
onError={handleError}
onHttpError={handleError}
onNavigationStateChange={handleNavigationStateChange}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
allowsBackForwardNavigationGestures={true}
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading dashboard...</Text>
</View>
)}
/>
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={AppColors.primary} />
</View>
)}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
backgroundColor: AppColors.background,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
backButton: {
padding: Spacing.xs,
marginRight: Spacing.sm,
},
headerTitle: {
flex: 1,
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.textPrimary,
},
headerTitleWithBack: {
marginLeft: 0,
},
refreshButton: {
padding: Spacing.xs,
},
webViewContainer: {
flex: 1,
},
webView: {
flex: 1,
},
loadingContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: AppColors.background,
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.8)',
},
loadingText: {
marginTop: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
buildVersionBadge: {
position: 'absolute',
bottom: 60,
right: 10,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
zIndex: 9999,
},
buildVersionText: {
color: '#00FF00',
fontSize: 10,
fontWeight: '600',
fontFamily: 'monospace',
},
});