283 lines
12 KiB
TypeScript
283 lines
12 KiB
TypeScript
import { useAlert } from '@/components/AlertComponent';
|
|
import LoadingScreen from '@/components/LoadingScreen';
|
|
import SetDescriptionModal from '@/components/SetDescriptionModal';
|
|
import api from '@/utils/api';
|
|
import { Image } from 'expo-image';
|
|
import { downloadAndShareDocument } from '@/utils/documentUtils';
|
|
import ImageView from "react-native-image-viewing";
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { ChevronLeft, ImageIcon, Share2, Trash2, Pencil } from 'lucide-react-native';
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
import { Dimensions, RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
|
export default function JournalDetailScreen() {
|
|
const router = useRouter();
|
|
const alert = useAlert();
|
|
const params = useLocalSearchParams();
|
|
const insets = useSafeAreaInsets();
|
|
|
|
const [placeName, setPlaceName] = useState<string>('');
|
|
const [photos, setPhotos] = useState<any[]>([]);
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
|
|
const [descModalVisible, setDescModalVisible] = useState(false);
|
|
const [editIndex, setEditIndex] = useState<number | null>(null);
|
|
|
|
const handleDescription = async (desc: string) => {
|
|
if (editIndex === null) return;
|
|
const photo = photos[editIndex];
|
|
if (!photo) return;
|
|
|
|
setDescModalVisible(false);
|
|
try {
|
|
const response = await api.post('/journal/save-description', {
|
|
id: photo.id,
|
|
description: desc,
|
|
});
|
|
if (response.data?.success) {
|
|
fetchDetails(true);
|
|
alert.showAlert('success', 'Salvato', 'Descrizione aggiornata con successo.');
|
|
} else {
|
|
alert.showAlert('error', 'Errore', response.data?.message || 'Impossibile salvare la descrizione.');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert.showAlert('error', 'Errore', 'Si è verificato un errore durante il salvataggio.');
|
|
}
|
|
};
|
|
|
|
const [isSharing, setIsSharing] = useState(false);
|
|
|
|
const handleShare = async (uri: string) => {
|
|
if (isSharing) return; // Prevent multiple share actions
|
|
setIsSharing(true);
|
|
try {
|
|
const fileName = uri.split('/').pop() || 'immagine.jpg';
|
|
await downloadAndShareDocument('image/jpeg', fileName, uri);
|
|
} catch (error) {
|
|
console.error('Error sharing image:', error);
|
|
alert.showAlert('error', 'Errore', 'Si è verificato un errore durante la condivisione.');
|
|
} finally {
|
|
setIsSharing(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = (id: string) => {
|
|
alert.showConfirm(
|
|
'Sei sicuro?',
|
|
'Vuoi davvero eliminare questa foto?',
|
|
[
|
|
{ text: 'Annulla', style: 'cancel', onPress: () => {} },
|
|
{
|
|
text: 'Elimina',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
try {
|
|
const response = await api.post('/journal/delete-attachment', { id });
|
|
if (response.data?.success) {
|
|
setIsVisible(false);
|
|
fetchDetails(true);
|
|
alert.showAlert('success', 'Eliminata', 'La foto è stata rimossa con successo.');
|
|
} else {
|
|
alert.showAlert('error', 'Errore', response.data?.message || 'Impossibile eliminare la foto.');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert.showAlert('error', 'Errore', 'Impossibile eliminare la foto.');
|
|
}
|
|
}
|
|
}
|
|
]
|
|
);
|
|
};
|
|
|
|
const fetchDetails = useCallback(async (isRefreshing = false) => {
|
|
try {
|
|
if (!isRefreshing) setIsLoading(true);
|
|
|
|
const response = await api.get(`/journal/get-journal-items?id=${params.id}`);
|
|
|
|
if (response.data?.success) {
|
|
setPlaceName(response.data.result?.place_name || '');
|
|
setPhotos(response.data.result?.photos || []);
|
|
} else {
|
|
alert.showAlert('error', 'Errore', 'Impossibile caricare i dettagli.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Errore nel recupero del dettaglio giornale:', error);
|
|
alert.showAlert('error', 'Errore', 'Si è verificato un errore di rete.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, [params.id]);
|
|
|
|
useEffect(() => {
|
|
if (params.id) {
|
|
fetchDetails();
|
|
}
|
|
}, [params.id, fetchDetails]);
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true);
|
|
fetchDetails(true);
|
|
};
|
|
|
|
if (isLoading && !refreshing) {
|
|
return <LoadingScreen />;
|
|
}
|
|
|
|
// Calculate dimensions for the grid layout of photos
|
|
const windowWidth = Dimensions.get('window').width;
|
|
const padding = 40;
|
|
const gap = 8;
|
|
const itemSize = (windowWidth - padding - (gap * 2)) / 3;
|
|
|
|
const imageSource = photos.map(photo => ({ uri: photo.uri }));
|
|
|
|
// Custom Header for the ImageView
|
|
const CustomHeader = ({ imageIndex }: { imageIndex: number }) => {
|
|
const currentPhoto = photos[imageIndex];
|
|
if (!currentPhoto) return null;
|
|
|
|
return (
|
|
<View
|
|
className="bg-white/95 shadow-sm border-b border-gray-100"
|
|
style={{ paddingTop: insets.top }}
|
|
>
|
|
<View className="flex-row items-center justify-between px-4 py-3">
|
|
<View className="flex-row items-center gap-3 flex-1 mr-4">
|
|
<TouchableOpacity onPress={() => setIsVisible(false)} className="p-2 -ml-2 active:opacity-70">
|
|
<ChevronLeft size={28} color="#082963" />
|
|
</TouchableOpacity>
|
|
<Text className="text-[#082963] text-lg font-bold flex-1" numberOfLines={2}>{currentPhoto.date || 'Dettaglio'}</Text>
|
|
</View>
|
|
<View className="flex-row items-center gap-1">
|
|
<TouchableOpacity onPress={() => {
|
|
setEditIndex(imageIndex);
|
|
setDescModalVisible(true);
|
|
}} className="p-2 active:opacity-70">
|
|
<Pencil size={24} color="#082963" />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={() => handleShare(currentPhoto.uri)} disabled={isSharing} className={`p-2 active:opacity-70 ${isSharing ? 'opacity-50' : ''}`}>
|
|
<Share2 size={24} color="#082963" />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={() => handleDelete(currentPhoto.id)} className="p-2 active:opacity-70">
|
|
<Trash2 size={24} color="#082963" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// Custom Footer for the ImageView
|
|
const CustomFooter = ({ imageIndex }: { imageIndex: number }) => {
|
|
const currentPhoto = photos[imageIndex];
|
|
if (!currentPhoto || !currentPhoto.description) return null;
|
|
|
|
return (
|
|
<View
|
|
className="bg-black/60"
|
|
style={{ paddingBottom: insets.bottom }}
|
|
>
|
|
<View className="px-6 py-5">
|
|
<Text className="text-white text-center text-base font-medium leading-relaxed">
|
|
{currentPhoto.description}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View className="flex-1 bg-gray-50">
|
|
{/* Header */}
|
|
<View className="bg-white px-4 pb-6 shadow-sm border-b border-gray-100">
|
|
<SafeAreaView edges={['top']} className='pt-5'>
|
|
<View className='flex-row items-center gap-4 px-2'>
|
|
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
|
|
<ChevronLeft size={28} color="#4b5563" pointerEvents="none" />
|
|
</TouchableOpacity>
|
|
|
|
<Text
|
|
className="text-xl font-bold text-gray-800 leading-tight uppercase flex-1 pr-4"
|
|
numberOfLines={2}
|
|
ellipsizeMode="tail"
|
|
>
|
|
{placeName || 'Dettaglio Giornale'}
|
|
</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
</View>
|
|
|
|
<ScrollView
|
|
contentContainerStyle={{ padding: 20, paddingBottom: 100 }}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1071C2']} />}
|
|
>
|
|
{photos.length === 0 ? (
|
|
<View className="bg-white p-8 rounded-3xl border border-gray-100 items-center justify-center border-dashed mt-4">
|
|
<ImageIcon size={48} color="#d1d5db" />
|
|
<Text className="text-gray-400 font-medium text-center mt-4">Nessuna foto presente per questo giornale.</Text>
|
|
</View>
|
|
) : (
|
|
<View className="flex-row flex-wrap" style={{ gap: gap }}>
|
|
{photos.map((item, index) => (
|
|
<TouchableOpacity
|
|
key={index}
|
|
activeOpacity={0.8}
|
|
onPress={() => {
|
|
setCurrentIndex(index);
|
|
setIsVisible(true);
|
|
}}
|
|
style={{ width: itemSize }}
|
|
className="mb-2"
|
|
>
|
|
<View className="bg-gray-100 rounded-2xl overflow-hidden shadow-sm border border-gray-200 aspect-square items-center justify-center">
|
|
<Image
|
|
source={{ uri: item.uri }}
|
|
style={{ width: '100%', height: '100%' }}
|
|
contentFit="cover"
|
|
transition={200}
|
|
/>
|
|
</View>
|
|
{item.description && (
|
|
<Text className="text-xs text-gray-500 mt-2 text-center" numberOfLines={1}>
|
|
{item.description}
|
|
</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{/* Lightbox / Fullscreen viewer */}
|
|
<ImageView
|
|
images={imageSource}
|
|
imageIndex={currentIndex}
|
|
visible={isVisible}
|
|
onRequestClose={() => setIsVisible(false)}
|
|
HeaderComponent={CustomHeader}
|
|
FooterComponent={CustomFooter}
|
|
backgroundColor={"#EDF1F7"}
|
|
presentationStyle={'fullScreen'}
|
|
/>
|
|
|
|
<SetDescriptionModal
|
|
visible={descModalVisible}
|
|
initialDescription={editIndex !== null ? (photos[editIndex]?.description || '') : ''}
|
|
onClose={() => setDescModalVisible(false)}
|
|
onSave={handleDescription}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|