401 lines
11 KiB
TypeScript
401 lines
11 KiB
TypeScript
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ImageBackground, Alert, ActivityIndicator } from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { NavigationProp, useFocusEffect } from '@react-navigation/native';
|
|
import { Navigation, Clock, MapPin } from 'lucide-react-native';
|
|
import { colors } from '../../utils/colors';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import { supabase } from '../../services/supabase';
|
|
import { getDestinationLandmarkImage } from '../../services/destinationImage';
|
|
|
|
interface Props {
|
|
navigation: NavigationProp<any>;
|
|
}
|
|
|
|
type TripCardProps = {
|
|
trip: any;
|
|
};
|
|
|
|
const maskGoogleKeyInImageUrl = (url: string | null) => url?.replace(/([?&]key=)[^&]+/, '$1[REDACTED]') || null;
|
|
|
|
function TripCard({ trip }: TripCardProps) {
|
|
const [imageUrl, setImageUrl] = useState<string | null>(trip.destination_image_url || null);
|
|
const [landmarkName, setLandmarkName] = useState<string | null>(trip.destination_landmark_name || null);
|
|
const [placeId, setPlaceId] = useState<string | null>(trip.destination_place_id || trip.place_id || null);
|
|
const tripTitle = trip.title || trip.destination || 'Viagem';
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
console.log('[TripCardImageDebug] initial', {
|
|
tripName: tripTitle,
|
|
destination: trip.destination,
|
|
placeId: trip.destination_place_id || trip.place_id || null,
|
|
imageUrl: trip.destination_image_url || null,
|
|
});
|
|
|
|
if (trip.destination_image_url || !trip.destination) return;
|
|
|
|
getDestinationLandmarkImage(trip.destination, { tripTitle }).then(result => {
|
|
if (!isMounted) return;
|
|
setImageUrl(result.imageUrl);
|
|
setLandmarkName(result.landmarkName);
|
|
setPlaceId(result.placeId);
|
|
console.log('[TripCardImageDebug] fetched', {
|
|
tripName: tripTitle,
|
|
destination: trip.destination,
|
|
placeId: result.placeId,
|
|
imageUrl: maskGoogleKeyInImageUrl(result.imageUrl),
|
|
source: result.source,
|
|
fallbackReason: result.fallbackReason,
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [trip.destination, trip.destination_image_url, trip.destination_place_id, trip.place_id, tripTitle]);
|
|
|
|
useEffect(() => {
|
|
console.log('[TripCardImageDebug] render_image_uri', {
|
|
tripName: tripTitle,
|
|
destination: trip.destination,
|
|
placeId,
|
|
imageUrl: maskGoogleKeyInImageUrl(imageUrl),
|
|
hasValidRemoteUri: Boolean(imageUrl?.startsWith('https://')),
|
|
fallbackReason: imageUrl ? null : 'missing_image_url',
|
|
});
|
|
}, [trip.destination, tripTitle, placeId, imageUrl]);
|
|
|
|
const imageContent = (
|
|
<View style={styles.imageOverlay}>
|
|
<Text style={styles.tripImageTitle} numberOfLines={2}>{tripTitle}</Text>
|
|
<Text style={styles.tripImageSubtitle} numberOfLines={1}>
|
|
{landmarkName || trip.destination}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
return (
|
|
<View style={styles.mainTripCard}>
|
|
{imageUrl ? (
|
|
<ImageBackground source={{ uri: imageUrl }} style={styles.tripImage} imageStyle={styles.tripImageStyle}>
|
|
{imageContent}
|
|
</ImageBackground>
|
|
) : (
|
|
<View style={[styles.tripImage, styles.tripImageFallback]}>
|
|
<View style={styles.tripImageFallbackGlow} />
|
|
<View style={styles.tripImageFallbackAccent} />
|
|
{imageContent}
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.tripContent}>
|
|
<View style={styles.statsRow}>
|
|
<View style={styles.statItem}>
|
|
<Navigation color={colors.primary} size={16} />
|
|
<Text style={styles.statText}>{trip.distance}</Text>
|
|
</View>
|
|
<View style={styles.statDot} />
|
|
<View style={styles.statItem}>
|
|
<Clock color={colors.primary} size={16} />
|
|
<Text style={styles.statText}>{trip.duration}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.itinerarySnippet}>
|
|
<View style={styles.timeline}>
|
|
<View style={styles.dot} />
|
|
<View style={styles.line} />
|
|
<View style={[styles.dot, styles.dotEmpty]} />
|
|
</View>
|
|
<View style={styles.locations}>
|
|
<Text style={styles.locationText}>{trip.origin}</Text>
|
|
<Text style={styles.locationText}>{trip.destination}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export default function HomeScreen({ navigation }: Props) {
|
|
const { user } = useAuth();
|
|
const userName = user?.user_metadata?.display_name || user?.user_metadata?.name || user?.email || user?.user_metadata?.email || 'Viajante';
|
|
const initial = userName.charAt(0).toUpperCase();
|
|
|
|
const [trips, setTrips] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const fetchTrips = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
console.log('[HomeScreen] Fetching trips. User exists:', !!user);
|
|
let query = supabase.from('trips').select('*').order('created_at', { ascending: false });
|
|
|
|
if (user) {
|
|
query = query.eq('user_id', user.id);
|
|
} else {
|
|
query = query.is('user_id', null);
|
|
}
|
|
|
|
const { data, error } = await query;
|
|
|
|
if (error) {
|
|
console.error('[HomeScreen] Supabase error fetching trips:', error);
|
|
throw error;
|
|
}
|
|
|
|
console.log(`[HomeScreen] Fetched ${data?.length || 0} trips`);
|
|
setTrips(data || []);
|
|
} catch (error: any) {
|
|
console.error('[HomeScreen] Error loading trips:', error.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [user]);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
fetchTrips();
|
|
}, [fetchTrips])
|
|
);
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
|
|
{/* Header Section */}
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>As Tuas Viagens</Text>
|
|
<View style={styles.avatar}>
|
|
<Text style={styles.avatarText}>{initial}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{loading ? (
|
|
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 40 }} />
|
|
) : trips.length > 0 ? (
|
|
trips.map(trip => (
|
|
<TouchableOpacity
|
|
key={trip.id}
|
|
activeOpacity={0.9}
|
|
onPress={() => navigation.navigate('TripDetails', { trip })}
|
|
>
|
|
<TripCard trip={trip} />
|
|
</TouchableOpacity>
|
|
))
|
|
) : (
|
|
<View style={styles.promptCard}>
|
|
<Text style={styles.promptTitle}>Pronto para a próxima?</Text>
|
|
<Text style={styles.promptSubtitle}>
|
|
Descobre a melhor rota e a banda sonora{'\n'}perfeita para o caminho.
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.promptButton}
|
|
onPress={() => navigation.navigate('NewTripModal')}
|
|
>
|
|
<Text style={styles.promptButtonText}>Criar Nova Viagem</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: colors.background,
|
|
},
|
|
scrollContent: {
|
|
paddingHorizontal: 20,
|
|
paddingTop: 10,
|
|
paddingBottom: 120, // Extra space for bottom tab bar and floating button
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 24,
|
|
},
|
|
title: {
|
|
fontSize: 26,
|
|
fontWeight: '800',
|
|
color: colors.textMain,
|
|
},
|
|
avatar: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
backgroundColor: '#FFE5D4', // Light orange
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
avatarText: {
|
|
color: colors.primaryDark,
|
|
fontWeight: 'bold',
|
|
fontSize: 16,
|
|
},
|
|
mainTripCard: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 24,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 8 },
|
|
shadowOpacity: 0.05,
|
|
shadowRadius: 12,
|
|
elevation: 4,
|
|
marginBottom: 24,
|
|
},
|
|
tripImage: {
|
|
height: 180,
|
|
justifyContent: 'flex-end',
|
|
overflow: 'hidden',
|
|
borderTopLeftRadius: 24,
|
|
borderTopRightRadius: 24,
|
|
},
|
|
tripImageStyle: {
|
|
borderTopLeftRadius: 24,
|
|
borderTopRightRadius: 24,
|
|
},
|
|
tripImageFallback: {
|
|
backgroundColor: '#2B2018',
|
|
position: 'relative',
|
|
},
|
|
tripImageFallbackGlow: {
|
|
position: 'absolute',
|
|
top: -50,
|
|
right: -40,
|
|
width: 180,
|
|
height: 180,
|
|
borderRadius: 90,
|
|
backgroundColor: 'rgba(255, 122, 0, 0.35)',
|
|
},
|
|
tripImageFallbackAccent: {
|
|
position: 'absolute',
|
|
left: -30,
|
|
bottom: -60,
|
|
width: 190,
|
|
height: 190,
|
|
borderRadius: 95,
|
|
backgroundColor: 'rgba(255, 214, 180, 0.16)',
|
|
},
|
|
imageOverlay: {
|
|
padding: 20,
|
|
paddingTop: 80,
|
|
backgroundColor: 'rgba(0,0,0,0.42)',
|
|
borderTopLeftRadius: 24,
|
|
borderTopRightRadius: 24,
|
|
},
|
|
tripImageTitle: {
|
|
color: colors.white,
|
|
fontSize: 24,
|
|
fontWeight: '800',
|
|
marginBottom: 6,
|
|
},
|
|
tripImageSubtitle: {
|
|
color: colors.white,
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
opacity: 0.92,
|
|
},
|
|
tripContent: {
|
|
padding: 20,
|
|
},
|
|
statsRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 20,
|
|
},
|
|
statItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
},
|
|
statText: {
|
|
fontSize: 15,
|
|
fontWeight: 'bold',
|
|
color: colors.textSecondary,
|
|
},
|
|
statDot: {
|
|
width: 4,
|
|
height: 4,
|
|
borderRadius: 2,
|
|
backgroundColor: colors.inputBorder,
|
|
marginHorizontal: 12,
|
|
},
|
|
itinerarySnippet: {
|
|
flexDirection: 'row',
|
|
backgroundColor: colors.background,
|
|
padding: 16,
|
|
borderRadius: 16,
|
|
},
|
|
timeline: {
|
|
alignItems: 'center',
|
|
marginRight: 12,
|
|
paddingVertical: 4,
|
|
},
|
|
dot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: colors.primary,
|
|
},
|
|
dotEmpty: {
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 2,
|
|
borderColor: colors.primary,
|
|
},
|
|
line: {
|
|
width: 2,
|
|
height: 20,
|
|
backgroundColor: colors.inputBorder,
|
|
marginVertical: 2,
|
|
},
|
|
locations: {
|
|
justifyContent: 'space-between',
|
|
},
|
|
locationText: {
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
color: colors.textSecondary,
|
|
},
|
|
promptCard: {
|
|
backgroundColor: '#FFF5EB', // Very light orange
|
|
borderRadius: 24,
|
|
padding: 24,
|
|
borderWidth: 2,
|
|
borderColor: '#FFD4B8', // Dashed border color approx
|
|
borderStyle: 'dashed',
|
|
alignItems: 'center',
|
|
},
|
|
promptTitle: {
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
color: '#8C3800', // Dark brownish orange
|
|
marginBottom: 8,
|
|
},
|
|
promptSubtitle: {
|
|
fontSize: 14,
|
|
color: '#B34700',
|
|
textAlign: 'center',
|
|
marginBottom: 20,
|
|
lineHeight: 20,
|
|
},
|
|
promptButton: {
|
|
backgroundColor: colors.primary,
|
|
paddingHorizontal: 24,
|
|
paddingVertical: 14,
|
|
borderRadius: 20,
|
|
width: '100%',
|
|
alignItems: 'center',
|
|
},
|
|
promptButtonText: {
|
|
color: colors.white,
|
|
fontWeight: 'bold',
|
|
fontSize: 16,
|
|
},
|
|
});
|