Save current app progress

This commit is contained in:
2026-05-28 15:38:56 +01:00
parent 3a46a7f3d3
commit 134789cee1

View File

@@ -1,15 +1,155 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView } from 'react-native';
import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Settings, Music, Map as MapIcon, Heart, LogOut } from 'lucide-react-native';
import { Settings, Music, Map as MapIcon, Heart, LogOut, X } from 'lucide-react-native';
import { colors } from '../../utils/colors';
import { supabase } from '../../services/supabase';
import { useAuth } from '../../contexts/AuthContext';
import { useFocusEffect } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage';
const GENRES = [
'Pop', 'Rock', 'Hip-Hop', 'Rap', 'Indie', 'Eletrónica',
'Fado', 'Funk', 'Jazz', 'Classical', 'Reggaeton', 'Outro'
];
function parseDistanceToKm(distanceStr: string | null | undefined): number {
if (!distanceStr) return 0;
// Clean all spaces (including non-breaking spaces)
const cleanStr = distanceStr.replace(/\u00A0/g, '').replace(/\s+/g, '').toLowerCase();
// Extract number part
const match = cleanStr.match(/[\d.,]+/);
if (!match) return 0;
const numStr = match[0];
let val = 0;
// Parse numeric value based on localized decimal and thousands separators
if (numStr.includes('.') && numStr.includes(',')) {
if (numStr.indexOf('.') < numStr.indexOf(',')) {
// e.g. "1.234,5" -> dot thousands, comma decimal
val = parseFloat(numStr.replace(/\./g, '').replace(/,/g, '.'));
} else {
// e.g. "1,234.5" -> comma thousands, dot decimal
val = parseFloat(numStr.replace(/,/g, ''));
}
} else if (numStr.includes(',')) {
const parts = numStr.split(',');
if (parts[parts.length - 1].length === 3) {
// Thousands separator: e.g. "1,234" -> 1234
val = parseFloat(numStr.replace(/,/g, ''));
} else {
// Decimal separator: e.g. "12,5" -> 12.5
val = parseFloat(numStr.replace(/,/g, '.'));
}
} else if (numStr.includes('.')) {
const parts = numStr.split('.');
if (parts[parts.length - 1].length === 3) {
// Thousands separator: e.g. "1.234" -> 1234
val = parseFloat(numStr.replace(/\./g, ''));
} else {
// Decimal separator: e.g. "1.2" -> 1.2
val = parseFloat(numStr);
}
} else {
val = parseFloat(numStr);
}
if (isNaN(val)) return 0;
// Apply unit conversion if necessary
if (cleanStr.endsWith('mi') || cleanStr.endsWith('miles') || cleanStr.endsWith('milhas')) {
return val * 1.60934;
} else if (cleanStr.endsWith('m') && !cleanStr.endsWith('km')) {
return val / 1000;
} else if (cleanStr.endsWith('ft') || cleanStr.endsWith('feet') || cleanStr.endsWith('pés')) {
return val / 3280.84;
}
return val;
}
// @ts-ignore
export default function ProfileScreen({ navigation }) {
const { user } = useAuth();
const [tripsCount, setTripsCount] = useState(0);
const [totalDistance, setTotalDistance] = useState(0);
const [favoriteGenre, setFavoriteGenre] = useState('Ainda não definido');
const [selectedGenre, setSelectedGenre] = useState('Pop');
const [isGenreModalVisible, setIsGenreModalVisible] = useState(false);
const loadProfileData = useCallback(async () => {
try {
// 1. Fetch favorite genre from AsyncStorage
const storageKey = user ? `@favorite_genre_${user.id}` : '@favorite_genre_guest';
const storedGenre = await AsyncStorage.getItem(storageKey);
const activeGenre = storedGenre || 'Ainda não definido';
setFavoriteGenre(activeGenre);
if (storedGenre) {
setSelectedGenre(storedGenre);
}
// 2. Fetch user's trips from Supabase
let query = supabase.from('trips').select('distance');
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('Error fetching trips for profile stats:', error);
} else {
const count = data ? data.length : 0;
let sumKm = 0;
if (data) {
data.forEach(trip => {
if (trip.distance) {
sumKm += parseDistanceToKm(trip.distance);
}
});
}
const roundedDistance = Math.round(sumKm);
setTripsCount(count);
setTotalDistance(roundedDistance);
// Print the safe logs exactly as requested:
console.log('PROFILE_TRIPS_COUNT', count);
console.log('PROFILE_TOTAL_DISTANCE_KM', roundedDistance);
console.log('PROFILE_FAVORITE_GENRE', activeGenre);
}
} catch (err) {
console.error('Error loading profile statistics:', err);
}
}, [user]);
useFocusEffect(
useCallback(() => {
loadProfileData();
}, [loadProfileData])
);
const handleSaveGenre = async () => {
try {
const storageKey = user ? `@favorite_genre_${user.id}` : '@favorite_genre_guest';
await AsyncStorage.setItem(storageKey, selectedGenre);
setFavoriteGenre(selectedGenre);
setIsGenreModalVisible(false);
// Print the updated safe logs:
console.log('PROFILE_TRIPS_COUNT', tripsCount);
console.log('PROFILE_TOTAL_DISTANCE_KM', totalDistance);
console.log('PROFILE_FAVORITE_GENRE', selectedGenre);
} catch (err) {
console.error('Error saving favorite genre:', err);
}
};
const handleLogout = async () => {
const { error } = await supabase.auth.signOut();
if (error) {
@@ -52,16 +192,8 @@ export default function ProfileScreen({ navigation }) {
{/* Stats Row */}
<View style={styles.statsRow}>
<View style={styles.statCol}>
<Text style={styles.statNumber}>0</Text>
<Text style={styles.statLabel}>VIAGENS</Text>
</View>
<View style={styles.statCol}>
<Text style={styles.statNumber}>0</Text>
<Text style={styles.statLabel}>SEGUIDORES</Text>
</View>
<View style={styles.statCol}>
<Text style={styles.statNumber}>0</Text>
<Text style={styles.statLabel}>A SEGUIR</Text>
<Text style={styles.statNumber}>{tripsCount}</Text>
<Text style={styles.statLabel}>VIAGENS REALIZADAS</Text>
</View>
</View>
@@ -80,20 +212,28 @@ export default function ProfileScreen({ navigation }) {
</View>
<View style={styles.prefTextContainer}>
<Text style={styles.prefTitle}>Distância Total</Text>
<Text style={styles.prefSubtitle}>0 km conduzidos</Text>
<Text style={styles.prefSubtitle}>{totalDistance} km conduzidos</Text>
</View>
</View>
{/* Preference Item 2 */}
<View style={[styles.prefItem, { marginBottom: 0, marginTop: 24 }]}>
<TouchableOpacity
style={[styles.prefItem, { marginBottom: 0, marginTop: 24 }]}
onPress={() => {
if (favoriteGenre !== 'Ainda não definido') {
setSelectedGenre(favoriteGenre);
}
setIsGenreModalVisible(true);
}}
>
<View style={[styles.prefIconContainer, { backgroundColor: '#E8F5E9' }]}>
<Heart color={colors.spotify} size={20} />
</View>
<View style={styles.prefTextContainer}>
<Text style={styles.prefTitle}>Género Favorito</Text>
<Text style={styles.prefSubtitle}>Ainda não definido</Text>
</View>
<Text style={styles.prefSubtitle}>{favoriteGenre}</Text>
</View>
</TouchableOpacity>
</View>
</View>
@@ -105,6 +245,64 @@ export default function ProfileScreen({ navigation }) {
</TouchableOpacity>
</ScrollView>
{/* Genre Picker Modal */}
<Modal
visible={isGenreModalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setIsGenreModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Género Musical Favorito</Text>
<TouchableOpacity
style={styles.closeModalButton}
onPress={() => setIsGenreModalVisible(false)}
>
<X color={colors.textMain} size={20} />
</TouchableOpacity>
</View>
<Text style={styles.modalSubtitle}>
Escolhe o teu género musical favorito para personalizar as tuas bandas sonoras de viagem:
</Text>
<View style={styles.genresContainer}>
{GENRES.map((genre) => {
const isSelected = selectedGenre === genre;
return (
<TouchableOpacity
key={genre}
style={[
styles.genreTag,
isSelected && styles.genreTagSelected
]}
onPress={() => setSelectedGenre(genre)}
>
<Text
style={[
styles.genreTagText,
isSelected && styles.genreTagTextSelected
]}
>
{genre}
</Text>
</TouchableOpacity>
);
})}
</View>
<TouchableOpacity
style={styles.saveGenreButton}
onPress={handleSaveGenre}
>
<Text style={styles.saveGenreButtonText}>Guardar Preferência</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
@@ -192,22 +390,21 @@ const styles = StyleSheet.create({
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
justifyContent: 'center',
paddingHorizontal: 10,
marginBottom: 24,
},
statCol: {
alignItems: 'center',
flex: 1,
},
statNumber: {
fontSize: 22,
fontSize: 26,
fontWeight: '800',
color: colors.textMain,
marginBottom: 4,
},
statLabel: {
fontSize: 11,
fontSize: 12,
fontWeight: 'bold',
color: colors.textSecondary,
letterSpacing: 1,
@@ -279,4 +476,87 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: 'bold',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.white,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
padding: 24,
paddingBottom: 40,
maxHeight: '80%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.textMain,
},
closeModalButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.inputBackground,
justifyContent: 'center',
alignItems: 'center',
},
modalSubtitle: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: 20,
lineHeight: 20,
},
genresContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 30,
},
genreTag: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: colors.inputBackground,
borderWidth: 1,
borderColor: colors.inputBorder,
marginRight: 10,
marginBottom: 10,
},
genreTagSelected: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
genreTagText: {
fontSize: 14,
fontWeight: '600',
color: colors.textMain,
},
genreTagTextSelected: {
color: colors.white,
},
saveGenreButton: {
backgroundColor: colors.primary,
paddingVertical: 16,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
},
saveGenreButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: 'bold',
},
});