feat: Implement alert system and network connectivity handling, refactor error handling across screens

This commit is contained in:
2026-01-30 12:38:25 +01:00
parent 9bb8279631
commit 7a6a7f5d35
14 changed files with 277 additions and 40 deletions

View File

@ -0,0 +1,113 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { Modal, View, Text, TouchableOpacity, TouchableWithoutFeedback } from 'react-native';
import { CheckCircle, XCircle, Info, AlertTriangle } from 'lucide-react-native';
type AlertType = 'success' | 'error' | 'info' | 'warning';
interface AlertContextData {
showAlert: (type: AlertType, title: string, message: string) => void;
hideAlert: () => void;
}
const AlertContext = createContext<AlertContextData>({} as AlertContextData);
const ALERT_CONFIG = {
success: {
icon: CheckCircle,
color: 'text-green-600',
bgColor: 'bg-green-100',
btnColor: 'bg-green-600',
},
error: {
icon: XCircle,
color: 'text-red-600',
bgColor: 'bg-red-100',
btnColor: 'bg-red-600',
},
info: {
icon: Info,
color: 'text-sky-600',
bgColor: 'bg-sky-100',
btnColor: 'bg-sky-600',
},
warning: {
icon: AlertTriangle,
color: 'text-orange-600',
bgColor: 'bg-orange-100',
btnColor: 'bg-orange-600',
},
};
export const AlertProvider = ({ children }: { children: ReactNode }) => {
const [visible, setVisible] = useState(false);
const [title, setTitle] = useState('');
const [message, setMessage] = useState('');
const [type, setType] = useState<AlertType>('info');
const showAlert = (newType: AlertType, newTitle: string, newMessage: string) => {
setType(newType);
setTitle(newTitle);
setMessage(newMessage);
setVisible(true);
};
const hideAlert = () => {
setVisible(false);
};
const { icon: Icon, color, bgColor, btnColor } = ALERT_CONFIG[type];
return (
<AlertContext.Provider value={{ showAlert, hideAlert }}>
{children}
{/* IL COMPONENTE ALERT MODALE */}
<Modal
transparent
visible={visible}
animationType="fade"
onRequestClose={hideAlert}
>
{/* Backdrop scuro */}
<TouchableOpacity
activeOpacity={1}
onPress={hideAlert} // Chiude se clicchi fuori (opzionale)
className="flex-1 bg-black/60 justify-center items-center px-6"
>
{/* Contenitore Alert */}
<TouchableWithoutFeedback>
<View className="bg-white w-full max-w-sm rounded-3xl p-6 items-center shadow-2xl">
{/* Icona Cerchiata */}
<View className={`${bgColor} p-4 rounded-full mb-4`}>
<Icon size={32} className={color} strokeWidth={2.5} />
</View>
{/* Testi */}
<Text className="text-xl font-bold text-gray-900 text-center mb-2">
{title}
</Text>
<Text className="text-lg text-gray-500 text-center leading-relaxed mb-8">
{message}
</Text>
{/* Pulsante OK */}
<TouchableOpacity
onPress={hideAlert}
className={`w-full py-3.5 rounded-xl ${btnColor} active:opacity-90 shadow-sm`}
>
<Text className="text-white text-center font-bold text-lg">
Ok, ho capito
</Text>
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
</TouchableOpacity>
</Modal>
</AlertContext.Provider>
);
};
export const useAlert = () => useContext(AlertContext);

View File

@ -0,0 +1,48 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WifiOff, RefreshCw } from 'lucide-react-native';
interface OfflineScreenProps {
onRetry: () => void;
isRetrying?: boolean;
}
export default function OfflineScreen({ onRetry, isRetrying = false }: OfflineScreenProps) {
return (
<SafeAreaView className="flex-1 bg-white">
<View className="flex-1 items-center justify-center px-8">
{/* Icona con cerchio di sfondo */}
<View className="bg-gray-100 p-6 rounded-full mb-6">
<WifiOff size={64} className="text-gray-400" />
</View>
<Text className="text-2xl font-bold text-gray-800 mb-2 text-center">
Sei Offline
</Text>
<Text className="text-base text-gray-500 text-center mb-10 leading-6">
Sembra che non ci sia connessione a internet.{'\n'}Controlla il Wi-Fi o i dati mobili e riprova.
</Text>
{/* Pulsante Riprova */}
<TouchableOpacity
onPress={onRetry}
disabled={isRetrying}
className={`flex-row items-center justify-center w-full py-4 rounded-xl gap-4 ${
isRetrying ? 'bg-gray-300' : 'bg-[#099499] active:bg-[#077f83]'
}`}
>
<RefreshCw
size={20}
color="white"
className={`${isRetrying ? 'animate-spin' : ''}`} // TODO: animate-spin richiede config tailwind o gestiscilo manualmente
/>
<Text className="text-white font-bold text-lg">
{isRetrying ? 'Controllo...' : 'Riprova'}
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}

View File

@ -1,5 +1,6 @@
import { useAlert } from '@/components/AlertComponent';
import React, { useState } from 'react';
import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native';
import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView } from 'react-native';
import DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-datepicker';
import { TimeOffRequestType } from '@/types/types';
import { X } from 'lucide-react-native';
@ -16,6 +17,7 @@ interface RequestPermitModalProps {
export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) {
const defaultStyles = useDefaultStyles();
const alert = useAlert();
const [type, setType] = useState<TimeOffRequestType>(types[0]); // Default to first type
const [date, setDate] = useState<string | null>();
const [range, setRange] = useState<{
@ -61,9 +63,9 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
const response = await api.post('/time-off-request/save-request', requestData);
if (response.data.status === 'success') {
Alert.alert('Successo', response.data.message || 'La tua richiesta è stata inviata con successo.');
alert.showAlert('success', 'Successo', response.data.message || 'La tua richiesta è stata inviata con successo.');
} else {
Alert.alert('Errore', response.data.message || 'Impossibile inviare la richiesta.');
alert.showAlert('error', 'Errore', response.data.message || 'Impossibile inviare la richiesta.');
}
} catch (error: any) {
console.error('Errore nell\'invio della richiesta:', error);
@ -74,7 +76,10 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
// Funzione per inviare la richiesta
const handleSubmit = async () => {
const error = validateRequest(type, date, range, startTime, endTime, message);
if (error) return Alert.alert("Errore", error);
if (error) {
alert.showAlert("error", "Errore", error);
return;
}
const requestData = {
id_type: type.id,
@ -90,7 +95,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
onSubmit(requestData); // TODO: Gestire risposta e controllare fetch in index?
onClose();
} catch (e) {
Alert.alert("Errore", "Impossibile inviare la richiesta.");
alert.showAlert("error", "Errore", "Impossibile inviare la richiesta.");
}
};