Save current app progress
This commit is contained in:
@@ -1,15 +1,155 @@
|
|||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
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 { colors } from '../../utils/colors';
|
||||||
import { supabase } from '../../services/supabase';
|
import { supabase } from '../../services/supabase';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
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
|
// @ts-ignore
|
||||||
export default function ProfileScreen({ navigation }) {
|
export default function ProfileScreen({ navigation }) {
|
||||||
const { user } = useAuth();
|
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 handleLogout = async () => {
|
||||||
const { error } = await supabase.auth.signOut();
|
const { error } = await supabase.auth.signOut();
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -52,16 +192,8 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
{/* Stats Row */}
|
{/* Stats Row */}
|
||||||
<View style={styles.statsRow}>
|
<View style={styles.statsRow}>
|
||||||
<View style={styles.statCol}>
|
<View style={styles.statCol}>
|
||||||
<Text style={styles.statNumber}>0</Text>
|
<Text style={styles.statNumber}>{tripsCount}</Text>
|
||||||
<Text style={styles.statLabel}>VIAGENS</Text>
|
<Text style={styles.statLabel}>VIAGENS REALIZADAS</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>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -80,20 +212,28 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.prefTextContainer}>
|
<View style={styles.prefTextContainer}>
|
||||||
<Text style={styles.prefTitle}>Distância Total</Text>
|
<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>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Preference Item 2 */}
|
{/* 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' }]}>
|
<View style={[styles.prefIconContainer, { backgroundColor: '#E8F5E9' }]}>
|
||||||
<Heart color={colors.spotify} size={20} />
|
<Heart color={colors.spotify} size={20} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.prefTextContainer}>
|
<View style={styles.prefTextContainer}>
|
||||||
<Text style={styles.prefTitle}>Género Favorito</Text>
|
<Text style={styles.prefTitle}>Género Favorito</Text>
|
||||||
<Text style={styles.prefSubtitle}>Ainda não definido</Text>
|
<Text style={styles.prefSubtitle}>{favoriteGenre}</Text>
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -105,6 +245,64 @@ export default function ProfileScreen({ navigation }) {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
</ScrollView>
|
</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>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,22 +390,21 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
statsRow: {
|
statsRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'center',
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
statCol: {
|
statCol: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
statNumber: {
|
statNumber: {
|
||||||
fontSize: 22,
|
fontSize: 26,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
color: colors.textMain,
|
color: colors.textMain,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
statLabel: {
|
statLabel: {
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
@@ -279,4 +476,87 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: 'bold',
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user